From dd3ac6a0227e9ea7ce44ec3c7be5b23977a35c91 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Wed, 11 Mar 2026 09:54:47 -0300 Subject: [PATCH 01/29] refactor(types): Rename Data to JSON and DataFrame to Table (#11554) * change types Data -> JSON / DataFrame -> Table * fix table-json types on new components * test: Update notify component test for new input types Update test expectation to include JSON and Table types alongside Data and DataFrame for backward compatibility. Co-Authored-By: Claude Opus 4.5 * [autofix.ci] apply automated fixes * ruff style and checker * [autofix.ci] apply automated fixes * json fixer * [autofix.ci] apply automated fixes * change typos name on components * add more tests * ruff fixes * ruff fixes tests * code rabbit suggestions * fix broken test * [autofix.ci] apply automated fixes * change df and d operations to fit new names * [autofix.ci] apply automated fixes * fix fe tests * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * fix templates --------- Co-authored-by: Claude Opus 4.5 Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../base/langflow/initial_setup/setup.py | 41 +- .../Basic Prompt Chaining.json | 12 +- .../starter_projects/Basic Prompting.json | 12 +- .../starter_projects/Blog Writer.json | 48 +- .../Custom Component Generator.json | 41 +- .../starter_projects/Document Q&A.json | 16 +- .../Financial Report Parser.json | 46 +- .../starter_projects/Hybrid Search RAG.json | 80 +- .../Image Sentiment Analysis.json | 42 +- .../Instagram Copywriter.json | 22 +- .../starter_projects/Invoice Summarizer.json | 17 +- .../starter_projects/Knowledge Retrieval.json | 23 +- .../starter_projects/Market Research.json | 56 +- .../starter_projects/Meeting Summary.json | 93 +- .../starter_projects/Memory Chatbot.json | 25 +- .../starter_projects/News Aggregator.json | 36 +- .../starter_projects/Nvidia Remix.json | 42 +- .../Pok\303\251dex Agent.json" | 34 +- .../Portfolio Website Code Generator.json | 43 +- .../starter_projects/Price Deal Finder.json | 29 +- .../starter_projects/Research Agent.json | 22 +- .../Research Translation Loop.json | 81 +- .../SEO Keyword Generator.json | 10 +- .../starter_projects/SaaS Pricing.json | 24 +- .../starter_projects/Search agent.json | 24 +- .../Sequential Tasks Agents.json | 45 +- .../starter_projects/Simple Agent.json | 32 +- .../starter_projects/Social Media Agent.json | 23 +- .../Text Sentiment Analysis.json | 25 +- .../Travel Planning Agents.json | 45 +- .../Twitter Thread Generator.json | 12 +- .../starter_projects/Vector Store RAG.json | 68 +- .../starter_projects/Youtube Analysis.json | 71 +- src/backend/base/langflow/schema/data.py | 7 +- .../components/flow_controls/test_listen.py | 2 +- .../flow_controls/test_notify_component.py | 3 +- .../components/tools/test_python_repl_tool.py | 4 +- .../utils/__tests__/typeCompatibility.test.ts | 260 ++ src/frontend/src/utils/reactflowUtils.ts | 287 +- src/frontend/src/utils/styleUtils.ts | 5 + .../tests/core/features/composio.spec.ts | 2 +- .../tests/core/features/filterSidebar.spec.ts | 2 +- .../tests/core/features/stop-building.spec.ts | 2 +- .../Image Sentiment Analysis.spec.ts | 4 +- .../core/integrations/Market Research.spec.ts | 2 +- .../core/integrations/decisionFlow.spec.ts | 8 +- .../core/integrations/similarity.spec.ts | 4 +- .../extended/features/loop-component.spec.ts | 12 +- .../extended/integrations/duckduckgo.spec.ts | 2 +- src/lfx/src/lfx/_assets/component_index.json | 2715 +++++++++-------- .../src/lfx/_assets/stable_hash_history.json | 164 +- .../src/lfx/base/composio/composio_base.py | 2 +- src/lfx/src/lfx/base/compressors/model.py | 4 +- src/lfx/src/lfx/base/data/base_file.py | 2 +- .../lfx/base/document_transformers/model.py | 2 +- .../src/lfx/base/langchain_utilities/model.py | 2 +- src/lfx/src/lfx/base/vectorstores/model.py | 2 +- .../agentics/semantic_aggregator.py | 4 +- .../lfx/components/agentics/semantic_map.py | 4 +- .../agentics/synthetic_data_generator.py | 4 +- .../src/lfx/components/agentql/agentql_api.py | 2 +- .../components/amazon/s3_bucket_uploader.py | 2 +- src/lfx/src/lfx/components/arxiv/arxiv.py | 2 +- .../lfx/components/bing/bing_search_api.py | 2 +- .../lfx/components/confluence/confluence.py | 2 +- .../lfx/components/data_source/api_request.py | 4 +- .../lfx/components/data_source/csv_to_data.py | 2 +- .../components/data_source/json_to_data.py | 2 +- src/lfx/src/lfx/components/data_source/url.py | 2 +- .../lfx/components/deactivated/ingestion.py | 2 +- .../lfx/components/deactivated/split_text.py | 2 +- .../docling/chunk_docling_document.py | 6 +- .../docling/export_docling_document.py | 6 +- .../duckduckgo/duck_duck_go_search_run.py | 2 +- .../src/lfx/components/elastic/opensearch.py | 2 +- .../elastic/opensearch_multimodal.py | 2 +- .../files_and_knowledge/save_file.py | 2 +- .../firecrawl/firecrawl_crawl_api.py | 2 +- .../firecrawl/firecrawl_extract_api.py | 2 +- .../components/firecrawl/firecrawl_map_api.py | 2 +- .../firecrawl/firecrawl_scrape_api.py | 2 +- .../lfx/components/flow_controls/listen.py | 2 +- .../src/lfx/components/flow_controls/loop.py | 2 +- .../lfx/components/flow_controls/notify.py | 4 +- src/lfx/src/lfx/components/git/git.py | 2 +- .../lfx/components/glean/glean_search_api.py | 2 +- src/lfx/src/lfx/components/google/gmail.py | 2 +- .../components/google/google_drive_search.py | 2 +- .../components/input_output/chat_output.py | 2 +- .../lfx/components/input_output/webhook.py | 2 +- .../langchain_utilities/character.py | 2 +- .../html_link_extractor.py | 2 +- .../langchain_utilities/language_recursive.py | 2 +- .../langchain_utilities/language_semantic.py | 2 +- .../langchain_utilities/natural_language.py | 2 +- .../recursive_character.py | 2 +- .../langchain_utilities/self_query.py | 2 +- .../components/llm_operations/batch_run.py | 2 +- .../llm_operations/lambda_filter.py | 4 +- .../lfx/components/mem0/mem0_chat_memory.py | 2 +- .../components/models_and_agents/memory.py | 6 +- src/lfx/src/lfx/components/ollama/ollama.py | 4 +- .../components/processing/alter_metadata.py | 8 +- .../lfx/components/processing/converter.py | 93 +- .../lfx/components/processing/create_data.py | 4 +- .../lfx/components/processing/create_list.py | 4 +- .../components/processing/data_operations.py | 10 +- .../processing/data_to_dataframe.py | 2 +- .../processing/dataframe_operations.py | 26 +- .../processing/dynamic_create_data.py | 2 +- .../lfx/components/processing/filter_data.py | 2 +- .../processing/filter_data_values.py | 4 +- .../lfx/components/processing/merge_data.py | 2 +- .../components/processing/message_to_data.py | 2 +- .../lfx/components/processing/parse_data.py | 4 +- .../components/processing/parse_dataframe.py | 2 +- .../components/processing/parse_json_data.py | 2 +- .../src/lfx/components/processing/parser.py | 4 +- .../src/lfx/components/processing/regex.py | 2 +- .../lfx/components/processing/select_data.py | 2 +- .../lfx/components/processing/split_text.py | 2 +- .../components/processing/text_operations.py | 4 +- .../lfx/components/processing/update_data.py | 6 +- .../components/prototypes/python_function.py | 2 +- .../scrapegraph_markdownify_api.py | 2 +- .../scrapegraph/scrapegraph_search_api.py | 2 +- .../scrapegraph_smart_scraper_api.py | 2 +- .../src/lfx/components/searchapi/search.py | 2 +- src/lfx/src/lfx/components/serpapi/serp.py | 2 +- .../lfx/components/tavily/tavily_extract.py | 2 +- .../lfx/components/tavily/tavily_search.py | 2 +- .../tools/python_code_structured_tool.py | 2 +- .../twelvelabs/convert_astra_results.py | 2 +- .../components/twelvelabs/pegasus_index.py | 2 +- .../lfx/components/twelvelabs/split_video.py | 4 +- .../components/utilities/calculator_core.py | 2 +- .../lfx/components/vectorstores/local_db.py | 4 +- .../src/lfx/components/wikipedia/wikidata.py | 2 +- .../src/lfx/components/wikipedia/wikipedia.py | 2 +- .../wolframalpha/wolfram_alpha_api.py | 2 +- .../src/lfx/components/yahoosearch/yahoo.py | 2 +- src/lfx/src/lfx/field_typing/constants.py | 11 +- src/lfx/src/lfx/graph/edge/base.py | 87 +- src/lfx/src/lfx/inputs/__init__.py | 2 + src/lfx/src/lfx/inputs/inputs.py | 31 +- src/lfx/src/lfx/io/__init__.py | 2 + src/lfx/src/lfx/schema/data.py | 114 +- src/lfx/src/lfx/schema/dataframe.py | 74 +- .../test_component_display_names.py | 230 ++ .../unit/graph/edge/test_types_compatible.py | 357 +++ 150 files changed, 3886 insertions(+), 2104 deletions(-) create mode 100644 src/frontend/src/utils/__tests__/typeCompatibility.test.ts create mode 100644 src/lfx/tests/unit/components/test_component_display_names.py create mode 100644 src/lfx/tests/unit/graph/edge/test_types_compatible.py diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index 0dc7ce65afdc..f696dce9e53d 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -95,15 +95,26 @@ def update_projects_components_with_latest_component_versions(project_data, all_ } has_tool_outputs = any(output.get("types") == ["Tool"] for output in node_data.get("outputs", [])) if "outputs" in latest_node and not has_tool_outputs and not is_tool_or_agent: - # Set selected output as the previous selected output - for output in latest_node["outputs"]: + # Deep copy to avoid mutating the shared latest_node template across flows + new_outputs = deepcopy(latest_node["outputs"]) + # Set selected output as the previous selected output with type migration support + type_migrations = { + "Data": "JSON", + "DataFrame": "Table", + } + for output in new_outputs: node_data_output = next( (output_ for output_ in node_data["outputs"] if output_["name"] == output["name"]), None, ) if node_data_output: - output["selected"] = node_data_output.get("selected") - node_data["outputs"] = latest_node["outputs"] + old_selected = node_data_output.get("selected") + if old_selected: + # Old flows may use Data/DataFrame; map to JSON/Table for backward compatibility + migrated_selected = type_migrations.get(old_selected, old_selected) + if migrated_selected in output.get("types", []): + output["selected"] = migrated_selected + node_data["outputs"] = new_outputs if node_data["template"]["_type"] != latest_template["_type"]: node_data["template"]["_type"] = latest_template["_type"] @@ -429,13 +440,29 @@ def update_edges_with_latest_component_versions(project_data): source_handle["name"] = output_data.get("name") # Determine the new output types based on the output data + # Always prefer "types" over "selected" to ensure we use the current type names (JSON/Table) + # rather than potentially stale "selected" values (Data/DataFrame) if output_data: if len(output_data.get("types", [])) == 1: new_output_types = output_data.get("types", []) - elif output_data.get("selected"): - new_output_types = [output_data.get("selected")] + elif len(output_data.get("types", [])) > 1 and output_data.get("selected"): + # Only use "selected" if there are multiple types available + # and selected is present + selected = output_data.get("selected") + # Migrate old type names to new ones + type_migrations = { + "Data": "JSON", + "DataFrame": "Table", + } + migrated_selected = type_migrations.get(selected, selected) + # Verify the migrated selected is in the available types + if migrated_selected in output_data.get("types", []): + new_output_types = [migrated_selected] + else: + # Fallback to first type if selected is invalid + new_output_types = output_data.get("types", []) else: - new_output_types = [] + new_output_types = output_data.get("types", []) else: new_output_types = [] diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json index 38c4de8d3f9d..01dfe6ef8f7a 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json @@ -181,17 +181,19 @@ "id": "ChatOutput-WSW39", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "xy-edge__LanguageModelComponent-YIUOh{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-YIUOhœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-WSW39{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-WSW39œ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "xy-edge__LanguageModelComponent-YIUOh{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-YIUOhœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-WSW39{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-WSW39œ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "source": "LanguageModelComponent-YIUOh", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-YIUOhœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-WSW39", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-WSW39œ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-WSW39œ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" } ], "nodes": [ @@ -654,7 +656,7 @@ "legacy": false, "lf_version": "1.5.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -728,7 +730,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -783,7 +785,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json index 9e517ebf85e5..610934193002 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json @@ -74,18 +74,20 @@ "id": "ChatOutput-yK0AU", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-LanguageModelComponent-FLeYF{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-FLeYFœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-yK0AU{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-yK0AUœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-LanguageModelComponent-FLeYF{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-FLeYFœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-yK0AU{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-yK0AUœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "LanguageModelComponent-FLeYF", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-FLeYFœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-yK0AU", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-yK0AUœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-yK0AUœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" } ], "nodes": [ @@ -615,7 +617,7 @@ "legacy": false, "lf_version": "1.7.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -689,7 +691,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -744,7 +746,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json index 87358949a2bb..c623059f6206 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json @@ -68,7 +68,7 @@ "id": "URLComponent-DFXG5", "name": "page_results", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -76,17 +76,19 @@ "id": "ParserComponent-YRRd0", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "reactflow__edge-URLComponent-DFXG5{œdataTypeœ:œURLComponentœ,œidœ:œURLComponent-DFXG5œ,œnameœ:œpage_resultsœ,œoutput_typesœ:[œDataFrameœ]}-ParserComponent-YRRd0{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-YRRd0œ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-URLComponent-DFXG5{œdataTypeœ:œURLComponentœ,œidœ:œURLComponent-DFXG5œ,œnameœ:œpage_resultsœ,œoutput_typesœ:[œTableœ]}-ParserComponent-YRRd0{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-YRRd0œ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "URLComponent-DFXG5", - "sourceHandle": "{œdataTypeœ: œURLComponentœ, œidœ: œURLComponent-DFXG5œ, œnameœ: œpage_resultsœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œURLComponentœ, œidœ: œURLComponent-DFXG5œ, œnameœ: œpage_resultsœ, œoutput_typesœ: [œTableœ]}", "target": "ParserComponent-YRRd0", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-YRRd0œ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-YRRd0œ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -133,18 +135,20 @@ "id": "ChatOutput-GOjXV", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "xy-edge__LanguageModelComponent-1gwua{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-1gwuaœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-GOjXV{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-GOjXVœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "xy-edge__LanguageModelComponent-1gwua{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-1gwuaœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-GOjXV{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-GOjXVœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "LanguageModelComponent-1gwua", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-1gwuaœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-GOjXV", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-GOjXVœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-GOjXVœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" } ], "nodes": [ @@ -529,7 +533,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -603,7 +607,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -655,7 +659,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -827,7 +833,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "3cda25c3f7b5", + "code_hash": "cda7b997a730", "dependencies": { "dependencies": [ { @@ -877,17 +883,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"JSON or Table\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"JSON\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -994,7 +1002,8 @@ "id": "URLComponent-DFXG5", "node": { "base_classes": [ - "DataFrame" + "DataFrame", + "Table" ], "beta": false, "category": "data", @@ -1023,7 +1032,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "f773f55e3820", + "code_hash": "7c2b0b18854e", "dependencies": { "dependencies": [ { @@ -1061,10 +1070,10 @@ "group_outputs": false, "method": "fetch_content", "name": "page_results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -1139,7 +1148,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import importlib\nimport io\nimport re\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom langchain_community.document_loaders import RecursiveUrlLoader\nfrom markitdown import MarkItDown\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.data import safe_convert\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SliderInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.request_utils import get_user_agent\n\n# Constants\nDEFAULT_TIMEOUT = 30\nDEFAULT_MAX_DEPTH = 1\nDEFAULT_FORMAT = \"Text\"\n\n\nURL_REGEX = re.compile(\n r\"^(https?:\\/\\/)?\" r\"(www\\.)?\" r\"([a-zA-Z0-9.-]+)\" r\"(\\.[a-zA-Z]{2,})?\" r\"(:\\d+)?\" r\"(\\/[^\\s]*)?$\",\n re.IGNORECASE,\n)\n\nUSER_AGENT = None\n# Check if langflow is installed using importlib.util.find_spec(name))\nif importlib.util.find_spec(\"langflow\"):\n langflow_installed = True\n USER_AGENT = get_user_agent()\nelse:\n langflow_installed = False\n USER_AGENT = \"lfx\"\n\n\nclass URLComponent(Component):\n \"\"\"A component that loads and parses content from web pages recursively.\n\n This component allows fetching content from one or more URLs, with options to:\n - Control crawl depth\n - Prevent crawling outside the root domain\n - Use async loading for better performance\n - Extract either raw HTML or clean text\n - Configure request headers and timeouts\n \"\"\"\n\n display_name = \"URL\"\n description = \"Fetch content from one or more web pages, following links recursively.\"\n documentation: str = \"https://docs.langflow.org/url\"\n icon = \"layout-template\"\n name = \"URLComponent\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs to crawl recursively, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n input_types=[],\n ),\n SliderInput(\n name=\"max_depth\",\n display_name=\"Depth\",\n info=(\n \"Controls how many 'clicks' away from the initial page the crawler will go:\\n\"\n \"- depth 1: only the initial page\\n\"\n \"- depth 2: initial page + all pages linked directly from it\\n\"\n \"- depth 3: initial page + direct links + links found on those direct link pages\\n\"\n \"Note: This is about link traversal, not URL path depth.\"\n ),\n value=DEFAULT_MAX_DEPTH,\n range_spec=RangeSpec(min=1, max=5, step=1),\n required=False,\n min_label=\" \",\n max_label=\" \",\n min_label_icon=\"None\",\n max_label_icon=\"None\",\n # slider_input=True\n ),\n BoolInput(\n name=\"prevent_outside\",\n display_name=\"Prevent Outside\",\n info=(\n \"If enabled, only crawls URLs within the same domain as the root URL. \"\n \"This helps prevent the crawler from going to external websites.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"use_async\",\n display_name=\"Use Async\",\n info=(\n \"If enabled, uses asynchronous loading which can be significantly faster \"\n \"but might use more system resources.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=(\n \"Output Format. Use 'Text' to extract the text from the HTML, \"\n \"'Markdown' to parse the HTML into Markdown format, or 'HTML' \"\n \"for the raw HTML content.\"\n ),\n options=[\"Text\", \"HTML\", \"Markdown\"],\n value=DEFAULT_FORMAT,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout for the request in seconds.\",\n value=DEFAULT_TIMEOUT,\n required=False,\n advanced=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": USER_AGENT}],\n advanced=True,\n input_types=[\"DataFrame\"],\n ),\n BoolInput(\n name=\"filter_text_html\",\n display_name=\"Filter Text/HTML\",\n info=\"If enabled, filters out text/css content type from the results.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"continue_on_failure\",\n display_name=\"Continue on Failure\",\n info=\"If enabled, continues crawling even if some requests fail.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"check_response_status\",\n display_name=\"Check Response Status\",\n info=\"If enabled, checks the response status of the request.\",\n value=False,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"autoset_encoding\",\n display_name=\"Autoset Encoding\",\n info=\"If enabled, automatically sets the encoding of the request.\",\n value=True,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Extracted Pages\", name=\"page_results\", method=\"fetch_content\"),\n Output(display_name=\"Raw Content\", name=\"raw_results\", method=\"fetch_content_as_message\", tool_mode=False),\n ]\n\n @staticmethod\n def _html_extractor(x: str) -> str:\n \"\"\"Extract raw HTML content.\"\"\"\n return x\n\n @staticmethod\n def _text_extractor(x: str) -> str:\n \"\"\"Extract clean text from HTML.\"\"\"\n return BeautifulSoup(x, \"lxml\").get_text()\n\n @staticmethod\n def _markdown_extractor(x: str) -> str:\n \"\"\"Convert HTML to Markdown format.\"\"\"\n stream = io.BytesIO(x.encode(\"utf-8\"))\n result = MarkItDown(enable_plugins=False).convert_stream(stream)\n return result.markdown\n\n @staticmethod\n def validate_url(url: str) -> bool:\n \"\"\"Validates if the given string matches URL pattern.\n\n Args:\n url: The URL string to validate\n\n Returns:\n bool: True if the URL is valid, False otherwise\n \"\"\"\n return bool(URL_REGEX.match(url))\n\n def ensure_url(self, url: str) -> str:\n \"\"\"Ensures the given string is a valid URL.\n\n Args:\n url: The URL string to validate and normalize\n\n Returns:\n str: The normalized URL\n\n Raises:\n ValueError: If the URL is invalid\n \"\"\"\n url = url.strip()\n if not url.startswith((\"http://\", \"https://\")):\n url = \"https://\" + url\n\n if not self.validate_url(url):\n msg = f\"Invalid URL: {url}\"\n raise ValueError(msg)\n\n return url\n\n def _create_loader(self, url: str) -> RecursiveUrlLoader:\n \"\"\"Creates a RecursiveUrlLoader instance with the configured settings.\n\n Args:\n url: The URL to load\n\n Returns:\n RecursiveUrlLoader: Configured loader instance\n \"\"\"\n headers_dict = {header[\"key\"]: header[\"value\"] for header in self.headers if header[\"value\"] is not None}\n extractors = {\n \"HTML\": self._html_extractor,\n \"Markdown\": self._markdown_extractor,\n \"Text\": self._text_extractor,\n }\n extractor = extractors.get(self.format, self._text_extractor)\n\n return RecursiveUrlLoader(\n url=url,\n max_depth=self.max_depth,\n prevent_outside=self.prevent_outside,\n use_async=self.use_async,\n extractor=extractor,\n timeout=self.timeout,\n headers=headers_dict,\n check_response_status=self.check_response_status,\n continue_on_failure=self.continue_on_failure,\n base_url=url, # Add base_url to ensure consistent domain crawling\n autoset_encoding=self.autoset_encoding, # Enable automatic encoding detection\n exclude_dirs=[], # Allow customization of excluded directories\n link_regex=None, # Allow customization of link filtering\n )\n\n def fetch_url_contents(self) -> list[dict]:\n \"\"\"Load documents from the configured URLs.\n\n Returns:\n List[Data]: List of Data objects containing the fetched content\n\n Raises:\n ValueError: If no valid URLs are provided or if there's an error loading documents\n \"\"\"\n try:\n urls = list({self.ensure_url(url) for url in self.urls if url.strip()})\n logger.debug(f\"URLs: {urls}\")\n if not urls:\n msg = \"No valid URLs provided.\"\n raise ValueError(msg)\n\n all_docs = []\n for url in urls:\n logger.debug(f\"Loading documents from {url}\")\n\n try:\n loader = self._create_loader(url)\n docs = loader.load()\n\n if not docs:\n logger.warning(f\"No documents found for {url}\")\n continue\n\n logger.debug(f\"Found {len(docs)} documents from {url}\")\n all_docs.extend(docs)\n\n except requests.exceptions.RequestException as e:\n logger.exception(f\"Error loading documents from {url}: {e}\")\n continue\n\n if not all_docs:\n msg = \"No documents were successfully loaded from any URL\"\n raise ValueError(msg)\n\n # data = [Data(text=doc.page_content, **doc.metadata) for doc in all_docs]\n data = [\n {\n \"text\": safe_convert(doc.page_content, clean_data=True),\n \"url\": doc.metadata.get(\"source\", \"\"),\n \"title\": doc.metadata.get(\"title\", \"\"),\n \"description\": doc.metadata.get(\"description\", \"\"),\n \"content_type\": doc.metadata.get(\"content_type\", \"\"),\n \"language\": doc.metadata.get(\"language\", \"\"),\n }\n for doc in all_docs\n ]\n except Exception as e:\n error_msg = e.message if hasattr(e, \"message\") else e\n msg = f\"Error loading documents: {error_msg!s}\"\n logger.exception(msg)\n raise ValueError(msg) from e\n return data\n\n def fetch_content(self) -> DataFrame:\n \"\"\"Convert the documents to a DataFrame.\"\"\"\n return DataFrame(data=self.fetch_url_contents())\n\n def fetch_content_as_message(self) -> Message:\n \"\"\"Convert the documents to a Message.\"\"\"\n url_contents = self.fetch_url_contents()\n return Message(text=\"\\n\\n\".join([x[\"text\"] for x in url_contents]), data={\"data\": url_contents})\n" + "value": "import importlib\nimport io\nimport re\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom langchain_community.document_loaders import RecursiveUrlLoader\nfrom markitdown import MarkItDown\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.data import safe_convert\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SliderInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.request_utils import get_user_agent\n\n# Constants\nDEFAULT_TIMEOUT = 30\nDEFAULT_MAX_DEPTH = 1\nDEFAULT_FORMAT = \"Text\"\n\n\nURL_REGEX = re.compile(\n r\"^(https?:\\/\\/)?\" r\"(www\\.)?\" r\"([a-zA-Z0-9.-]+)\" r\"(\\.[a-zA-Z]{2,})?\" r\"(:\\d+)?\" r\"(\\/[^\\s]*)?$\",\n re.IGNORECASE,\n)\n\nUSER_AGENT = None\n# Check if langflow is installed using importlib.util.find_spec(name))\nif importlib.util.find_spec(\"langflow\"):\n langflow_installed = True\n USER_AGENT = get_user_agent()\nelse:\n langflow_installed = False\n USER_AGENT = \"lfx\"\n\n\nclass URLComponent(Component):\n \"\"\"A component that loads and parses content from web pages recursively.\n\n This component allows fetching content from one or more URLs, with options to:\n - Control crawl depth\n - Prevent crawling outside the root domain\n - Use async loading for better performance\n - Extract either raw HTML or clean text\n - Configure request headers and timeouts\n \"\"\"\n\n display_name = \"URL\"\n description = \"Fetch content from one or more web pages, following links recursively.\"\n documentation: str = \"https://docs.langflow.org/url\"\n icon = \"layout-template\"\n name = \"URLComponent\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs to crawl recursively, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n input_types=[],\n ),\n SliderInput(\n name=\"max_depth\",\n display_name=\"Depth\",\n info=(\n \"Controls how many 'clicks' away from the initial page the crawler will go:\\n\"\n \"- depth 1: only the initial page\\n\"\n \"- depth 2: initial page + all pages linked directly from it\\n\"\n \"- depth 3: initial page + direct links + links found on those direct link pages\\n\"\n \"Note: This is about link traversal, not URL path depth.\"\n ),\n value=DEFAULT_MAX_DEPTH,\n range_spec=RangeSpec(min=1, max=5, step=1),\n required=False,\n min_label=\" \",\n max_label=\" \",\n min_label_icon=\"None\",\n max_label_icon=\"None\",\n # slider_input=True\n ),\n BoolInput(\n name=\"prevent_outside\",\n display_name=\"Prevent Outside\",\n info=(\n \"If enabled, only crawls URLs within the same domain as the root URL. \"\n \"This helps prevent the crawler from going to external websites.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"use_async\",\n display_name=\"Use Async\",\n info=(\n \"If enabled, uses asynchronous loading which can be significantly faster \"\n \"but might use more system resources.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=(\n \"Output Format. Use 'Text' to extract the text from the HTML, \"\n \"'Markdown' to parse the HTML into Markdown format, or 'HTML' \"\n \"for the raw HTML content.\"\n ),\n options=[\"Text\", \"HTML\", \"Markdown\"],\n value=DEFAULT_FORMAT,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout for the request in seconds.\",\n value=DEFAULT_TIMEOUT,\n required=False,\n advanced=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": USER_AGENT}],\n advanced=True,\n input_types=[\"DataFrame\", \"Table\"],\n ),\n BoolInput(\n name=\"filter_text_html\",\n display_name=\"Filter Text/HTML\",\n info=\"If enabled, filters out text/css content type from the results.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"continue_on_failure\",\n display_name=\"Continue on Failure\",\n info=\"If enabled, continues crawling even if some requests fail.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"check_response_status\",\n display_name=\"Check Response Status\",\n info=\"If enabled, checks the response status of the request.\",\n value=False,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"autoset_encoding\",\n display_name=\"Autoset Encoding\",\n info=\"If enabled, automatically sets the encoding of the request.\",\n value=True,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Extracted Pages\", name=\"page_results\", method=\"fetch_content\"),\n Output(display_name=\"Raw Content\", name=\"raw_results\", method=\"fetch_content_as_message\", tool_mode=False),\n ]\n\n @staticmethod\n def _html_extractor(x: str) -> str:\n \"\"\"Extract raw HTML content.\"\"\"\n return x\n\n @staticmethod\n def _text_extractor(x: str) -> str:\n \"\"\"Extract clean text from HTML.\"\"\"\n return BeautifulSoup(x, \"lxml\").get_text()\n\n @staticmethod\n def _markdown_extractor(x: str) -> str:\n \"\"\"Convert HTML to Markdown format.\"\"\"\n stream = io.BytesIO(x.encode(\"utf-8\"))\n result = MarkItDown(enable_plugins=False).convert_stream(stream)\n return result.markdown\n\n @staticmethod\n def validate_url(url: str) -> bool:\n \"\"\"Validates if the given string matches URL pattern.\n\n Args:\n url: The URL string to validate\n\n Returns:\n bool: True if the URL is valid, False otherwise\n \"\"\"\n return bool(URL_REGEX.match(url))\n\n def ensure_url(self, url: str) -> str:\n \"\"\"Ensures the given string is a valid URL.\n\n Args:\n url: The URL string to validate and normalize\n\n Returns:\n str: The normalized URL\n\n Raises:\n ValueError: If the URL is invalid\n \"\"\"\n url = url.strip()\n if not url.startswith((\"http://\", \"https://\")):\n url = \"https://\" + url\n\n if not self.validate_url(url):\n msg = f\"Invalid URL: {url}\"\n raise ValueError(msg)\n\n return url\n\n def _create_loader(self, url: str) -> RecursiveUrlLoader:\n \"\"\"Creates a RecursiveUrlLoader instance with the configured settings.\n\n Args:\n url: The URL to load\n\n Returns:\n RecursiveUrlLoader: Configured loader instance\n \"\"\"\n headers_dict = {header[\"key\"]: header[\"value\"] for header in self.headers if header[\"value\"] is not None}\n extractors = {\n \"HTML\": self._html_extractor,\n \"Markdown\": self._markdown_extractor,\n \"Text\": self._text_extractor,\n }\n extractor = extractors.get(self.format, self._text_extractor)\n\n return RecursiveUrlLoader(\n url=url,\n max_depth=self.max_depth,\n prevent_outside=self.prevent_outside,\n use_async=self.use_async,\n extractor=extractor,\n timeout=self.timeout,\n headers=headers_dict,\n check_response_status=self.check_response_status,\n continue_on_failure=self.continue_on_failure,\n base_url=url, # Add base_url to ensure consistent domain crawling\n autoset_encoding=self.autoset_encoding, # Enable automatic encoding detection\n exclude_dirs=[], # Allow customization of excluded directories\n link_regex=None, # Allow customization of link filtering\n )\n\n def fetch_url_contents(self) -> list[dict]:\n \"\"\"Load documents from the configured URLs.\n\n Returns:\n List[Data]: List of Data objects containing the fetched content\n\n Raises:\n ValueError: If no valid URLs are provided or if there's an error loading documents\n \"\"\"\n try:\n urls = list({self.ensure_url(url) for url in self.urls if url.strip()})\n logger.debug(f\"URLs: {urls}\")\n if not urls:\n msg = \"No valid URLs provided.\"\n raise ValueError(msg)\n\n all_docs = []\n for url in urls:\n logger.debug(f\"Loading documents from {url}\")\n\n try:\n loader = self._create_loader(url)\n docs = loader.load()\n\n if not docs:\n logger.warning(f\"No documents found for {url}\")\n continue\n\n logger.debug(f\"Found {len(docs)} documents from {url}\")\n all_docs.extend(docs)\n\n except requests.exceptions.RequestException as e:\n logger.exception(f\"Error loading documents from {url}: {e}\")\n continue\n\n if not all_docs:\n msg = \"No documents were successfully loaded from any URL\"\n raise ValueError(msg)\n\n # data = [Data(text=doc.page_content, **doc.metadata) for doc in all_docs]\n data = [\n {\n \"text\": safe_convert(doc.page_content, clean_data=True),\n \"url\": doc.metadata.get(\"source\", \"\"),\n \"title\": doc.metadata.get(\"title\", \"\"),\n \"description\": doc.metadata.get(\"description\", \"\"),\n \"content_type\": doc.metadata.get(\"content_type\", \"\"),\n \"language\": doc.metadata.get(\"language\", \"\"),\n }\n for doc in all_docs\n ]\n except Exception as e:\n error_msg = e.message if hasattr(e, \"message\") else e\n msg = f\"Error loading documents: {error_msg!s}\"\n logger.exception(msg)\n raise ValueError(msg) from e\n return data\n\n def fetch_content(self) -> DataFrame:\n \"\"\"Convert the documents to a DataFrame.\"\"\"\n return DataFrame(data=self.fetch_url_contents())\n\n def fetch_content_as_message(self) -> Message:\n \"\"\"Convert the documents to a Message.\"\"\"\n url_contents = self.fetch_url_contents()\n return Message(text=\"\\n\\n\".join([x[\"text\"] for x in url_contents]), data={\"data\": url_contents})\n" }, "continue_on_failure": { "_input_type": "BoolInput", @@ -1209,7 +1218,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "is_list": true, "list_add_label": "Add More", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json index 5ccd3bc9d0b9..98f4ae9a1df8 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json @@ -162,18 +162,20 @@ "id": "ChatOutput-VoIob", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-LanguageModelComponent-SCqm9{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-SCqm9œ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-VoIob{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-VoIobœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-LanguageModelComponent-SCqm9{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-SCqm9œ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-VoIob{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-VoIobœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "LanguageModelComponent-SCqm9", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-SCqm9œ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-VoIob", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-VoIobœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-VoIobœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -214,6 +216,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -241,7 +244,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "efd064ef48ff", + "code_hash": "460243b16a3a", "dependencies": { "dependencies": [ { @@ -272,14 +275,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Dataframe", + "display_name": "Table", "group_outputs": false, "method": "retrieve_messages_dataframe", "name": "dataframe", - "selected": null, + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -303,7 +306,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any, cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text\nfrom lfx.inputs.inputs import DropdownInput, HandleInput, IntInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.memory import aget_messages, astore_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\nfrom lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass MemoryComponent(Component):\n display_name = \"Message History\"\n description = \"Stores or retrieves stored chat messages from Langflow tables or an external memory.\"\n documentation: str = \"https://docs.langflow.org/message-history\"\n icon = \"message-square-more\"\n name = \"Memory\"\n default_keys = [\"mode\", \"memory\", \"session_id\", \"context_id\"]\n mode_config = {\n \"Store\": [\"message\", \"memory\", \"sender\", \"sender_name\", \"session_id\", \"context_id\"],\n \"Retrieve\": [\"n_messages\", \"order\", \"template\", \"memory\", \"session_id\", \"context_id\"],\n }\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Retrieve\", \"Store\"],\n value=\"Retrieve\",\n info=\"Operation mode: Store messages or Retrieve messages.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The chat message to be stored.\",\n tool_mode=True,\n dynamic=True,\n show=False,\n ),\n HandleInput(\n name=\"memory\",\n display_name=\"External Memory\",\n input_types=[\"Memory\"],\n info=\"Retrieve messages from an external memory. If empty, it will use the Langflow tables.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"sender_type\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER, \"Machine and User\"],\n value=\"Machine and User\",\n info=\"Filter by sender type.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender\",\n display_name=\"Sender\",\n info=\"The sender of the message. Might be Machine or User. \"\n \"If empty, the current sender parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Filter by sender name.\",\n advanced=True,\n show=False,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Messages\",\n value=100,\n info=\"Number of messages to retrieve.\",\n advanced=True,\n show=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 value=\"\",\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 DropdownInput(\n name=\"order\",\n display_name=\"Order\",\n options=[\"Ascending\", \"Descending\"],\n value=\"Ascending\",\n info=\"Order of the messages.\",\n advanced=True,\n tool_mode=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {sender} or any other key in the message data.\",\n value=\"{sender_name}: {text}\",\n advanced=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Message\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True),\n Output(display_name=\"Dataframe\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"mode\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n if field_value == \"Store\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Stored Messages\",\n name=\"stored_messages\",\n method=\"store_message\",\n hidden=True,\n dynamic=True,\n )\n ]\n if field_value == \"Retrieve\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Messages\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True\n ),\n Output(\n display_name=\"Dataframe\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True\n ),\n ]\n return frontend_node\n\n async def store_message(self) -> Message:\n message = Message(text=self.message) if isinstance(self.message, str) else self.message\n\n message.context_id = self.context_id or message.context_id\n message.session_id = self.session_id or message.session_id\n message.sender = self.sender or message.sender or MESSAGE_SENDER_AI\n message.sender_name = self.sender_name or message.sender_name or MESSAGE_SENDER_NAME_AI\n\n stored_messages: list[Message] = []\n\n if self.memory:\n self.memory.context_id = message.context_id\n self.memory.session_id = message.session_id\n lc_message = message.to_lc_message()\n await self.memory.aadd_messages([lc_message])\n\n stored_messages = await self.memory.aget_messages() or []\n\n stored_messages = [Message.from_lc_message(m) for m in stored_messages] if stored_messages else []\n\n if message.sender:\n stored_messages = [m for m in stored_messages if m.sender == message.sender]\n else:\n await astore_message(message, flow_id=self.graph.flow_id)\n stored_messages = (\n await aget_messages(\n session_id=message.session_id,\n context_id=message.context_id,\n sender_name=message.sender_name,\n sender=message.sender,\n )\n or []\n )\n\n if not stored_messages:\n msg = \"No messages were stored. Please ensure that the session ID and sender are properly set.\"\n raise ValueError(msg)\n\n stored_message = stored_messages[0]\n self.status = stored_message\n return stored_message\n\n async def retrieve_messages(self) -> Data:\n sender_type = self.sender_type\n sender_name = self.sender_name\n session_id = self.session_id\n context_id = self.context_id\n n_messages = self.n_messages\n order = \"DESC\" if self.order == \"Descending\" else \"ASC\"\n\n if sender_type == \"Machine and User\":\n sender_type = None\n\n if self.memory and not hasattr(self.memory, \"aget_messages\"):\n memory_name = type(self.memory).__name__\n err_msg = f\"External Memory object ({memory_name}) must have 'aget_messages' method.\"\n raise AttributeError(err_msg)\n # Check if n_messages is None or 0\n if n_messages == 0:\n stored = []\n elif self.memory:\n # override session_id\n self.memory.session_id = session_id\n self.memory.context_id = context_id\n\n stored = await self.memory.aget_messages()\n # langchain memories are supposed to return messages in ascending order\n\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages first\n\n if order == \"DESC\":\n stored = stored[::-1] # Then reverse if needed\n\n stored = [Message.from_lc_message(m) for m in stored]\n if sender_type:\n expected_type = MESSAGE_SENDER_AI if sender_type == MESSAGE_SENDER_AI else MESSAGE_SENDER_USER\n stored = [m for m in stored if m.type == expected_type]\n else:\n # For internal memory, we always fetch the last N messages by ordering by DESC\n stored = await aget_messages(\n sender=sender_type,\n sender_name=sender_name,\n session_id=session_id,\n context_id=context_id,\n limit=10000,\n order=order,\n )\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages\n\n # self.status = stored\n return cast(\"Data\", stored)\n\n async def retrieve_messages_as_text(self) -> Message:\n stored_text = data_to_text(self.template, await self.retrieve_messages())\n # self.status = stored_text\n return Message(text=stored_text)\n\n async def retrieve_messages_dataframe(self) -> DataFrame:\n \"\"\"Convert the retrieved messages into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the message data.\n \"\"\"\n messages = await self.retrieve_messages()\n return DataFrame(messages)\n\n def update_build_config(\n self,\n build_config: dotdict,\n field_value: Any, # noqa: ARG002\n field_name: str | None = None, # noqa: ARG002\n ) -> dotdict:\n return set_current_fields(\n build_config=build_config,\n action_fields=self.mode_config,\n selected_action=build_config[\"mode\"][\"value\"],\n default_fields=self.default_keys,\n func=set_field_display,\n )\n" + "value": "from typing import Any, cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text\nfrom lfx.inputs.inputs import DropdownInput, HandleInput, IntInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.memory import aget_messages, astore_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\nfrom lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass MemoryComponent(Component):\n display_name = \"Message History\"\n description = \"Stores or retrieves stored chat messages from Langflow tables or an external memory.\"\n documentation: str = \"https://docs.langflow.org/message-history\"\n icon = \"message-square-more\"\n name = \"Memory\"\n default_keys = [\"mode\", \"memory\", \"session_id\", \"context_id\"]\n mode_config = {\n \"Store\": [\"message\", \"memory\", \"sender\", \"sender_name\", \"session_id\", \"context_id\"],\n \"Retrieve\": [\"n_messages\", \"order\", \"template\", \"memory\", \"session_id\", \"context_id\"],\n }\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Retrieve\", \"Store\"],\n value=\"Retrieve\",\n info=\"Operation mode: Store messages or Retrieve messages.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The chat message to be stored.\",\n tool_mode=True,\n dynamic=True,\n show=False,\n ),\n HandleInput(\n name=\"memory\",\n display_name=\"External Memory\",\n input_types=[\"Memory\"],\n info=\"Retrieve messages from an external memory. If empty, it will use the Langflow tables.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"sender_type\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER, \"Machine and User\"],\n value=\"Machine and User\",\n info=\"Filter by sender type.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender\",\n display_name=\"Sender\",\n info=\"The sender of the message. Might be Machine or User. \"\n \"If empty, the current sender parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Filter by sender name.\",\n advanced=True,\n show=False,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Messages\",\n value=100,\n info=\"Number of messages to retrieve.\",\n advanced=True,\n show=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 value=\"\",\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 DropdownInput(\n name=\"order\",\n display_name=\"Order\",\n options=[\"Ascending\", \"Descending\"],\n value=\"Ascending\",\n info=\"Order of the messages.\",\n advanced=True,\n tool_mode=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {sender} or any other key in the message data.\",\n value=\"{sender_name}: {text}\",\n advanced=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Message\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"mode\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n if field_value == \"Store\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Stored Messages\",\n name=\"stored_messages\",\n method=\"store_message\",\n hidden=True,\n dynamic=True,\n )\n ]\n if field_value == \"Retrieve\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Messages\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True\n ),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n return frontend_node\n\n async def store_message(self) -> Message:\n message = Message(text=self.message) if isinstance(self.message, str) else self.message\n\n message.context_id = self.context_id or message.context_id\n message.session_id = self.session_id or message.session_id\n message.sender = self.sender or message.sender or MESSAGE_SENDER_AI\n message.sender_name = self.sender_name or message.sender_name or MESSAGE_SENDER_NAME_AI\n\n stored_messages: list[Message] = []\n\n if self.memory:\n self.memory.context_id = message.context_id\n self.memory.session_id = message.session_id\n lc_message = message.to_lc_message()\n await self.memory.aadd_messages([lc_message])\n\n stored_messages = await self.memory.aget_messages() or []\n\n stored_messages = [Message.from_lc_message(m) for m in stored_messages] if stored_messages else []\n\n if message.sender:\n stored_messages = [m for m in stored_messages if m.sender == message.sender]\n else:\n await astore_message(message, flow_id=self.graph.flow_id)\n stored_messages = (\n await aget_messages(\n session_id=message.session_id,\n context_id=message.context_id,\n sender_name=message.sender_name,\n sender=message.sender,\n )\n or []\n )\n\n if not stored_messages:\n msg = \"No messages were stored. Please ensure that the session ID and sender are properly set.\"\n raise ValueError(msg)\n\n stored_message = stored_messages[0]\n self.status = stored_message\n return stored_message\n\n async def retrieve_messages(self) -> Data:\n sender_type = self.sender_type\n sender_name = self.sender_name\n session_id = self.session_id\n context_id = self.context_id\n n_messages = self.n_messages\n order = \"DESC\" if self.order == \"Descending\" else \"ASC\"\n\n if sender_type == \"Machine and User\":\n sender_type = None\n\n if self.memory and not hasattr(self.memory, \"aget_messages\"):\n memory_name = type(self.memory).__name__\n err_msg = f\"External Memory object ({memory_name}) must have 'aget_messages' method.\"\n raise AttributeError(err_msg)\n # Check if n_messages is None or 0\n if n_messages == 0:\n stored = []\n elif self.memory:\n # override session_id\n self.memory.session_id = session_id\n self.memory.context_id = context_id\n\n stored = await self.memory.aget_messages()\n # langchain memories are supposed to return messages in ascending order\n\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages first\n\n if order == \"DESC\":\n stored = stored[::-1] # Then reverse if needed\n\n stored = [Message.from_lc_message(m) for m in stored]\n if sender_type:\n expected_type = MESSAGE_SENDER_AI if sender_type == MESSAGE_SENDER_AI else MESSAGE_SENDER_USER\n stored = [m for m in stored if m.type == expected_type]\n else:\n # For internal memory, we always fetch the last N messages by ordering by DESC\n stored = await aget_messages(\n sender=sender_type,\n sender_name=sender_name,\n session_id=session_id,\n context_id=context_id,\n limit=10000,\n order=order,\n )\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages\n\n # self.status = stored\n return cast(\"Data\", stored)\n\n async def retrieve_messages_as_text(self) -> Message:\n stored_text = data_to_text(self.template, await self.retrieve_messages())\n # self.status = stored_text\n return Message(text=stored_text)\n\n async def retrieve_messages_dataframe(self) -> DataFrame:\n \"\"\"Convert the retrieved messages into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the message data.\n \"\"\"\n messages = await self.retrieve_messages()\n return DataFrame(messages)\n\n def update_build_config(\n self,\n build_config: dotdict,\n field_value: Any, # noqa: ARG002\n field_name: str | None = None, # noqa: ARG002\n ) -> dotdict:\n return set_current_fields(\n build_config=build_config,\n action_fields=self.mode_config,\n selected_action=build_config[\"mode\"][\"value\"],\n default_fields=self.default_keys,\n func=set_field_display,\n )\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -889,6 +892,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -918,7 +922,7 @@ "name": "page_results", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -1058,7 +1062,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "is_list": true, "list_add_label": "Add More", @@ -1243,6 +1248,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -1272,7 +1278,7 @@ "name": "page_results", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -1412,7 +1418,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "is_list": true, "list_add_label": "Add More", @@ -1603,6 +1610,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -1632,7 +1640,7 @@ "name": "page_results", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -1772,7 +1780,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "is_list": true, "list_add_label": "Add More", @@ -2259,7 +2268,7 @@ "key": "ChatOutput", "legacy": false, "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -2335,7 +2344,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -2391,7 +2400,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json b/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json index ae13080ef318..110ef0e7b241 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json @@ -74,18 +74,20 @@ "id": "ChatOutput-bcQIH", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-LanguageModelComponent-htMuI{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-htMuIœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-bcQIH{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-bcQIHœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-LanguageModelComponent-htMuI{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-htMuIœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-bcQIH{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-bcQIHœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "LanguageModelComponent-htMuI", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-htMuIœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-bcQIH", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-bcQIHœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-bcQIHœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -409,7 +411,7 @@ "legacy": false, "lf_version": "1.4.3", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -483,7 +485,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -538,7 +540,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1264,7 +1268,8 @@ "id": "File-b2gOG", "node": { "base_classes": [ - "DataFrame" + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -1555,6 +1560,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json b/src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json index 27060a08e9fb..45976c736233 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json @@ -38,7 +38,7 @@ "id": "StructuredOutput-ZOoVy", "name": "dataframe_output", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -46,17 +46,19 @@ "id": "ParserComponent-pxQmg", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "reactflow__edge-StructuredOutput-ZOoVy{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-ZOoVyœ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œDataFrameœ]}-ParserComponent-pxQmg{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-pxQmgœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-StructuredOutput-ZOoVy{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-ZOoVyœ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œTableœ]}-ParserComponent-pxQmg{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-pxQmgœ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "StructuredOutput-ZOoVy", - "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-ZOoVyœ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-ZOoVyœ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œTableœ]}", "target": "ParserComponent-pxQmg", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-pxQmgœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-pxQmgœ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -75,18 +77,20 @@ "id": "ChatOutput-RZUAi", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-ParserComponent-pxQmg{œdataTypeœ:œParserComponentœ,œidœ:œParserComponent-pxQmgœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-RZUAi{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-RZUAiœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-ParserComponent-pxQmg{œdataTypeœ:œParserComponentœ,œidœ:œParserComponent-pxQmgœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-RZUAi{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-RZUAiœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "ParserComponent-pxQmg", "sourceHandle": "{œdataTypeœ: œParserComponentœ, œidœ: œParserComponent-pxQmgœ, œnameœ: œparsed_textœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-RZUAi", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-RZUAiœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-RZUAiœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" } ], "nodes": [ @@ -121,7 +125,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -197,7 +201,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -253,7 +257,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -715,7 +721,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -768,10 +776,10 @@ "group_outputs": false, "method": "build_structured_output", "name": "structured_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -782,10 +790,10 @@ "group_outputs": false, "method": "build_structured_dataframe", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -1388,7 +1396,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "3cda25c3f7b5", + "code_hash": "cda7b997a730", "dependencies": { "dependencies": [ { @@ -1437,17 +1445,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"JSON or Table\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"JSON\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json b/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json index 85dae8f39f9d..bf29e0c3ad8f 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json @@ -18,18 +18,20 @@ "id": "ChatOutput-uBHDy", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-ParserComponent-TR5bm{œdataTypeœ:œParserComponentœ,œidœ:œParserComponent-TR5bmœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-uBHDy{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-uBHDyœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-ParserComponent-TR5bm{œdataTypeœ:œParserComponentœ,œidœ:œParserComponent-TR5bmœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-uBHDy{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-uBHDyœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "ParserComponent-TR5bm", "sourceHandle": "{œdataTypeœ: œParserComponentœ, œidœ: œParserComponent-TR5bmœ, œnameœ: œparsed_textœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-uBHDy", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-uBHDyœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-uBHDyœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -68,7 +70,7 @@ "id": "StructuredOutput-dSKGb", "name": "dataframe_output", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -76,17 +78,19 @@ "id": "ParserComponent-3GmW3", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "reactflow__edge-StructuredOutput-dSKGb{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-dSKGbœ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œDataFrameœ]}-ParserComponent-3GmW3{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-3GmW3œ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-StructuredOutput-dSKGb{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-dSKGbœ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œTableœ]}-ParserComponent-3GmW3{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-3GmW3œ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "StructuredOutput-dSKGb", - "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-dSKGbœ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-dSKGbœ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œTableœ]}", "target": "ParserComponent-3GmW3", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-3GmW3œ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-3GmW3œ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -150,7 +154,7 @@ "id": "AstraDB-AkDya", "name": "search_results", "output_types": [ - "Data" + "JSON" ] }, "targetHandle": { @@ -158,16 +162,18 @@ "id": "ParserComponent-TR5bm", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "xy-edge__AstraDB-AkDya{œdataTypeœ:œAstraDBœ,œidœ:œAstraDB-AkDyaœ,œnameœ:œsearch_resultsœ,œoutput_typesœ:[œDataœ]}-ParserComponent-TR5bm{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-TR5bmœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "xy-edge__AstraDB-AkDya{œdataTypeœ:œAstraDBœ,œidœ:œAstraDB-AkDyaœ,œnameœ:œsearch_resultsœ,œoutput_typesœ:[œJSONœ]}-ParserComponent-TR5bm{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-TR5bmœ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "source": "AstraDB-AkDya", - "sourceHandle": "{œdataTypeœ: œAstraDBœ, œidœ: œAstraDB-AkDyaœ, œnameœ: œsearch_resultsœ, œoutput_typesœ: [œDataœ]}", + "sourceHandle": "{œdataTypeœ: œAstraDBœ, œidœ: œAstraDB-AkDyaœ, œnameœ: œsearch_resultsœ, œoutput_typesœ: [œJSONœ]}", "target": "ParserComponent-TR5bm", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-TR5bmœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-TR5bmœ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -472,7 +478,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "3cda25c3f7b5", + "code_hash": "cda7b997a730", "dependencies": { "dependencies": [ { @@ -521,17 +527,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"JSON or Table\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"JSON\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -661,7 +669,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -736,7 +744,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -792,7 +800,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -938,7 +948,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "3cda25c3f7b5", + "code_hash": "cda7b997a730", "dependencies": { "dependencies": [ { @@ -987,17 +997,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"JSON or Table\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"JSON\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -1130,7 +1142,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -1183,10 +1197,10 @@ "group_outputs": false, "method": "build_structured_output", "name": "structured_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -1197,10 +1211,10 @@ "group_outputs": false, "method": "build_structured_dataframe", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -1690,7 +1704,9 @@ "node": { "base_classes": [ "Data", + "JSON", "DataFrame", + "Table", "VectorStore" ], "beta": false, @@ -1763,24 +1779,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json index 0b0b69a45b34..8f39ad1d94c0 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json @@ -18,18 +18,20 @@ "id": "ChatOutput-Ou5RJ", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-parser-IFSS9{œdataTypeœ:œparserœ,œidœ:œparser-IFSS9œ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Ou5RJ{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Ou5RJœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-parser-IFSS9{œdataTypeœ:œparserœ,œidœ:œparser-IFSS9œ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Ou5RJ{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Ou5RJœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "parser-IFSS9", "sourceHandle": "{œdataTypeœ: œparserœ, œidœ: œparser-IFSS9œ, œnameœ: œparsed_textœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-Ou5RJ", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Ou5RJœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Ou5RJœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -40,7 +42,7 @@ "id": "StructuredOutput-e4qlS", "name": "structured_output", "output_types": [ - "Data" + "JSON" ] }, "targetHandle": { @@ -48,17 +50,19 @@ "id": "parser-IFSS9", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "reactflow__edge-StructuredOutput-e4qlS{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-e4qlSœ,œnameœ:œstructured_outputœ,œoutput_typesœ:[œDataœ]}-parser-IFSS9{œfieldNameœ:œinput_dataœ,œidœ:œparser-IFSS9œ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-StructuredOutput-e4qlS{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-e4qlSœ,œnameœ:œstructured_outputœ,œoutput_typesœ:[œJSONœ]}-parser-IFSS9{œfieldNameœ:œinput_dataœ,œidœ:œparser-IFSS9œ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "StructuredOutput-e4qlS", - "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-e4qlSœ, œnameœ: œstructured_outputœ, œoutput_typesœ: [œDataœ]}", + "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-e4qlSœ, œnameœ: œstructured_outputœ, œoutput_typesœ: [œJSONœ]}", "target": "parser-IFSS9", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-IFSS9œ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-IFSS9œ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -447,7 +451,7 @@ "legacy": false, "lf_version": "1.7.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -521,7 +525,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -576,7 +580,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -959,12 +965,14 @@ "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -1071,7 +1079,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -1124,10 +1134,10 @@ "group_outputs": false, "method": "build_structured_output", "name": "structured_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -1138,10 +1148,10 @@ "group_outputs": false, "method": "build_structured_dataframe", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json b/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json index 6464572cb09b..daf937d64fb3 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json @@ -47,18 +47,20 @@ "id": "ChatOutput-xm3UQ", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-Prompt-vj0Ef{œdataTypeœ:œPromptœ,œidœ:œPrompt-vj0Efœ,œnameœ:œpromptœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-xm3UQ{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-xm3UQœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-Prompt-vj0Ef{œdataTypeœ:œPromptœ,œidœ:œPrompt-vj0Efœ,œnameœ:œpromptœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-xm3UQ{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-xm3UQœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "Prompt-vj0Ef", "sourceHandle": "{œdataTypeœ: œPromptœ, œidœ: œPrompt-vj0Efœ, œnameœ: œpromptœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-xm3UQ", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-xm3UQœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-xm3UQœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -1117,7 +1119,7 @@ "icon": "MessagesSquare", "legacy": false, "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -1191,7 +1193,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1246,7 +1248,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1618,6 +1622,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -1647,7 +1652,7 @@ "last_updated": "2025-07-18T17:42:31.004Z", "legacy": false, "metadata": { - "code_hash": "e602eaec8316", + "code_hash": "5638a305a99c", "dependencies": { "dependencies": [ { @@ -1738,7 +1743,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "days": { "_input_type": "IntInput", @@ -2437,7 +2442,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json b/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json index 1c368500474a..1a5747b35668 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json @@ -98,18 +98,20 @@ "id": "ChatOutput-Acmbw", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "xy-edge__Agent-CBCVT{œdataTypeœ:œAgentœ,œidœ:œAgent-CBCVTœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Acmbw{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Acmbwœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "xy-edge__Agent-CBCVT{œdataTypeœ:œAgentœ,œidœ:œAgent-CBCVTœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Acmbw{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Acmbwœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "Agent-CBCVT", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-CBCVTœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-Acmbw", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Acmbwœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Acmbwœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" } ], "nodes": [ @@ -335,7 +337,7 @@ "legacy": false, "lf_version": "1.1.5", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -410,7 +412,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -466,7 +468,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1546,7 +1550,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json b/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json index 723e548baddb..20fd7287b0c7 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json @@ -9,7 +9,7 @@ "id": "KnowledgeBase-kgwih", "name": "retrieve_data", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -17,17 +17,19 @@ "id": "ChatOutput-OG4M9", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "xy-edge__KnowledgeBase-kgwih{œdataTypeœ:œKnowledgeBaseœ,œidœ:œKnowledgeBase-kgwihœ,œnameœ:œretrieve_dataœ,œoutput_typesœ:[œDataFrameœ]}-ChatOutput-OG4M9{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-OG4M9œ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "xy-edge__KnowledgeBase-kgwih{œdataTypeœ:œKnowledgeBaseœ,œidœ:œKnowledgeBase-kgwihœ,œnameœ:œretrieve_dataœ,œoutput_typesœ:[œTableœ]}-ChatOutput-OG4M9{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-OG4M9œ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "source": "KnowledgeBase-kgwih", - "sourceHandle": "{œdataTypeœ: œKnowledgeBaseœ, œidœ: œKnowledgeBase-kgwihœ, œnameœ: œretrieve_dataœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œKnowledgeBaseœ, œidœ: œKnowledgeBase-kgwihœ, œnameœ: œretrieve_dataœ, œoutput_typesœ: [œTableœ]}", "target": "ChatOutput-OG4M9", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-OG4M9œ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-OG4M9œ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "className": "", @@ -252,7 +254,7 @@ "legacy": false, "lf_version": "1.5.0.post1", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -327,7 +329,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -383,7 +385,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -509,7 +513,8 @@ "id": "KnowledgeBase-kgwih", "node": { "base_classes": [ - "DataFrame" + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -597,10 +602,10 @@ "group_outputs": false, "method": "retrieve_data", "name": "retrieve_data", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json b/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json index b8fca4b63b3b..b435878fa574 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json @@ -74,18 +74,20 @@ "id": "ChatOutput-tjFWM", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "xy-edge__ParserComponent-8lfAE{œdataTypeœ:œParserComponentœ,œidœ:œParserComponent-8lfAEœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-tjFWM{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-tjFWMœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "xy-edge__ParserComponent-8lfAE{œdataTypeœ:œParserComponentœ,œidœ:œParserComponent-8lfAEœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-tjFWM{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-tjFWMœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "ParserComponent-8lfAE", "sourceHandle": "{œdataTypeœ: œParserComponentœ, œidœ: œParserComponent-8lfAEœ, œnameœ: œparsed_textœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-tjFWM", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-tjFWMœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-tjFWMœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -124,7 +126,7 @@ "id": "StructuredOutput-zfjb9", "name": "dataframe_output", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -132,17 +134,19 @@ "id": "ParserComponent-8lfAE", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "xy-edge__StructuredOutput-zfjb9{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-zfjb9œ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œDataFrameœ]}-ParserComponent-8lfAE{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-8lfAEœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "xy-edge__StructuredOutput-zfjb9{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-zfjb9œ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œTableœ]}-ParserComponent-8lfAE{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-8lfAEœ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "StructuredOutput-zfjb9", - "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-zfjb9œ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-zfjb9œ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œTableœ]}", "target": "ParserComponent-8lfAE", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-8lfAEœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-8lfAEœ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -443,7 +447,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -517,7 +521,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -572,7 +576,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -735,6 +741,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -765,7 +772,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "e602eaec8316", + "code_hash": "5638a305a99c", "dependencies": { "dependencies": [ { @@ -856,7 +863,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "days": { "_input_type": "IntInput", @@ -1555,7 +1562,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -1750,7 +1760,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "3cda25c3f7b5", + "code_hash": "cda7b997a730", "dependencies": { "dependencies": [ { @@ -1799,17 +1809,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"JSON or Table\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"JSON\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -1916,7 +1928,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -1968,10 +1982,10 @@ "group_outputs": false, "method": "build_structured_output", "name": "structured_output", - "selected": null, + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -1982,10 +1996,10 @@ "group_outputs": false, "method": "build_structured_dataframe", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json b/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json index cb26f0ee9076..dfcb0455dfb3 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json @@ -39,24 +39,25 @@ "id": "AssemblyAITranscriptionJobCreator-Nu1dV", "name": "transcript_id", "output_types": [ - "Data" + "JSON" ] }, "targetHandle": { "fieldName": "transcript_id", "id": "AssemblyAITranscriptionJobPoller-sCJsy", "inputTypes": [ - "Data" + "Data", + "JSON" ], "type": "other" } }, - "id": "reactflow__edge-AssemblyAITranscriptionJobCreator-Nu1dV{œdataTypeœ:œAssemblyAITranscriptionJobCreatorœ,œidœ:œAssemblyAITranscriptionJobCreator-Nu1dVœ,œnameœ:œtranscript_idœ,œoutput_typesœ:[œDataœ]}-AssemblyAITranscriptionJobPoller-sCJsy{œfieldNameœ:œtranscript_idœ,œidœ:œAssemblyAITranscriptionJobPoller-sCJsyœ,œinputTypesœ:[œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-AssemblyAITranscriptionJobCreator-Nu1dV{œdataTypeœ:œAssemblyAITranscriptionJobCreatorœ,œidœ:œAssemblyAITranscriptionJobCreator-Nu1dVœ,œnameœ:œtranscript_idœ,œoutput_typesœ:[œJSONœ]}-AssemblyAITranscriptionJobPoller-sCJsy{œfieldNameœ:œtranscript_idœ,œidœ:œAssemblyAITranscriptionJobPoller-sCJsyœ,œinputTypesœ:[œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "AssemblyAITranscriptionJobCreator-Nu1dV", - "sourceHandle": "{œdataTypeœ: œAssemblyAITranscriptionJobCreatorœ, œidœ: œAssemblyAITranscriptionJobCreator-Nu1dVœ, œnameœ: œtranscript_idœ, œoutput_typesœ: [œDataœ]}", + "sourceHandle": "{œdataTypeœ: œAssemblyAITranscriptionJobCreatorœ, œidœ: œAssemblyAITranscriptionJobCreator-Nu1dVœ, œnameœ: œtranscript_idœ, œoutput_typesœ: [œJSONœ]}", "target": "AssemblyAITranscriptionJobPoller-sCJsy", - "targetHandle": "{œfieldNameœ: œtranscript_idœ, œidœ: œAssemblyAITranscriptionJobPoller-sCJsyœ, œinputTypesœ: [œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œtranscript_idœ, œidœ: œAssemblyAITranscriptionJobPoller-sCJsyœ, œinputTypesœ: [œDataœ, œJSONœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -67,7 +68,7 @@ "id": "AssemblyAITranscriptionJobPoller-sCJsy", "name": "transcription_result", "output_types": [ - "Data" + "JSON" ] }, "targetHandle": { @@ -75,17 +76,19 @@ "id": "parser-6bJ9b", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "reactflow__edge-AssemblyAITranscriptionJobPoller-sCJsy{œdataTypeœ:œAssemblyAITranscriptionJobPollerœ,œidœ:œAssemblyAITranscriptionJobPoller-sCJsyœ,œnameœ:œtranscription_resultœ,œoutput_typesœ:[œDataœ]}-parser-6bJ9b{œfieldNameœ:œinput_dataœ,œidœ:œparser-6bJ9bœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-AssemblyAITranscriptionJobPoller-sCJsy{œdataTypeœ:œAssemblyAITranscriptionJobPollerœ,œidœ:œAssemblyAITranscriptionJobPoller-sCJsyœ,œnameœ:œtranscription_resultœ,œoutput_typesœ:[œJSONœ]}-parser-6bJ9b{œfieldNameœ:œinput_dataœ,œidœ:œparser-6bJ9bœ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "AssemblyAITranscriptionJobPoller-sCJsy", - "sourceHandle": "{œdataTypeœ: œAssemblyAITranscriptionJobPollerœ, œidœ: œAssemblyAITranscriptionJobPoller-sCJsyœ, œnameœ: œtranscription_resultœ, œoutput_typesœ: [œDataœ]}", + "sourceHandle": "{œdataTypeœ: œAssemblyAITranscriptionJobPollerœ, œidœ: œAssemblyAITranscriptionJobPoller-sCJsyœ, œnameœ: œtranscription_resultœ, œoutput_typesœ: [œJSONœ]}", "target": "parser-6bJ9b", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-6bJ9bœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-6bJ9bœ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -133,18 +136,20 @@ "id": "ChatOutput-iChI5", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-parser-6bJ9b{œdataTypeœ:œparserœ,œidœ:œparser-6bJ9bœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-iChI5{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-iChI5œ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-parser-6bJ9b{œdataTypeœ:œparserœ,œidœ:œparser-6bJ9bœ,œnameœ:œparsed_textœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-iChI5{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-iChI5œ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "parser-6bJ9b", "sourceHandle": "{œdataTypeœ: œparserœ, œidœ: œparser-6bJ9bœ, œnameœ: œparsed_textœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-iChI5", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-iChI5œ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-iChI5œ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -191,18 +196,20 @@ "id": "ChatOutput-9KeOi", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-LanguageModelComponent-cPCaH{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-cPCaHœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-9KeOi{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-9KeOiœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-LanguageModelComponent-cPCaH{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-cPCaHœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-9KeOi{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-9KeOiœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "LanguageModelComponent-cPCaH", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-cPCaHœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-9KeOi", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-9KeOiœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-9KeOiœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -249,18 +256,20 @@ "id": "ChatOutput-R039P", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-LanguageModelComponent-mMKmF{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-mMKmFœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-R039P{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-R039Pœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-LanguageModelComponent-mMKmF{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-mMKmFœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-R039P{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-R039Pœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "LanguageModelComponent-mMKmF", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-mMKmFœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-R039P", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-R039Pœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-R039Pœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "data": { @@ -295,7 +304,8 @@ "id": "AssemblyAITranscriptionJobPoller-sCJsy", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "conditional_paths": [], @@ -340,10 +350,10 @@ "group_outputs": false, "method": "poll_transcription_job", "name": "transcription_result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -417,7 +427,8 @@ "dynamic": false, "info": "The ID of the transcription job to poll", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -669,7 +680,7 @@ "legacy": false, "lf_version": "1.1.5", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -745,7 +756,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -800,7 +811,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -947,7 +960,7 @@ "legacy": false, "lf_version": "1.1.1", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -1023,7 +1036,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1078,7 +1091,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1225,7 +1240,7 @@ "legacy": false, "lf_version": "1.1.5", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -1301,7 +1316,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1356,7 +1371,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1689,6 +1706,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -1716,7 +1734,7 @@ "legacy": false, "lf_version": "1.1.5", "metadata": { - "code_hash": "efd064ef48ff", + "code_hash": "460243b16a3a", "dependencies": { "dependencies": [ { @@ -1748,14 +1766,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Dataframe", + "display_name": "Table", "group_outputs": false, "method": "retrieve_messages_dataframe", "name": "dataframe", - "selected": null, + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -1779,7 +1797,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any, cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text\nfrom lfx.inputs.inputs import DropdownInput, HandleInput, IntInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.memory import aget_messages, astore_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\nfrom lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass MemoryComponent(Component):\n display_name = \"Message History\"\n description = \"Stores or retrieves stored chat messages from Langflow tables or an external memory.\"\n documentation: str = \"https://docs.langflow.org/message-history\"\n icon = \"message-square-more\"\n name = \"Memory\"\n default_keys = [\"mode\", \"memory\", \"session_id\", \"context_id\"]\n mode_config = {\n \"Store\": [\"message\", \"memory\", \"sender\", \"sender_name\", \"session_id\", \"context_id\"],\n \"Retrieve\": [\"n_messages\", \"order\", \"template\", \"memory\", \"session_id\", \"context_id\"],\n }\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Retrieve\", \"Store\"],\n value=\"Retrieve\",\n info=\"Operation mode: Store messages or Retrieve messages.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The chat message to be stored.\",\n tool_mode=True,\n dynamic=True,\n show=False,\n ),\n HandleInput(\n name=\"memory\",\n display_name=\"External Memory\",\n input_types=[\"Memory\"],\n info=\"Retrieve messages from an external memory. If empty, it will use the Langflow tables.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"sender_type\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER, \"Machine and User\"],\n value=\"Machine and User\",\n info=\"Filter by sender type.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender\",\n display_name=\"Sender\",\n info=\"The sender of the message. Might be Machine or User. \"\n \"If empty, the current sender parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Filter by sender name.\",\n advanced=True,\n show=False,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Messages\",\n value=100,\n info=\"Number of messages to retrieve.\",\n advanced=True,\n show=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 value=\"\",\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 DropdownInput(\n name=\"order\",\n display_name=\"Order\",\n options=[\"Ascending\", \"Descending\"],\n value=\"Ascending\",\n info=\"Order of the messages.\",\n advanced=True,\n tool_mode=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {sender} or any other key in the message data.\",\n value=\"{sender_name}: {text}\",\n advanced=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Message\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True),\n Output(display_name=\"Dataframe\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"mode\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n if field_value == \"Store\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Stored Messages\",\n name=\"stored_messages\",\n method=\"store_message\",\n hidden=True,\n dynamic=True,\n )\n ]\n if field_value == \"Retrieve\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Messages\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True\n ),\n Output(\n display_name=\"Dataframe\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True\n ),\n ]\n return frontend_node\n\n async def store_message(self) -> Message:\n message = Message(text=self.message) if isinstance(self.message, str) else self.message\n\n message.context_id = self.context_id or message.context_id\n message.session_id = self.session_id or message.session_id\n message.sender = self.sender or message.sender or MESSAGE_SENDER_AI\n message.sender_name = self.sender_name or message.sender_name or MESSAGE_SENDER_NAME_AI\n\n stored_messages: list[Message] = []\n\n if self.memory:\n self.memory.context_id = message.context_id\n self.memory.session_id = message.session_id\n lc_message = message.to_lc_message()\n await self.memory.aadd_messages([lc_message])\n\n stored_messages = await self.memory.aget_messages() or []\n\n stored_messages = [Message.from_lc_message(m) for m in stored_messages] if stored_messages else []\n\n if message.sender:\n stored_messages = [m for m in stored_messages if m.sender == message.sender]\n else:\n await astore_message(message, flow_id=self.graph.flow_id)\n stored_messages = (\n await aget_messages(\n session_id=message.session_id,\n context_id=message.context_id,\n sender_name=message.sender_name,\n sender=message.sender,\n )\n or []\n )\n\n if not stored_messages:\n msg = \"No messages were stored. Please ensure that the session ID and sender are properly set.\"\n raise ValueError(msg)\n\n stored_message = stored_messages[0]\n self.status = stored_message\n return stored_message\n\n async def retrieve_messages(self) -> Data:\n sender_type = self.sender_type\n sender_name = self.sender_name\n session_id = self.session_id\n context_id = self.context_id\n n_messages = self.n_messages\n order = \"DESC\" if self.order == \"Descending\" else \"ASC\"\n\n if sender_type == \"Machine and User\":\n sender_type = None\n\n if self.memory and not hasattr(self.memory, \"aget_messages\"):\n memory_name = type(self.memory).__name__\n err_msg = f\"External Memory object ({memory_name}) must have 'aget_messages' method.\"\n raise AttributeError(err_msg)\n # Check if n_messages is None or 0\n if n_messages == 0:\n stored = []\n elif self.memory:\n # override session_id\n self.memory.session_id = session_id\n self.memory.context_id = context_id\n\n stored = await self.memory.aget_messages()\n # langchain memories are supposed to return messages in ascending order\n\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages first\n\n if order == \"DESC\":\n stored = stored[::-1] # Then reverse if needed\n\n stored = [Message.from_lc_message(m) for m in stored]\n if sender_type:\n expected_type = MESSAGE_SENDER_AI if sender_type == MESSAGE_SENDER_AI else MESSAGE_SENDER_USER\n stored = [m for m in stored if m.type == expected_type]\n else:\n # For internal memory, we always fetch the last N messages by ordering by DESC\n stored = await aget_messages(\n sender=sender_type,\n sender_name=sender_name,\n session_id=session_id,\n context_id=context_id,\n limit=10000,\n order=order,\n )\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages\n\n # self.status = stored\n return cast(\"Data\", stored)\n\n async def retrieve_messages_as_text(self) -> Message:\n stored_text = data_to_text(self.template, await self.retrieve_messages())\n # self.status = stored_text\n return Message(text=stored_text)\n\n async def retrieve_messages_dataframe(self) -> DataFrame:\n \"\"\"Convert the retrieved messages into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the message data.\n \"\"\"\n messages = await self.retrieve_messages()\n return DataFrame(messages)\n\n def update_build_config(\n self,\n build_config: dotdict,\n field_value: Any, # noqa: ARG002\n field_name: str | None = None, # noqa: ARG002\n ) -> dotdict:\n return set_current_fields(\n build_config=build_config,\n action_fields=self.mode_config,\n selected_action=build_config[\"mode\"][\"value\"],\n default_fields=self.default_keys,\n func=set_field_display,\n )\n" + "value": "from typing import Any, cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text\nfrom lfx.inputs.inputs import DropdownInput, HandleInput, IntInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.memory import aget_messages, astore_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\nfrom lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass MemoryComponent(Component):\n display_name = \"Message History\"\n description = \"Stores or retrieves stored chat messages from Langflow tables or an external memory.\"\n documentation: str = \"https://docs.langflow.org/message-history\"\n icon = \"message-square-more\"\n name = \"Memory\"\n default_keys = [\"mode\", \"memory\", \"session_id\", \"context_id\"]\n mode_config = {\n \"Store\": [\"message\", \"memory\", \"sender\", \"sender_name\", \"session_id\", \"context_id\"],\n \"Retrieve\": [\"n_messages\", \"order\", \"template\", \"memory\", \"session_id\", \"context_id\"],\n }\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Retrieve\", \"Store\"],\n value=\"Retrieve\",\n info=\"Operation mode: Store messages or Retrieve messages.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The chat message to be stored.\",\n tool_mode=True,\n dynamic=True,\n show=False,\n ),\n HandleInput(\n name=\"memory\",\n display_name=\"External Memory\",\n input_types=[\"Memory\"],\n info=\"Retrieve messages from an external memory. If empty, it will use the Langflow tables.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"sender_type\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER, \"Machine and User\"],\n value=\"Machine and User\",\n info=\"Filter by sender type.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender\",\n display_name=\"Sender\",\n info=\"The sender of the message. Might be Machine or User. \"\n \"If empty, the current sender parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Filter by sender name.\",\n advanced=True,\n show=False,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Messages\",\n value=100,\n info=\"Number of messages to retrieve.\",\n advanced=True,\n show=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 value=\"\",\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 DropdownInput(\n name=\"order\",\n display_name=\"Order\",\n options=[\"Ascending\", \"Descending\"],\n value=\"Ascending\",\n info=\"Order of the messages.\",\n advanced=True,\n tool_mode=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {sender} or any other key in the message data.\",\n value=\"{sender_name}: {text}\",\n advanced=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Message\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"mode\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n if field_value == \"Store\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Stored Messages\",\n name=\"stored_messages\",\n method=\"store_message\",\n hidden=True,\n dynamic=True,\n )\n ]\n if field_value == \"Retrieve\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Messages\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True\n ),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n return frontend_node\n\n async def store_message(self) -> Message:\n message = Message(text=self.message) if isinstance(self.message, str) else self.message\n\n message.context_id = self.context_id or message.context_id\n message.session_id = self.session_id or message.session_id\n message.sender = self.sender or message.sender or MESSAGE_SENDER_AI\n message.sender_name = self.sender_name or message.sender_name or MESSAGE_SENDER_NAME_AI\n\n stored_messages: list[Message] = []\n\n if self.memory:\n self.memory.context_id = message.context_id\n self.memory.session_id = message.session_id\n lc_message = message.to_lc_message()\n await self.memory.aadd_messages([lc_message])\n\n stored_messages = await self.memory.aget_messages() or []\n\n stored_messages = [Message.from_lc_message(m) for m in stored_messages] if stored_messages else []\n\n if message.sender:\n stored_messages = [m for m in stored_messages if m.sender == message.sender]\n else:\n await astore_message(message, flow_id=self.graph.flow_id)\n stored_messages = (\n await aget_messages(\n session_id=message.session_id,\n context_id=message.context_id,\n sender_name=message.sender_name,\n sender=message.sender,\n )\n or []\n )\n\n if not stored_messages:\n msg = \"No messages were stored. Please ensure that the session ID and sender are properly set.\"\n raise ValueError(msg)\n\n stored_message = stored_messages[0]\n self.status = stored_message\n return stored_message\n\n async def retrieve_messages(self) -> Data:\n sender_type = self.sender_type\n sender_name = self.sender_name\n session_id = self.session_id\n context_id = self.context_id\n n_messages = self.n_messages\n order = \"DESC\" if self.order == \"Descending\" else \"ASC\"\n\n if sender_type == \"Machine and User\":\n sender_type = None\n\n if self.memory and not hasattr(self.memory, \"aget_messages\"):\n memory_name = type(self.memory).__name__\n err_msg = f\"External Memory object ({memory_name}) must have 'aget_messages' method.\"\n raise AttributeError(err_msg)\n # Check if n_messages is None or 0\n if n_messages == 0:\n stored = []\n elif self.memory:\n # override session_id\n self.memory.session_id = session_id\n self.memory.context_id = context_id\n\n stored = await self.memory.aget_messages()\n # langchain memories are supposed to return messages in ascending order\n\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages first\n\n if order == \"DESC\":\n stored = stored[::-1] # Then reverse if needed\n\n stored = [Message.from_lc_message(m) for m in stored]\n if sender_type:\n expected_type = MESSAGE_SENDER_AI if sender_type == MESSAGE_SENDER_AI else MESSAGE_SENDER_USER\n stored = [m for m in stored if m.type == expected_type]\n else:\n # For internal memory, we always fetch the last N messages by ordering by DESC\n stored = await aget_messages(\n sender=sender_type,\n sender_name=sender_name,\n session_id=session_id,\n context_id=context_id,\n limit=10000,\n order=order,\n )\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages\n\n # self.status = stored\n return cast(\"Data\", stored)\n\n async def retrieve_messages_as_text(self) -> Message:\n stored_text = data_to_text(self.template, await self.retrieve_messages())\n # self.status = stored_text\n return Message(text=stored_text)\n\n async def retrieve_messages_dataframe(self) -> DataFrame:\n \"\"\"Convert the retrieved messages into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the message data.\n \"\"\"\n messages = await self.retrieve_messages()\n return DataFrame(messages)\n\n def update_build_config(\n self,\n build_config: dotdict,\n field_value: Any, # noqa: ARG002\n field_name: str | None = None, # noqa: ARG002\n ) -> dotdict:\n return set_current_fields(\n build_config=build_config,\n action_fields=self.mode_config,\n selected_action=build_config[\"mode\"][\"value\"],\n default_fields=self.default_keys,\n func=set_field_display,\n )\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -2433,7 +2451,8 @@ "id": "AssemblyAITranscriptionJobCreator-Nu1dV", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "category": "assemblyai", @@ -2486,10 +2505,10 @@ "group_outputs": false, "method": "create_transcription_job", "name": "transcript_id", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -2892,12 +2911,14 @@ "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json index dbbbd48859ff..31f088f46be3 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json @@ -103,18 +103,20 @@ "id": "ChatOutput-2ljRT", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-LanguageModelComponent-n8KRg{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-n8KRgœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-2ljRT{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-2ljRTœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-LanguageModelComponent-n8KRg{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-n8KRgœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-2ljRT{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-2ljRTœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "LanguageModelComponent-n8KRg", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-n8KRgœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-2ljRT", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-2ljRTœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-2ljRTœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" } ], "nodes": [ @@ -418,7 +420,7 @@ "legacy": false, "lf_version": "1.4.3", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -492,7 +494,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -547,7 +549,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -895,7 +899,8 @@ "id": "Memory-MKGtC", "node": { "base_classes": [ - "DataFrame" + "DataFrame", + "Table" ], "beta": false, "category": "helpers", @@ -924,7 +929,7 @@ "legacy": false, "lf_version": "1.4.3", "metadata": { - "code_hash": "efd064ef48ff", + "code_hash": "460243b16a3a", "dependencies": { "dependencies": [ { @@ -956,14 +961,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Dataframe", + "display_name": "Table", "group_outputs": false, "method": "retrieve_messages_dataframe", "name": "dataframe", - "selected": null, + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -988,7 +993,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any, cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text\nfrom lfx.inputs.inputs import DropdownInput, HandleInput, IntInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.memory import aget_messages, astore_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\nfrom lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass MemoryComponent(Component):\n display_name = \"Message History\"\n description = \"Stores or retrieves stored chat messages from Langflow tables or an external memory.\"\n documentation: str = \"https://docs.langflow.org/message-history\"\n icon = \"message-square-more\"\n name = \"Memory\"\n default_keys = [\"mode\", \"memory\", \"session_id\", \"context_id\"]\n mode_config = {\n \"Store\": [\"message\", \"memory\", \"sender\", \"sender_name\", \"session_id\", \"context_id\"],\n \"Retrieve\": [\"n_messages\", \"order\", \"template\", \"memory\", \"session_id\", \"context_id\"],\n }\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Retrieve\", \"Store\"],\n value=\"Retrieve\",\n info=\"Operation mode: Store messages or Retrieve messages.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The chat message to be stored.\",\n tool_mode=True,\n dynamic=True,\n show=False,\n ),\n HandleInput(\n name=\"memory\",\n display_name=\"External Memory\",\n input_types=[\"Memory\"],\n info=\"Retrieve messages from an external memory. If empty, it will use the Langflow tables.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"sender_type\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER, \"Machine and User\"],\n value=\"Machine and User\",\n info=\"Filter by sender type.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender\",\n display_name=\"Sender\",\n info=\"The sender of the message. Might be Machine or User. \"\n \"If empty, the current sender parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Filter by sender name.\",\n advanced=True,\n show=False,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Messages\",\n value=100,\n info=\"Number of messages to retrieve.\",\n advanced=True,\n show=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 value=\"\",\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 DropdownInput(\n name=\"order\",\n display_name=\"Order\",\n options=[\"Ascending\", \"Descending\"],\n value=\"Ascending\",\n info=\"Order of the messages.\",\n advanced=True,\n tool_mode=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {sender} or any other key in the message data.\",\n value=\"{sender_name}: {text}\",\n advanced=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Message\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True),\n Output(display_name=\"Dataframe\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"mode\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n if field_value == \"Store\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Stored Messages\",\n name=\"stored_messages\",\n method=\"store_message\",\n hidden=True,\n dynamic=True,\n )\n ]\n if field_value == \"Retrieve\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Messages\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True\n ),\n Output(\n display_name=\"Dataframe\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True\n ),\n ]\n return frontend_node\n\n async def store_message(self) -> Message:\n message = Message(text=self.message) if isinstance(self.message, str) else self.message\n\n message.context_id = self.context_id or message.context_id\n message.session_id = self.session_id or message.session_id\n message.sender = self.sender or message.sender or MESSAGE_SENDER_AI\n message.sender_name = self.sender_name or message.sender_name or MESSAGE_SENDER_NAME_AI\n\n stored_messages: list[Message] = []\n\n if self.memory:\n self.memory.context_id = message.context_id\n self.memory.session_id = message.session_id\n lc_message = message.to_lc_message()\n await self.memory.aadd_messages([lc_message])\n\n stored_messages = await self.memory.aget_messages() or []\n\n stored_messages = [Message.from_lc_message(m) for m in stored_messages] if stored_messages else []\n\n if message.sender:\n stored_messages = [m for m in stored_messages if m.sender == message.sender]\n else:\n await astore_message(message, flow_id=self.graph.flow_id)\n stored_messages = (\n await aget_messages(\n session_id=message.session_id,\n context_id=message.context_id,\n sender_name=message.sender_name,\n sender=message.sender,\n )\n or []\n )\n\n if not stored_messages:\n msg = \"No messages were stored. Please ensure that the session ID and sender are properly set.\"\n raise ValueError(msg)\n\n stored_message = stored_messages[0]\n self.status = stored_message\n return stored_message\n\n async def retrieve_messages(self) -> Data:\n sender_type = self.sender_type\n sender_name = self.sender_name\n session_id = self.session_id\n context_id = self.context_id\n n_messages = self.n_messages\n order = \"DESC\" if self.order == \"Descending\" else \"ASC\"\n\n if sender_type == \"Machine and User\":\n sender_type = None\n\n if self.memory and not hasattr(self.memory, \"aget_messages\"):\n memory_name = type(self.memory).__name__\n err_msg = f\"External Memory object ({memory_name}) must have 'aget_messages' method.\"\n raise AttributeError(err_msg)\n # Check if n_messages is None or 0\n if n_messages == 0:\n stored = []\n elif self.memory:\n # override session_id\n self.memory.session_id = session_id\n self.memory.context_id = context_id\n\n stored = await self.memory.aget_messages()\n # langchain memories are supposed to return messages in ascending order\n\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages first\n\n if order == \"DESC\":\n stored = stored[::-1] # Then reverse if needed\n\n stored = [Message.from_lc_message(m) for m in stored]\n if sender_type:\n expected_type = MESSAGE_SENDER_AI if sender_type == MESSAGE_SENDER_AI else MESSAGE_SENDER_USER\n stored = [m for m in stored if m.type == expected_type]\n else:\n # For internal memory, we always fetch the last N messages by ordering by DESC\n stored = await aget_messages(\n sender=sender_type,\n sender_name=sender_name,\n session_id=session_id,\n context_id=context_id,\n limit=10000,\n order=order,\n )\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages\n\n # self.status = stored\n return cast(\"Data\", stored)\n\n async def retrieve_messages_as_text(self) -> Message:\n stored_text = data_to_text(self.template, await self.retrieve_messages())\n # self.status = stored_text\n return Message(text=stored_text)\n\n async def retrieve_messages_dataframe(self) -> DataFrame:\n \"\"\"Convert the retrieved messages into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the message data.\n \"\"\"\n messages = await self.retrieve_messages()\n return DataFrame(messages)\n\n def update_build_config(\n self,\n build_config: dotdict,\n field_value: Any, # noqa: ARG002\n field_name: str | None = None, # noqa: ARG002\n ) -> dotdict:\n return set_current_fields(\n build_config=build_config,\n action_fields=self.mode_config,\n selected_action=build_config[\"mode\"][\"value\"],\n default_fields=self.default_keys,\n func=set_field_display,\n )\n" + "value": "from typing import Any, cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text\nfrom lfx.inputs.inputs import DropdownInput, HandleInput, IntInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.memory import aget_messages, astore_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\nfrom lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass MemoryComponent(Component):\n display_name = \"Message History\"\n description = \"Stores or retrieves stored chat messages from Langflow tables or an external memory.\"\n documentation: str = \"https://docs.langflow.org/message-history\"\n icon = \"message-square-more\"\n name = \"Memory\"\n default_keys = [\"mode\", \"memory\", \"session_id\", \"context_id\"]\n mode_config = {\n \"Store\": [\"message\", \"memory\", \"sender\", \"sender_name\", \"session_id\", \"context_id\"],\n \"Retrieve\": [\"n_messages\", \"order\", \"template\", \"memory\", \"session_id\", \"context_id\"],\n }\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Retrieve\", \"Store\"],\n value=\"Retrieve\",\n info=\"Operation mode: Store messages or Retrieve messages.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The chat message to be stored.\",\n tool_mode=True,\n dynamic=True,\n show=False,\n ),\n HandleInput(\n name=\"memory\",\n display_name=\"External Memory\",\n input_types=[\"Memory\"],\n info=\"Retrieve messages from an external memory. If empty, it will use the Langflow tables.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"sender_type\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER, \"Machine and User\"],\n value=\"Machine and User\",\n info=\"Filter by sender type.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender\",\n display_name=\"Sender\",\n info=\"The sender of the message. Might be Machine or User. \"\n \"If empty, the current sender parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Filter by sender name.\",\n advanced=True,\n show=False,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Messages\",\n value=100,\n info=\"Number of messages to retrieve.\",\n advanced=True,\n show=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 value=\"\",\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 DropdownInput(\n name=\"order\",\n display_name=\"Order\",\n options=[\"Ascending\", \"Descending\"],\n value=\"Ascending\",\n info=\"Order of the messages.\",\n advanced=True,\n tool_mode=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {sender} or any other key in the message data.\",\n value=\"{sender_name}: {text}\",\n advanced=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Message\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"mode\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n if field_value == \"Store\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Stored Messages\",\n name=\"stored_messages\",\n method=\"store_message\",\n hidden=True,\n dynamic=True,\n )\n ]\n if field_value == \"Retrieve\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Messages\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True\n ),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n return frontend_node\n\n async def store_message(self) -> Message:\n message = Message(text=self.message) if isinstance(self.message, str) else self.message\n\n message.context_id = self.context_id or message.context_id\n message.session_id = self.session_id or message.session_id\n message.sender = self.sender or message.sender or MESSAGE_SENDER_AI\n message.sender_name = self.sender_name or message.sender_name or MESSAGE_SENDER_NAME_AI\n\n stored_messages: list[Message] = []\n\n if self.memory:\n self.memory.context_id = message.context_id\n self.memory.session_id = message.session_id\n lc_message = message.to_lc_message()\n await self.memory.aadd_messages([lc_message])\n\n stored_messages = await self.memory.aget_messages() or []\n\n stored_messages = [Message.from_lc_message(m) for m in stored_messages] if stored_messages else []\n\n if message.sender:\n stored_messages = [m for m in stored_messages if m.sender == message.sender]\n else:\n await astore_message(message, flow_id=self.graph.flow_id)\n stored_messages = (\n await aget_messages(\n session_id=message.session_id,\n context_id=message.context_id,\n sender_name=message.sender_name,\n sender=message.sender,\n )\n or []\n )\n\n if not stored_messages:\n msg = \"No messages were stored. Please ensure that the session ID and sender are properly set.\"\n raise ValueError(msg)\n\n stored_message = stored_messages[0]\n self.status = stored_message\n return stored_message\n\n async def retrieve_messages(self) -> Data:\n sender_type = self.sender_type\n sender_name = self.sender_name\n session_id = self.session_id\n context_id = self.context_id\n n_messages = self.n_messages\n order = \"DESC\" if self.order == \"Descending\" else \"ASC\"\n\n if sender_type == \"Machine and User\":\n sender_type = None\n\n if self.memory and not hasattr(self.memory, \"aget_messages\"):\n memory_name = type(self.memory).__name__\n err_msg = f\"External Memory object ({memory_name}) must have 'aget_messages' method.\"\n raise AttributeError(err_msg)\n # Check if n_messages is None or 0\n if n_messages == 0:\n stored = []\n elif self.memory:\n # override session_id\n self.memory.session_id = session_id\n self.memory.context_id = context_id\n\n stored = await self.memory.aget_messages()\n # langchain memories are supposed to return messages in ascending order\n\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages first\n\n if order == \"DESC\":\n stored = stored[::-1] # Then reverse if needed\n\n stored = [Message.from_lc_message(m) for m in stored]\n if sender_type:\n expected_type = MESSAGE_SENDER_AI if sender_type == MESSAGE_SENDER_AI else MESSAGE_SENDER_USER\n stored = [m for m in stored if m.type == expected_type]\n else:\n # For internal memory, we always fetch the last N messages by ordering by DESC\n stored = await aget_messages(\n sender=sender_type,\n sender_name=sender_name,\n session_id=session_id,\n context_id=context_id,\n limit=10000,\n order=order,\n )\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages\n\n # self.status = stored\n return cast(\"Data\", stored)\n\n async def retrieve_messages_as_text(self) -> Message:\n stored_text = data_to_text(self.template, await self.retrieve_messages())\n # self.status = stored_text\n return Message(text=stored_text)\n\n async def retrieve_messages_dataframe(self) -> DataFrame:\n \"\"\"Convert the retrieved messages into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the message data.\n \"\"\"\n messages = await self.retrieve_messages()\n return DataFrame(messages)\n\n def update_build_config(\n self,\n build_config: dotdict,\n field_value: Any, # noqa: ARG002\n field_name: str | None = None, # noqa: ARG002\n ) -> dotdict:\n return set_current_fields(\n build_config=build_config,\n action_fields=self.mode_config,\n selected_action=build_config[\"mode\"][\"value\"],\n default_fields=self.default_keys,\n func=set_field_display,\n )\n" }, "context_id": { "_input_type": "MessageTextInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json b/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json index e4a91421c860..b3d1a068bd08 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json @@ -74,18 +74,20 @@ "id": "ChatOutput-jNfAw", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-Agent-ZH2Rd{œdataTypeœ:œAgentœ,œidœ:œAgent-ZH2Rdœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-jNfAw{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-jNfAwœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-Agent-ZH2Rd{œdataTypeœ:œAgentœ,œidœ:œAgent-ZH2Rdœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-jNfAw{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-jNfAwœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "Agent-ZH2Rd", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-ZH2Rdœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-jNfAw", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-jNfAwœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-jNfAwœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -104,18 +106,20 @@ "id": "SaveToFile-VVVVb", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "xy-edge__ChatOutput-jNfAw{œdataTypeœ:œChatOutputœ,œidœ:œChatOutput-jNfAwœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-SaveToFile-VVVVb{œfieldNameœ:œinputœ,œidœ:œSaveToFile-VVVVbœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "xy-edge__ChatOutput-jNfAw{œdataTypeœ:œChatOutputœ,œidœ:œChatOutput-jNfAwœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-SaveToFile-VVVVb{œfieldNameœ:œinputœ,œidœ:œSaveToFile-VVVVbœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "ChatOutput-jNfAw", "sourceHandle": "{œdataTypeœ: œChatOutputœ, œidœ: œChatOutput-jNfAwœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", "target": "SaveToFile-VVVVb", - "targetHandle": "{œfieldNameœ: œinputœ, œidœ: œSaveToFile-VVVVbœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinputœ, œidœ: œSaveToFile-VVVVbœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -179,7 +183,8 @@ "id": "AgentQL-064NO", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "conditional_paths": [], @@ -206,7 +211,7 @@ "legacy": false, "lf_version": "1.4.3", "metadata": { - "code_hash": "37de3210aed9", + "code_hash": "3737ac221d7d", "dependencies": { "dependencies": [ { @@ -279,7 +284,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass AgentQL(Component):\n display_name = \"Extract Web Data\"\n description = \"Extracts structured data from a web page using an AgentQL query or a Natural Language description.\"\n documentation: str = \"https://docs.agentql.com/rest-api/api-reference\"\n icon = \"AgentQL\"\n name = \"AgentQL\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"AgentQL API Key\",\n required=True,\n password=True,\n info=\"Your AgentQL API key from dev.agentql.com\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL of the public web page you want to extract data from.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"query\",\n display_name=\"AgentQL Query\",\n required=False,\n info=\"The AgentQL query to execute. Learn more at https://docs.agentql.com/agentql-query or use a prompt.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n required=False,\n info=\"A Natural Language description of the data to extract from the page. Alternative to AgentQL query.\",\n tool_mode=True,\n ),\n BoolInput(\n name=\"is_stealth_mode_enabled\",\n display_name=\"Enable Stealth Mode (Beta)\",\n info=\"Enable experimental anti-bot evasion strategies. May not work for all websites at all times.\",\n value=False,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Seconds to wait for a request.\",\n value=900,\n advanced=True,\n ),\n DropdownInput(\n name=\"mode\",\n display_name=\"Request Mode\",\n info=\"'standard' uses deep data analysis, while 'fast' trades some depth of analysis for speed.\",\n options=[\"fast\", \"standard\"],\n value=\"fast\",\n advanced=True,\n ),\n IntInput(\n name=\"wait_for\",\n display_name=\"Wait For\",\n info=\"Seconds to wait for the page to load before extracting data.\",\n value=0,\n range_spec=RangeSpec(min=0, max=10, step_type=\"int\"),\n advanced=True,\n ),\n BoolInput(\n name=\"is_scroll_to_bottom_enabled\",\n display_name=\"Enable scroll to bottom\",\n info=\"Scroll to bottom of the page before extracting data.\",\n value=False,\n advanced=True,\n ),\n BoolInput(\n name=\"is_screenshot_enabled\",\n display_name=\"Enable screenshot\",\n info=\"Take a screenshot before extracting data. Returned in 'metadata' as a Base64 string.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"build_output\"),\n ]\n\n def build_output(self) -> Data:\n endpoint = \"https://api.agentql.com/v1/query-data\"\n headers = {\n \"X-API-Key\": self.api_key,\n \"Content-Type\": \"application/json\",\n \"X-TF-Request-Origin\": \"langflow\",\n }\n\n payload = {\n \"url\": self.url,\n \"query\": self.query,\n \"prompt\": self.prompt,\n \"params\": {\n \"mode\": self.mode,\n \"wait_for\": self.wait_for,\n \"is_scroll_to_bottom_enabled\": self.is_scroll_to_bottom_enabled,\n \"is_screenshot_enabled\": self.is_screenshot_enabled,\n },\n \"metadata\": {\n \"experimental_stealth_mode_enabled\": self.is_stealth_mode_enabled,\n },\n }\n\n if not self.prompt and not self.query:\n self.status = \"Either Query or Prompt must be provided.\"\n raise ValueError(self.status)\n if self.prompt and self.query:\n self.status = \"Both Query and Prompt can't be provided at the same time.\"\n raise ValueError(self.status)\n\n try:\n response = httpx.post(endpoint, headers=headers, json=payload, timeout=self.timeout)\n response.raise_for_status()\n\n json = response.json()\n data = Data(result=json[\"data\"], metadata=json[\"metadata\"])\n\n except httpx.HTTPStatusError as e:\n response = e.response\n if response.status_code == httpx.codes.UNAUTHORIZED:\n self.status = \"Please, provide a valid API Key. You can create one at https://dev.agentql.com.\"\n else:\n try:\n error_json = response.json()\n logger.error(\n f\"Failure response: '{response.status_code} {response.reason_phrase}' with body: {error_json}\"\n )\n msg = error_json[\"error_info\"] if \"error_info\" in error_json else error_json[\"detail\"]\n except (ValueError, TypeError):\n msg = f\"HTTP {e}.\"\n self.status = msg\n raise ValueError(self.status) from e\n\n else:\n self.status = data\n return data\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass AgentQL(Component):\n display_name = \"Extract Web Data\"\n description = \"Extracts structured data from a web page using an AgentQL query or a Natural Language description.\"\n documentation: str = \"https://docs.agentql.com/rest-api/api-reference\"\n icon = \"AgentQL\"\n name = \"AgentQL\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"AgentQL API Key\",\n required=True,\n password=True,\n info=\"Your AgentQL API key from dev.agentql.com\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL of the public web page you want to extract data from.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"query\",\n display_name=\"AgentQL Query\",\n required=False,\n info=\"The AgentQL query to execute. Learn more at https://docs.agentql.com/agentql-query or use a prompt.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n required=False,\n info=\"A Natural Language description of the data to extract from the page. Alternative to AgentQL query.\",\n tool_mode=True,\n ),\n BoolInput(\n name=\"is_stealth_mode_enabled\",\n display_name=\"Enable Stealth Mode (Beta)\",\n info=\"Enable experimental anti-bot evasion strategies. May not work for all websites at all times.\",\n value=False,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Seconds to wait for a request.\",\n value=900,\n advanced=True,\n ),\n DropdownInput(\n name=\"mode\",\n display_name=\"Request Mode\",\n info=\"'standard' uses deep data analysis, while 'fast' trades some depth of analysis for speed.\",\n options=[\"fast\", \"standard\"],\n value=\"fast\",\n advanced=True,\n ),\n IntInput(\n name=\"wait_for\",\n display_name=\"Wait For\",\n info=\"Seconds to wait for the page to load before extracting data.\",\n value=0,\n range_spec=RangeSpec(min=0, max=10, step_type=\"int\"),\n advanced=True,\n ),\n BoolInput(\n name=\"is_scroll_to_bottom_enabled\",\n display_name=\"Enable scroll to bottom\",\n info=\"Scroll to bottom of the page before extracting data.\",\n value=False,\n advanced=True,\n ),\n BoolInput(\n name=\"is_screenshot_enabled\",\n display_name=\"Enable screenshot\",\n info=\"Take a screenshot before extracting data. Returned in 'metadata' as a Base64 string.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"build_output\"),\n ]\n\n def build_output(self) -> Data:\n endpoint = \"https://api.agentql.com/v1/query-data\"\n headers = {\n \"X-API-Key\": self.api_key,\n \"Content-Type\": \"application/json\",\n \"X-TF-Request-Origin\": \"langflow\",\n }\n\n payload = {\n \"url\": self.url,\n \"query\": self.query,\n \"prompt\": self.prompt,\n \"params\": {\n \"mode\": self.mode,\n \"wait_for\": self.wait_for,\n \"is_scroll_to_bottom_enabled\": self.is_scroll_to_bottom_enabled,\n \"is_screenshot_enabled\": self.is_screenshot_enabled,\n },\n \"metadata\": {\n \"experimental_stealth_mode_enabled\": self.is_stealth_mode_enabled,\n },\n }\n\n if not self.prompt and not self.query:\n self.status = \"Either Query or Prompt must be provided.\"\n raise ValueError(self.status)\n if self.prompt and self.query:\n self.status = \"Both Query and Prompt can't be provided at the same time.\"\n raise ValueError(self.status)\n\n try:\n response = httpx.post(endpoint, headers=headers, json=payload, timeout=self.timeout)\n response.raise_for_status()\n\n json = response.json()\n data = Data(result=json[\"data\"], metadata=json[\"metadata\"])\n\n except httpx.HTTPStatusError as e:\n response = e.response\n if response.status_code == httpx.codes.UNAUTHORIZED:\n self.status = \"Please, provide a valid API Key. You can create one at https://dev.agentql.com.\"\n else:\n try:\n error_json = response.json()\n logger.error(\n f\"Failure response: '{response.status_code} {response.reason_phrase}' with body: {error_json}\"\n )\n msg = error_json[\"error_info\"] if \"error_info\" in error_json else error_json[\"detail\"]\n except (ValueError, TypeError):\n msg = f\"HTTP {e}.\"\n self.status = msg\n raise ValueError(self.status) from e\n\n else:\n self.status = data\n return data\n" }, "is_screenshot_enabled": { "_input_type": "BoolInput", @@ -876,7 +881,7 @@ "legacy": false, "lf_version": "1.4.3", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -951,7 +956,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1007,7 +1012,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1537,7 +1544,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -1742,7 +1752,7 @@ "last_updated": "2025-09-30T16:16:26.172Z", "legacy": false, "metadata": { - "code_hash": "6d0e4842271e", + "code_hash": "f8b6df3c93c0", "dependencies": { "dependencies": [ { @@ -1943,7 +1953,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nfrom collections.abc import AsyncIterator, Iterator\nfrom pathlib import Path\nfrom typing import Any\n\nimport orjson\nimport pandas as pd\nfrom fastapi import UploadFile\nfrom fastapi.encoders import jsonable_encoder\n\nfrom lfx.custom import Component\nfrom lfx.inputs import SortableListInput\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, SecretStrInput, StrInput\nfrom lfx.schema import Data, DataFrame, Message\nfrom lfx.services.deps import get_settings_service, get_storage_service, session_scope\nfrom lfx.template.field.base import Output\nfrom lfx.utils.validate_cloud import is_astra_cloud_environment\n\n\ndef _get_storage_location_options():\n \"\"\"Get storage location options, filtering out Local if in Astra cloud environment.\"\"\"\n all_options = [{\"name\": \"AWS\", \"icon\": \"Amazon\"}, {\"name\": \"Google Drive\", \"icon\": \"google\"}]\n if is_astra_cloud_environment():\n return all_options\n return [{\"name\": \"Local\", \"icon\": \"hard-drive\"}, *all_options]\n\n\nclass SaveToFileComponent(Component):\n display_name = \"Write File\"\n description = \"Save data to local file, AWS S3, or Google Drive in the selected format.\"\n documentation: str = \"https://docs.langflow.org/write-file\"\n icon = \"file-text\"\n name = \"SaveToFile\"\n\n # File format options for different storage types\n LOCAL_DATA_FORMAT_CHOICES = [\"csv\", \"excel\", \"json\", \"markdown\"]\n LOCAL_MESSAGE_FORMAT_CHOICES = [\"txt\", \"json\", \"markdown\"]\n AWS_FORMAT_CHOICES = [\n \"txt\",\n \"json\",\n \"csv\",\n \"xml\",\n \"html\",\n \"md\",\n \"yaml\",\n \"log\",\n \"tsv\",\n \"jsonl\",\n \"parquet\",\n \"xlsx\",\n \"zip\",\n ]\n GDRIVE_FORMAT_CHOICES = [\"txt\", \"json\", \"csv\", \"xlsx\", \"slides\", \"docs\", \"jpg\", \"mp3\"]\n\n inputs = [\n SortableListInput(\n name=\"storage_location\",\n display_name=\"Storage Location\",\n placeholder=\"Select Location\",\n info=\"Choose where to save the file.\",\n options=_get_storage_location_options(),\n real_time_refresh=True,\n limit=1,\n value=[{\"name\": \"Local\", \"icon\": \"hard-drive\"}],\n advanced=True,\n ),\n # Common inputs\n HandleInput(\n name=\"input\",\n display_name=\"File Content\",\n info=\"The input to save.\",\n dynamic=True,\n input_types=[\"Data\", \"DataFrame\", \"Message\"],\n required=True,\n ),\n StrInput(\n name=\"file_name\",\n display_name=\"File Name\",\n info=\"Name file will be saved as (without extension).\",\n required=True,\n show=False,\n tool_mode=True,\n ),\n BoolInput(\n name=\"append_mode\",\n display_name=\"Append\",\n info=(\n \"Append to file if it exists (only for Local storage with plain text formats). \"\n \"Not supported for cloud storage (AWS/Google Drive).\"\n ),\n value=False,\n show=False,\n ),\n # Format inputs (dynamic based on storage location)\n DropdownInput(\n name=\"local_format\",\n display_name=\"File Format\",\n options=list(dict.fromkeys(LOCAL_DATA_FORMAT_CHOICES + LOCAL_MESSAGE_FORMAT_CHOICES)),\n info=\"Select the file format for local storage.\",\n value=\"json\",\n show=False,\n ),\n DropdownInput(\n name=\"aws_format\",\n display_name=\"File Format\",\n options=AWS_FORMAT_CHOICES,\n info=\"Select the file format for AWS S3 storage.\",\n value=\"txt\",\n show=False,\n ),\n DropdownInput(\n name=\"gdrive_format\",\n display_name=\"File Format\",\n options=GDRIVE_FORMAT_CHOICES,\n info=\"Select the file format for Google Drive storage.\",\n value=\"txt\",\n show=False,\n ),\n # AWS S3 specific inputs\n SecretStrInput(\n name=\"aws_access_key_id\",\n display_name=\"AWS Access Key ID\",\n info=\"AWS Access key ID.\",\n show=False,\n advanced=True,\n required=True,\n ),\n SecretStrInput(\n name=\"aws_secret_access_key\",\n display_name=\"AWS Secret Key\",\n info=\"AWS Secret Key.\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"bucket_name\",\n display_name=\"S3 Bucket Name\",\n info=\"Enter the name of the S3 bucket.\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"aws_region\",\n display_name=\"AWS Region\",\n info=\"AWS region (e.g., us-east-1, eu-west-1).\",\n show=False,\n advanced=True,\n ),\n StrInput(\n name=\"s3_prefix\",\n display_name=\"S3 Prefix\",\n info=\"Prefix for all files in S3.\",\n show=False,\n advanced=True,\n ),\n # Google Drive specific inputs\n SecretStrInput(\n name=\"service_account_key\",\n display_name=\"GCP Credentials Secret Key\",\n info=\"Your Google Cloud Platform service account JSON key as a secret string (complete JSON content).\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"folder_id\",\n display_name=\"Google Drive Folder ID\",\n info=(\n \"The Google Drive folder ID where the file will be uploaded. \"\n \"The folder must be shared with the service account email.\"\n ),\n required=True,\n show=False,\n advanced=True,\n ),\n ]\n\n outputs = [Output(display_name=\"File Path\", name=\"message\", method=\"save_to_file\")]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Update build configuration to show/hide fields based on storage location selection.\"\"\"\n # Update options dynamically based on cloud environment\n # This ensures options are refreshed when build_config is updated\n if \"storage_location\" in build_config:\n updated_options = _get_storage_location_options()\n build_config[\"storage_location\"][\"options\"] = updated_options\n\n if field_name != \"storage_location\":\n return build_config\n\n # Extract selected storage location\n selected = [location[\"name\"] for location in field_value] if isinstance(field_value, list) else []\n\n # Hide all dynamic fields first\n dynamic_fields = [\n \"file_name\", # Common fields (input is always visible)\n \"append_mode\",\n \"local_format\",\n \"aws_format\",\n \"gdrive_format\",\n \"aws_access_key_id\",\n \"aws_secret_access_key\",\n \"bucket_name\",\n \"aws_region\",\n \"s3_prefix\",\n \"service_account_key\",\n \"folder_id\",\n ]\n\n for f_name in dynamic_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = False\n\n # Show fields based on selected storage location\n if len(selected) == 1:\n location = selected[0]\n\n # Show file_name when any storage location is selected\n if \"file_name\" in build_config:\n build_config[\"file_name\"][\"show\"] = True\n\n # Show append_mode only for Local storage (not supported for cloud storage)\n if \"append_mode\" in build_config:\n build_config[\"append_mode\"][\"show\"] = location == \"Local\"\n\n if location == \"Local\":\n if \"local_format\" in build_config:\n build_config[\"local_format\"][\"show\"] = True\n\n elif location == \"AWS\":\n aws_fields = [\n \"aws_format\",\n \"aws_access_key_id\",\n \"aws_secret_access_key\",\n \"bucket_name\",\n \"aws_region\",\n \"s3_prefix\",\n ]\n for f_name in aws_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = True\n build_config[f_name][\"advanced\"] = False\n\n elif location == \"Google Drive\":\n gdrive_fields = [\"gdrive_format\", \"service_account_key\", \"folder_id\"]\n for f_name in gdrive_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = True\n build_config[f_name][\"advanced\"] = False\n\n return build_config\n\n async def save_to_file(self) -> Message:\n \"\"\"Save the input to a file and upload it, returning a confirmation message.\"\"\"\n # Validate inputs\n if not self.file_name:\n msg = \"File name must be provided.\"\n raise ValueError(msg)\n if not self._get_input_type():\n msg = \"Input type is not set.\"\n raise ValueError(msg)\n\n # Get selected storage location\n storage_location = self._get_selected_storage_location()\n if not storage_location:\n msg = \"Storage location must be selected.\"\n raise ValueError(msg)\n\n # Check if Local storage is disabled in cloud environment\n if storage_location == \"Local\" and is_astra_cloud_environment():\n msg = \"Local storage is not available in cloud environment. Please use AWS or Google Drive.\"\n raise ValueError(msg)\n\n # Route to appropriate save method based on storage location\n if storage_location == \"Local\":\n return await self._save_to_local()\n if storage_location == \"AWS\":\n return await self._save_to_aws()\n if storage_location == \"Google Drive\":\n return await self._save_to_google_drive()\n msg = f\"Unsupported storage location: {storage_location}\"\n raise ValueError(msg)\n\n def _get_input_type(self) -> str:\n \"\"\"Determine the input type based on the provided input.\"\"\"\n # Use exact type checking (type() is) instead of isinstance() to avoid inheritance issues.\n # Since Message inherits from Data, isinstance(message, Data) would return True for Message objects,\n # causing Message inputs to be incorrectly identified as Data type.\n if type(self.input) is DataFrame:\n return \"DataFrame\"\n if type(self.input) is Message:\n return \"Message\"\n if type(self.input) is Data:\n return \"Data\"\n msg = f\"Unsupported input type: {type(self.input)}\"\n raise ValueError(msg)\n\n def _get_default_format(self) -> str:\n \"\"\"Return the default file format based on input type.\"\"\"\n if self._get_input_type() == \"DataFrame\":\n return \"csv\"\n if self._get_input_type() == \"Data\":\n return \"json\"\n if self._get_input_type() == \"Message\":\n return \"json\"\n return \"json\" # Fallback\n\n def _adjust_file_path_with_format(self, path: Path, fmt: str) -> Path:\n \"\"\"Adjust the file path to include the correct extension.\"\"\"\n file_extension = path.suffix.lower().lstrip(\".\")\n if fmt == \"excel\":\n return Path(f\"{path}.xlsx\").expanduser() if file_extension not in [\"xlsx\", \"xls\"] else path\n return Path(f\"{path}.{fmt}\").expanduser() if file_extension != fmt else path\n\n def _is_plain_text_format(self, fmt: str) -> bool:\n \"\"\"Check if a file format is plain text (supports appending).\"\"\"\n plain_text_formats = [\"txt\", \"json\", \"markdown\", \"md\", \"csv\", \"xml\", \"html\", \"yaml\", \"log\", \"tsv\", \"jsonl\"]\n return fmt.lower() in plain_text_formats\n\n async def _upload_file(self, file_path: Path) -> None:\n \"\"\"Upload the saved file using the upload_user_file service.\"\"\"\n from langflow.api.v2.files import upload_user_file\n from langflow.services.database.models.user.crud import get_user_by_id\n\n # Ensure the file exists\n if not file_path.exists():\n msg = f\"File not found: {file_path}\"\n raise FileNotFoundError(msg)\n\n # Upload the file - always use append=False because the local file already contains\n # the correct content (either new or appended locally)\n with file_path.open(\"rb\") as f:\n async with session_scope() as db:\n if not self.user_id:\n msg = \"User ID is required for file saving.\"\n raise ValueError(msg)\n current_user = await get_user_by_id(db, self.user_id)\n\n await upload_user_file(\n file=UploadFile(filename=file_path.name, file=f, size=file_path.stat().st_size),\n session=db,\n current_user=current_user,\n storage_service=get_storage_service(),\n settings_service=get_settings_service(),\n append=False,\n )\n\n def _save_dataframe(self, dataframe: DataFrame, path: Path, fmt: str) -> str:\n \"\"\"Save a DataFrame to the specified file format.\"\"\"\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"csv\":\n dataframe.to_csv(path, index=False, mode=\"a\" if should_append else \"w\", header=not should_append)\n elif fmt == \"excel\":\n dataframe.to_excel(path, index=False, engine=\"openpyxl\")\n elif fmt == \"json\":\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new data\n new_records = json.loads(dataframe.to_json(orient=\"records\"))\n existing_data.extend(new_records)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n dataframe.to_json(path, orient=\"records\", indent=2)\n elif fmt == \"markdown\":\n content = dataframe.to_markdown(index=False)\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported DataFrame format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"DataFrame {action} '{path}'\"\n\n def _save_data(self, data: Data, path: Path, fmt: str) -> str:\n \"\"\"Save a Data object to the specified file format.\"\"\"\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"csv\":\n pd.DataFrame(data.data).to_csv(\n path,\n index=False,\n mode=\"a\" if should_append else \"w\",\n header=not should_append,\n )\n elif fmt == \"excel\":\n pd.DataFrame(data.data).to_excel(path, index=False, engine=\"openpyxl\")\n elif fmt == \"json\":\n new_data = jsonable_encoder(data.data)\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new data\n if isinstance(new_data, list):\n existing_data.extend(new_data)\n else:\n existing_data.append(new_data)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n content = orjson.dumps(new_data, option=orjson.OPT_INDENT_2).decode(\"utf-8\")\n path.write_text(content, encoding=\"utf-8\")\n elif fmt == \"markdown\":\n content = pd.DataFrame(data.data).to_markdown(index=False)\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported Data format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"Data {action} '{path}'\"\n\n async def _save_message(self, message: Message, path: Path, fmt: str) -> str:\n \"\"\"Save a Message to the specified file format, handling async iterators.\"\"\"\n content = \"\"\n if message.text is None:\n content = \"\"\n elif isinstance(message.text, AsyncIterator):\n async for item in message.text:\n content += str(item) + \" \"\n content = content.strip()\n elif isinstance(message.text, Iterator):\n content = \" \".join(str(item) for item in message.text)\n else:\n content = str(message.text)\n\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"txt\":\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n elif fmt == \"json\":\n new_message = {\"message\": content}\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new message\n existing_data.append(new_message)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n path.write_text(json.dumps(new_message, indent=2), encoding=\"utf-8\")\n elif fmt == \"markdown\":\n md_content = f\"**Message:**\\n\\n{content}\"\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + md_content, encoding=\"utf-8\")\n else:\n path.write_text(md_content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported Message format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"Message {action} '{path}'\"\n\n def _get_selected_storage_location(self) -> str:\n \"\"\"Get the selected storage location from the SortableListInput.\"\"\"\n if hasattr(self, \"storage_location\") and self.storage_location:\n if isinstance(self.storage_location, list) and len(self.storage_location) > 0:\n return self.storage_location[0].get(\"name\", \"\")\n if isinstance(self.storage_location, dict):\n return self.storage_location.get(\"name\", \"\")\n return \"\"\n\n def _get_file_format_for_location(self, location: str) -> str:\n \"\"\"Get the appropriate file format based on storage location.\"\"\"\n if location == \"Local\":\n return getattr(self, \"local_format\", None) or self._get_default_format()\n if location == \"AWS\":\n return getattr(self, \"aws_format\", \"txt\")\n if location == \"Google Drive\":\n return getattr(self, \"gdrive_format\", \"txt\")\n return self._get_default_format()\n\n async def _save_to_local(self) -> Message:\n \"\"\"Save file to local storage (original functionality).\"\"\"\n file_format = self._get_file_format_for_location(\"Local\")\n\n # Validate file format based on input type\n allowed_formats = (\n self.LOCAL_MESSAGE_FORMAT_CHOICES if self._get_input_type() == \"Message\" else self.LOCAL_DATA_FORMAT_CHOICES\n )\n if file_format not in allowed_formats:\n msg = f\"Invalid file format '{file_format}' for {self._get_input_type()}. Allowed: {allowed_formats}\"\n raise ValueError(msg)\n\n # Prepare file path\n file_path = Path(self.file_name).expanduser()\n if not file_path.parent.exists():\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path = self._adjust_file_path_with_format(file_path, file_format)\n\n # Save the input to file based on type\n if self._get_input_type() == \"DataFrame\":\n confirmation = self._save_dataframe(self.input, file_path, file_format)\n elif self._get_input_type() == \"Data\":\n confirmation = self._save_data(self.input, file_path, file_format)\n elif self._get_input_type() == \"Message\":\n confirmation = await self._save_message(self.input, file_path, file_format)\n else:\n msg = f\"Unsupported input type: {self._get_input_type()}\"\n raise ValueError(msg)\n\n # Upload the saved file\n await self._upload_file(file_path)\n\n # Return the final file path and confirmation message\n final_path = Path.cwd() / file_path if not file_path.is_absolute() else file_path\n return Message(text=f\"{confirmation} at {final_path}\")\n\n async def _save_to_aws(self) -> Message:\n \"\"\"Save file to AWS S3 using S3 functionality.\"\"\"\n import os\n\n import boto3\n\n from lfx.base.data.cloud_storage_utils import create_s3_client, validate_aws_credentials\n\n # Get AWS credentials from component inputs or fall back to environment variables\n aws_access_key_id = getattr(self, \"aws_access_key_id\", None)\n if aws_access_key_id and hasattr(aws_access_key_id, \"get_secret_value\"):\n aws_access_key_id = aws_access_key_id.get_secret_value()\n if not aws_access_key_id:\n aws_access_key_id = os.getenv(\"AWS_ACCESS_KEY_ID\")\n\n aws_secret_access_key = getattr(self, \"aws_secret_access_key\", None)\n if aws_secret_access_key and hasattr(aws_secret_access_key, \"get_secret_value\"):\n aws_secret_access_key = aws_secret_access_key.get_secret_value()\n if not aws_secret_access_key:\n aws_secret_access_key = os.getenv(\"AWS_SECRET_ACCESS_KEY\")\n\n bucket_name = getattr(self, \"bucket_name\", None)\n if not bucket_name:\n # Try to get from storage service settings\n settings = get_settings_service().settings\n bucket_name = settings.object_storage_bucket_name\n\n # Validate AWS credentials\n if not aws_access_key_id:\n msg = (\n \"AWS Access Key ID is required for S3 storage. Provide it as a component input \"\n \"or set AWS_ACCESS_KEY_ID environment variable.\"\n )\n raise ValueError(msg)\n if not aws_secret_access_key:\n msg = (\n \"AWS Secret Key is required for S3 storage. Provide it as a component input \"\n \"or set AWS_SECRET_ACCESS_KEY environment variable.\"\n )\n raise ValueError(msg)\n if not bucket_name:\n msg = (\n \"S3 Bucket Name is required for S3 storage. Provide it as a component input \"\n \"or set LANGFLOW_OBJECT_STORAGE_BUCKET_NAME environment variable.\"\n )\n raise ValueError(msg)\n\n # Validate AWS credentials\n validate_aws_credentials(self)\n\n # Create S3 client\n s3_client = create_s3_client(self)\n client_config: dict[str, Any] = {\n \"aws_access_key_id\": str(aws_access_key_id),\n \"aws_secret_access_key\": str(aws_secret_access_key),\n }\n\n # Get region from component input, environment variable, or settings\n aws_region = getattr(self, \"aws_region\", None)\n if not aws_region:\n aws_region = os.getenv(\"AWS_DEFAULT_REGION\") or os.getenv(\"AWS_REGION\")\n if aws_region:\n client_config[\"region_name\"] = str(aws_region)\n\n s3_client = boto3.client(\"s3\", **client_config)\n\n # Extract content\n content = self._extract_content_for_upload()\n file_format = self._get_file_format_for_location(\"AWS\")\n\n # Generate file path\n file_path = f\"{self.file_name}.{file_format}\"\n if hasattr(self, \"s3_prefix\") and self.s3_prefix:\n file_path = f\"{self.s3_prefix.rstrip('/')}/{file_path}\"\n\n # Create temporary file\n import tempfile\n\n with tempfile.NamedTemporaryFile(\n mode=\"w\", encoding=\"utf-8\", suffix=f\".{file_format}\", delete=False\n ) as temp_file:\n temp_file.write(content)\n temp_file_path = temp_file.name\n\n try:\n # Upload to S3\n s3_client.upload_file(temp_file_path, bucket_name, file_path)\n s3_url = f\"s3://{bucket_name}/{file_path}\"\n return Message(text=f\"File successfully uploaded to {s3_url}\")\n finally:\n # Clean up temp file\n if Path(temp_file_path).exists():\n Path(temp_file_path).unlink()\n\n async def _save_to_google_drive(self) -> Message:\n \"\"\"Save file to Google Drive using Google Drive functionality.\"\"\"\n import tempfile\n\n from googleapiclient.http import MediaFileUpload\n\n from lfx.base.data.cloud_storage_utils import create_google_drive_service\n\n # Validate Google Drive credentials\n if not getattr(self, \"service_account_key\", None):\n msg = \"GCP Credentials Secret Key is required for Google Drive storage\"\n raise ValueError(msg)\n if not getattr(self, \"folder_id\", None):\n msg = \"Google Drive Folder ID is required for Google Drive storage\"\n raise ValueError(msg)\n\n # Create Google Drive service with full drive scope (needed for folder operations)\n drive_service, credentials = create_google_drive_service(\n self.service_account_key, scopes=[\"https://www.googleapis.com/auth/drive\"], return_credentials=True\n )\n\n # Extract content and format\n content = self._extract_content_for_upload()\n file_format = self._get_file_format_for_location(\"Google Drive\")\n\n # Handle special Google Drive formats\n if file_format in [\"slides\", \"docs\"]:\n return await self._save_to_google_apps(drive_service, credentials, content, file_format)\n\n # Create temporary file\n file_path = f\"{self.file_name}.{file_format}\"\n with tempfile.NamedTemporaryFile(\n mode=\"w\",\n encoding=\"utf-8\",\n suffix=f\".{file_format}\",\n delete=False,\n ) as temp_file:\n temp_file.write(content)\n temp_file_path = temp_file.name\n\n try:\n # Upload to Google Drive\n # Note: We skip explicit folder verification since it requires broader permissions.\n # If the folder doesn't exist or isn't accessible, the create() call will fail with a clear error.\n file_metadata = {\"name\": file_path, \"parents\": [self.folder_id]}\n media = MediaFileUpload(temp_file_path, resumable=True)\n\n try:\n uploaded_file = (\n drive_service.files().create(body=file_metadata, media_body=media, fields=\"id\").execute()\n )\n except Exception as e:\n msg = (\n f\"Unable to upload file to Google Drive folder '{self.folder_id}'. \"\n f\"Error: {e!s}. \"\n \"Please ensure: 1) The folder ID is correct, 2) The folder exists, \"\n \"3) The service account has been granted access to this folder.\"\n )\n raise ValueError(msg) from e\n\n file_id = uploaded_file.get(\"id\")\n file_url = f\"https://drive.google.com/file/d/{file_id}/view\"\n return Message(text=f\"File successfully uploaded to Google Drive: {file_url}\")\n finally:\n # Clean up temp file\n if Path(temp_file_path).exists():\n Path(temp_file_path).unlink()\n\n async def _save_to_google_apps(self, drive_service, credentials, content: str, app_type: str) -> Message:\n \"\"\"Save content to Google Apps (Slides or Docs).\"\"\"\n import time\n\n if app_type == \"slides\":\n from googleapiclient.discovery import build\n\n slides_service = build(\"slides\", \"v1\", credentials=credentials)\n\n file_metadata = {\n \"name\": self.file_name,\n \"mimeType\": \"application/vnd.google-apps.presentation\",\n \"parents\": [self.folder_id],\n }\n\n created_file = drive_service.files().create(body=file_metadata, fields=\"id\").execute()\n presentation_id = created_file[\"id\"]\n\n time.sleep(2) # Wait for file to be available # noqa: ASYNC251\n\n presentation = slides_service.presentations().get(presentationId=presentation_id).execute()\n slide_id = presentation[\"slides\"][0][\"objectId\"]\n\n # Add content to slide\n requests = [\n {\n \"createShape\": {\n \"objectId\": \"TextBox_01\",\n \"shapeType\": \"TEXT_BOX\",\n \"elementProperties\": {\n \"pageObjectId\": slide_id,\n \"size\": {\n \"height\": {\"magnitude\": 3000000, \"unit\": \"EMU\"},\n \"width\": {\"magnitude\": 6000000, \"unit\": \"EMU\"},\n },\n \"transform\": {\n \"scaleX\": 1,\n \"scaleY\": 1,\n \"translateX\": 1000000,\n \"translateY\": 1000000,\n \"unit\": \"EMU\",\n },\n },\n }\n },\n {\"insertText\": {\"objectId\": \"TextBox_01\", \"insertionIndex\": 0, \"text\": content}},\n ]\n\n slides_service.presentations().batchUpdate(\n presentationId=presentation_id, body={\"requests\": requests}\n ).execute()\n file_url = f\"https://docs.google.com/presentation/d/{presentation_id}/edit\"\n\n elif app_type == \"docs\":\n from googleapiclient.discovery import build\n\n docs_service = build(\"docs\", \"v1\", credentials=credentials)\n\n file_metadata = {\n \"name\": self.file_name,\n \"mimeType\": \"application/vnd.google-apps.document\",\n \"parents\": [self.folder_id],\n }\n\n created_file = drive_service.files().create(body=file_metadata, fields=\"id\").execute()\n document_id = created_file[\"id\"]\n\n time.sleep(2) # Wait for file to be available # noqa: ASYNC251\n\n # Add content to document\n requests = [{\"insertText\": {\"location\": {\"index\": 1}, \"text\": content}}]\n docs_service.documents().batchUpdate(documentId=document_id, body={\"requests\": requests}).execute()\n file_url = f\"https://docs.google.com/document/d/{document_id}/edit\"\n\n return Message(text=f\"File successfully created in Google {app_type.title()}: {file_url}\")\n\n def _extract_content_for_upload(self) -> str:\n \"\"\"Extract content from input for upload to cloud services.\"\"\"\n if self._get_input_type() == \"DataFrame\":\n return self.input.to_csv(index=False)\n if self._get_input_type() == \"Data\":\n if hasattr(self.input, \"data\") and self.input.data:\n if isinstance(self.input.data, dict):\n import json\n\n return json.dumps(self.input.data, indent=2, ensure_ascii=False)\n return str(self.input.data)\n return str(self.input)\n if self._get_input_type() == \"Message\":\n return str(self.input.text) if self.input.text else str(self.input)\n return str(self.input)\n" + "value": "import json\nfrom collections.abc import AsyncIterator, Iterator\nfrom pathlib import Path\nfrom typing import Any\n\nimport orjson\nimport pandas as pd\nfrom fastapi import UploadFile\nfrom fastapi.encoders import jsonable_encoder\n\nfrom lfx.custom import Component\nfrom lfx.inputs import SortableListInput\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, SecretStrInput, StrInput\nfrom lfx.schema import Data, DataFrame, Message\nfrom lfx.services.deps import get_settings_service, get_storage_service, session_scope\nfrom lfx.template.field.base import Output\nfrom lfx.utils.validate_cloud import is_astra_cloud_environment\n\n\ndef _get_storage_location_options():\n \"\"\"Get storage location options, filtering out Local if in Astra cloud environment.\"\"\"\n all_options = [{\"name\": \"AWS\", \"icon\": \"Amazon\"}, {\"name\": \"Google Drive\", \"icon\": \"google\"}]\n if is_astra_cloud_environment():\n return all_options\n return [{\"name\": \"Local\", \"icon\": \"hard-drive\"}, *all_options]\n\n\nclass SaveToFileComponent(Component):\n display_name = \"Write File\"\n description = \"Save data to local file, AWS S3, or Google Drive in the selected format.\"\n documentation: str = \"https://docs.langflow.org/write-file\"\n icon = \"file-text\"\n name = \"SaveToFile\"\n\n # File format options for different storage types\n LOCAL_DATA_FORMAT_CHOICES = [\"csv\", \"excel\", \"json\", \"markdown\"]\n LOCAL_MESSAGE_FORMAT_CHOICES = [\"txt\", \"json\", \"markdown\"]\n AWS_FORMAT_CHOICES = [\n \"txt\",\n \"json\",\n \"csv\",\n \"xml\",\n \"html\",\n \"md\",\n \"yaml\",\n \"log\",\n \"tsv\",\n \"jsonl\",\n \"parquet\",\n \"xlsx\",\n \"zip\",\n ]\n GDRIVE_FORMAT_CHOICES = [\"txt\", \"json\", \"csv\", \"xlsx\", \"slides\", \"docs\", \"jpg\", \"mp3\"]\n\n inputs = [\n SortableListInput(\n name=\"storage_location\",\n display_name=\"Storage Location\",\n placeholder=\"Select Location\",\n info=\"Choose where to save the file.\",\n options=_get_storage_location_options(),\n real_time_refresh=True,\n limit=1,\n value=[{\"name\": \"Local\", \"icon\": \"hard-drive\"}],\n advanced=True,\n ),\n # Common inputs\n HandleInput(\n name=\"input\",\n display_name=\"File Content\",\n info=\"The input to save.\",\n dynamic=True,\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\", \"Message\"],\n required=True,\n ),\n StrInput(\n name=\"file_name\",\n display_name=\"File Name\",\n info=\"Name file will be saved as (without extension).\",\n required=True,\n show=False,\n tool_mode=True,\n ),\n BoolInput(\n name=\"append_mode\",\n display_name=\"Append\",\n info=(\n \"Append to file if it exists (only for Local storage with plain text formats). \"\n \"Not supported for cloud storage (AWS/Google Drive).\"\n ),\n value=False,\n show=False,\n ),\n # Format inputs (dynamic based on storage location)\n DropdownInput(\n name=\"local_format\",\n display_name=\"File Format\",\n options=list(dict.fromkeys(LOCAL_DATA_FORMAT_CHOICES + LOCAL_MESSAGE_FORMAT_CHOICES)),\n info=\"Select the file format for local storage.\",\n value=\"json\",\n show=False,\n ),\n DropdownInput(\n name=\"aws_format\",\n display_name=\"File Format\",\n options=AWS_FORMAT_CHOICES,\n info=\"Select the file format for AWS S3 storage.\",\n value=\"txt\",\n show=False,\n ),\n DropdownInput(\n name=\"gdrive_format\",\n display_name=\"File Format\",\n options=GDRIVE_FORMAT_CHOICES,\n info=\"Select the file format for Google Drive storage.\",\n value=\"txt\",\n show=False,\n ),\n # AWS S3 specific inputs\n SecretStrInput(\n name=\"aws_access_key_id\",\n display_name=\"AWS Access Key ID\",\n info=\"AWS Access key ID.\",\n show=False,\n advanced=True,\n required=True,\n ),\n SecretStrInput(\n name=\"aws_secret_access_key\",\n display_name=\"AWS Secret Key\",\n info=\"AWS Secret Key.\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"bucket_name\",\n display_name=\"S3 Bucket Name\",\n info=\"Enter the name of the S3 bucket.\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"aws_region\",\n display_name=\"AWS Region\",\n info=\"AWS region (e.g., us-east-1, eu-west-1).\",\n show=False,\n advanced=True,\n ),\n StrInput(\n name=\"s3_prefix\",\n display_name=\"S3 Prefix\",\n info=\"Prefix for all files in S3.\",\n show=False,\n advanced=True,\n ),\n # Google Drive specific inputs\n SecretStrInput(\n name=\"service_account_key\",\n display_name=\"GCP Credentials Secret Key\",\n info=\"Your Google Cloud Platform service account JSON key as a secret string (complete JSON content).\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"folder_id\",\n display_name=\"Google Drive Folder ID\",\n info=(\n \"The Google Drive folder ID where the file will be uploaded. \"\n \"The folder must be shared with the service account email.\"\n ),\n required=True,\n show=False,\n advanced=True,\n ),\n ]\n\n outputs = [Output(display_name=\"File Path\", name=\"message\", method=\"save_to_file\")]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Update build configuration to show/hide fields based on storage location selection.\"\"\"\n # Update options dynamically based on cloud environment\n # This ensures options are refreshed when build_config is updated\n if \"storage_location\" in build_config:\n updated_options = _get_storage_location_options()\n build_config[\"storage_location\"][\"options\"] = updated_options\n\n if field_name != \"storage_location\":\n return build_config\n\n # Extract selected storage location\n selected = [location[\"name\"] for location in field_value] if isinstance(field_value, list) else []\n\n # Hide all dynamic fields first\n dynamic_fields = [\n \"file_name\", # Common fields (input is always visible)\n \"append_mode\",\n \"local_format\",\n \"aws_format\",\n \"gdrive_format\",\n \"aws_access_key_id\",\n \"aws_secret_access_key\",\n \"bucket_name\",\n \"aws_region\",\n \"s3_prefix\",\n \"service_account_key\",\n \"folder_id\",\n ]\n\n for f_name in dynamic_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = False\n\n # Show fields based on selected storage location\n if len(selected) == 1:\n location = selected[0]\n\n # Show file_name when any storage location is selected\n if \"file_name\" in build_config:\n build_config[\"file_name\"][\"show\"] = True\n\n # Show append_mode only for Local storage (not supported for cloud storage)\n if \"append_mode\" in build_config:\n build_config[\"append_mode\"][\"show\"] = location == \"Local\"\n\n if location == \"Local\":\n if \"local_format\" in build_config:\n build_config[\"local_format\"][\"show\"] = True\n\n elif location == \"AWS\":\n aws_fields = [\n \"aws_format\",\n \"aws_access_key_id\",\n \"aws_secret_access_key\",\n \"bucket_name\",\n \"aws_region\",\n \"s3_prefix\",\n ]\n for f_name in aws_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = True\n build_config[f_name][\"advanced\"] = False\n\n elif location == \"Google Drive\":\n gdrive_fields = [\"gdrive_format\", \"service_account_key\", \"folder_id\"]\n for f_name in gdrive_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = True\n build_config[f_name][\"advanced\"] = False\n\n return build_config\n\n async def save_to_file(self) -> Message:\n \"\"\"Save the input to a file and upload it, returning a confirmation message.\"\"\"\n # Validate inputs\n if not self.file_name:\n msg = \"File name must be provided.\"\n raise ValueError(msg)\n if not self._get_input_type():\n msg = \"Input type is not set.\"\n raise ValueError(msg)\n\n # Get selected storage location\n storage_location = self._get_selected_storage_location()\n if not storage_location:\n msg = \"Storage location must be selected.\"\n raise ValueError(msg)\n\n # Check if Local storage is disabled in cloud environment\n if storage_location == \"Local\" and is_astra_cloud_environment():\n msg = \"Local storage is not available in cloud environment. Please use AWS or Google Drive.\"\n raise ValueError(msg)\n\n # Route to appropriate save method based on storage location\n if storage_location == \"Local\":\n return await self._save_to_local()\n if storage_location == \"AWS\":\n return await self._save_to_aws()\n if storage_location == \"Google Drive\":\n return await self._save_to_google_drive()\n msg = f\"Unsupported storage location: {storage_location}\"\n raise ValueError(msg)\n\n def _get_input_type(self) -> str:\n \"\"\"Determine the input type based on the provided input.\"\"\"\n # Use exact type checking (type() is) instead of isinstance() to avoid inheritance issues.\n # Since Message inherits from Data, isinstance(message, Data) would return True for Message objects,\n # causing Message inputs to be incorrectly identified as Data type.\n if type(self.input) is DataFrame:\n return \"DataFrame\"\n if type(self.input) is Message:\n return \"Message\"\n if type(self.input) is Data:\n return \"Data\"\n msg = f\"Unsupported input type: {type(self.input)}\"\n raise ValueError(msg)\n\n def _get_default_format(self) -> str:\n \"\"\"Return the default file format based on input type.\"\"\"\n if self._get_input_type() == \"DataFrame\":\n return \"csv\"\n if self._get_input_type() == \"Data\":\n return \"json\"\n if self._get_input_type() == \"Message\":\n return \"json\"\n return \"json\" # Fallback\n\n def _adjust_file_path_with_format(self, path: Path, fmt: str) -> Path:\n \"\"\"Adjust the file path to include the correct extension.\"\"\"\n file_extension = path.suffix.lower().lstrip(\".\")\n if fmt == \"excel\":\n return Path(f\"{path}.xlsx\").expanduser() if file_extension not in [\"xlsx\", \"xls\"] else path\n return Path(f\"{path}.{fmt}\").expanduser() if file_extension != fmt else path\n\n def _is_plain_text_format(self, fmt: str) -> bool:\n \"\"\"Check if a file format is plain text (supports appending).\"\"\"\n plain_text_formats = [\"txt\", \"json\", \"markdown\", \"md\", \"csv\", \"xml\", \"html\", \"yaml\", \"log\", \"tsv\", \"jsonl\"]\n return fmt.lower() in plain_text_formats\n\n async def _upload_file(self, file_path: Path) -> None:\n \"\"\"Upload the saved file using the upload_user_file service.\"\"\"\n from langflow.api.v2.files import upload_user_file\n from langflow.services.database.models.user.crud import get_user_by_id\n\n # Ensure the file exists\n if not file_path.exists():\n msg = f\"File not found: {file_path}\"\n raise FileNotFoundError(msg)\n\n # Upload the file - always use append=False because the local file already contains\n # the correct content (either new or appended locally)\n with file_path.open(\"rb\") as f:\n async with session_scope() as db:\n if not self.user_id:\n msg = \"User ID is required for file saving.\"\n raise ValueError(msg)\n current_user = await get_user_by_id(db, self.user_id)\n\n await upload_user_file(\n file=UploadFile(filename=file_path.name, file=f, size=file_path.stat().st_size),\n session=db,\n current_user=current_user,\n storage_service=get_storage_service(),\n settings_service=get_settings_service(),\n append=False,\n )\n\n def _save_dataframe(self, dataframe: DataFrame, path: Path, fmt: str) -> str:\n \"\"\"Save a DataFrame to the specified file format.\"\"\"\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"csv\":\n dataframe.to_csv(path, index=False, mode=\"a\" if should_append else \"w\", header=not should_append)\n elif fmt == \"excel\":\n dataframe.to_excel(path, index=False, engine=\"openpyxl\")\n elif fmt == \"json\":\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new data\n new_records = json.loads(dataframe.to_json(orient=\"records\"))\n existing_data.extend(new_records)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n dataframe.to_json(path, orient=\"records\", indent=2)\n elif fmt == \"markdown\":\n content = dataframe.to_markdown(index=False)\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported DataFrame format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"DataFrame {action} '{path}'\"\n\n def _save_data(self, data: Data, path: Path, fmt: str) -> str:\n \"\"\"Save a Data object to the specified file format.\"\"\"\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"csv\":\n pd.DataFrame(data.data).to_csv(\n path,\n index=False,\n mode=\"a\" if should_append else \"w\",\n header=not should_append,\n )\n elif fmt == \"excel\":\n pd.DataFrame(data.data).to_excel(path, index=False, engine=\"openpyxl\")\n elif fmt == \"json\":\n new_data = jsonable_encoder(data.data)\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new data\n if isinstance(new_data, list):\n existing_data.extend(new_data)\n else:\n existing_data.append(new_data)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n content = orjson.dumps(new_data, option=orjson.OPT_INDENT_2).decode(\"utf-8\")\n path.write_text(content, encoding=\"utf-8\")\n elif fmt == \"markdown\":\n content = pd.DataFrame(data.data).to_markdown(index=False)\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported Data format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"Data {action} '{path}'\"\n\n async def _save_message(self, message: Message, path: Path, fmt: str) -> str:\n \"\"\"Save a Message to the specified file format, handling async iterators.\"\"\"\n content = \"\"\n if message.text is None:\n content = \"\"\n elif isinstance(message.text, AsyncIterator):\n async for item in message.text:\n content += str(item) + \" \"\n content = content.strip()\n elif isinstance(message.text, Iterator):\n content = \" \".join(str(item) for item in message.text)\n else:\n content = str(message.text)\n\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"txt\":\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n elif fmt == \"json\":\n new_message = {\"message\": content}\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new message\n existing_data.append(new_message)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n path.write_text(json.dumps(new_message, indent=2), encoding=\"utf-8\")\n elif fmt == \"markdown\":\n md_content = f\"**Message:**\\n\\n{content}\"\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + md_content, encoding=\"utf-8\")\n else:\n path.write_text(md_content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported Message format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"Message {action} '{path}'\"\n\n def _get_selected_storage_location(self) -> str:\n \"\"\"Get the selected storage location from the SortableListInput.\"\"\"\n if hasattr(self, \"storage_location\") and self.storage_location:\n if isinstance(self.storage_location, list) and len(self.storage_location) > 0:\n return self.storage_location[0].get(\"name\", \"\")\n if isinstance(self.storage_location, dict):\n return self.storage_location.get(\"name\", \"\")\n return \"\"\n\n def _get_file_format_for_location(self, location: str) -> str:\n \"\"\"Get the appropriate file format based on storage location.\"\"\"\n if location == \"Local\":\n return getattr(self, \"local_format\", None) or self._get_default_format()\n if location == \"AWS\":\n return getattr(self, \"aws_format\", \"txt\")\n if location == \"Google Drive\":\n return getattr(self, \"gdrive_format\", \"txt\")\n return self._get_default_format()\n\n async def _save_to_local(self) -> Message:\n \"\"\"Save file to local storage (original functionality).\"\"\"\n file_format = self._get_file_format_for_location(\"Local\")\n\n # Validate file format based on input type\n allowed_formats = (\n self.LOCAL_MESSAGE_FORMAT_CHOICES if self._get_input_type() == \"Message\" else self.LOCAL_DATA_FORMAT_CHOICES\n )\n if file_format not in allowed_formats:\n msg = f\"Invalid file format '{file_format}' for {self._get_input_type()}. Allowed: {allowed_formats}\"\n raise ValueError(msg)\n\n # Prepare file path\n file_path = Path(self.file_name).expanduser()\n if not file_path.parent.exists():\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path = self._adjust_file_path_with_format(file_path, file_format)\n\n # Save the input to file based on type\n if self._get_input_type() == \"DataFrame\":\n confirmation = self._save_dataframe(self.input, file_path, file_format)\n elif self._get_input_type() == \"Data\":\n confirmation = self._save_data(self.input, file_path, file_format)\n elif self._get_input_type() == \"Message\":\n confirmation = await self._save_message(self.input, file_path, file_format)\n else:\n msg = f\"Unsupported input type: {self._get_input_type()}\"\n raise ValueError(msg)\n\n # Upload the saved file\n await self._upload_file(file_path)\n\n # Return the final file path and confirmation message\n final_path = Path.cwd() / file_path if not file_path.is_absolute() else file_path\n return Message(text=f\"{confirmation} at {final_path}\")\n\n async def _save_to_aws(self) -> Message:\n \"\"\"Save file to AWS S3 using S3 functionality.\"\"\"\n import os\n\n import boto3\n\n from lfx.base.data.cloud_storage_utils import create_s3_client, validate_aws_credentials\n\n # Get AWS credentials from component inputs or fall back to environment variables\n aws_access_key_id = getattr(self, \"aws_access_key_id\", None)\n if aws_access_key_id and hasattr(aws_access_key_id, \"get_secret_value\"):\n aws_access_key_id = aws_access_key_id.get_secret_value()\n if not aws_access_key_id:\n aws_access_key_id = os.getenv(\"AWS_ACCESS_KEY_ID\")\n\n aws_secret_access_key = getattr(self, \"aws_secret_access_key\", None)\n if aws_secret_access_key and hasattr(aws_secret_access_key, \"get_secret_value\"):\n aws_secret_access_key = aws_secret_access_key.get_secret_value()\n if not aws_secret_access_key:\n aws_secret_access_key = os.getenv(\"AWS_SECRET_ACCESS_KEY\")\n\n bucket_name = getattr(self, \"bucket_name\", None)\n if not bucket_name:\n # Try to get from storage service settings\n settings = get_settings_service().settings\n bucket_name = settings.object_storage_bucket_name\n\n # Validate AWS credentials\n if not aws_access_key_id:\n msg = (\n \"AWS Access Key ID is required for S3 storage. Provide it as a component input \"\n \"or set AWS_ACCESS_KEY_ID environment variable.\"\n )\n raise ValueError(msg)\n if not aws_secret_access_key:\n msg = (\n \"AWS Secret Key is required for S3 storage. Provide it as a component input \"\n \"or set AWS_SECRET_ACCESS_KEY environment variable.\"\n )\n raise ValueError(msg)\n if not bucket_name:\n msg = (\n \"S3 Bucket Name is required for S3 storage. Provide it as a component input \"\n \"or set LANGFLOW_OBJECT_STORAGE_BUCKET_NAME environment variable.\"\n )\n raise ValueError(msg)\n\n # Validate AWS credentials\n validate_aws_credentials(self)\n\n # Create S3 client\n s3_client = create_s3_client(self)\n client_config: dict[str, Any] = {\n \"aws_access_key_id\": str(aws_access_key_id),\n \"aws_secret_access_key\": str(aws_secret_access_key),\n }\n\n # Get region from component input, environment variable, or settings\n aws_region = getattr(self, \"aws_region\", None)\n if not aws_region:\n aws_region = os.getenv(\"AWS_DEFAULT_REGION\") or os.getenv(\"AWS_REGION\")\n if aws_region:\n client_config[\"region_name\"] = str(aws_region)\n\n s3_client = boto3.client(\"s3\", **client_config)\n\n # Extract content\n content = self._extract_content_for_upload()\n file_format = self._get_file_format_for_location(\"AWS\")\n\n # Generate file path\n file_path = f\"{self.file_name}.{file_format}\"\n if hasattr(self, \"s3_prefix\") and self.s3_prefix:\n file_path = f\"{self.s3_prefix.rstrip('/')}/{file_path}\"\n\n # Create temporary file\n import tempfile\n\n with tempfile.NamedTemporaryFile(\n mode=\"w\", encoding=\"utf-8\", suffix=f\".{file_format}\", delete=False\n ) as temp_file:\n temp_file.write(content)\n temp_file_path = temp_file.name\n\n try:\n # Upload to S3\n s3_client.upload_file(temp_file_path, bucket_name, file_path)\n s3_url = f\"s3://{bucket_name}/{file_path}\"\n return Message(text=f\"File successfully uploaded to {s3_url}\")\n finally:\n # Clean up temp file\n if Path(temp_file_path).exists():\n Path(temp_file_path).unlink()\n\n async def _save_to_google_drive(self) -> Message:\n \"\"\"Save file to Google Drive using Google Drive functionality.\"\"\"\n import tempfile\n\n from googleapiclient.http import MediaFileUpload\n\n from lfx.base.data.cloud_storage_utils import create_google_drive_service\n\n # Validate Google Drive credentials\n if not getattr(self, \"service_account_key\", None):\n msg = \"GCP Credentials Secret Key is required for Google Drive storage\"\n raise ValueError(msg)\n if not getattr(self, \"folder_id\", None):\n msg = \"Google Drive Folder ID is required for Google Drive storage\"\n raise ValueError(msg)\n\n # Create Google Drive service with full drive scope (needed for folder operations)\n drive_service, credentials = create_google_drive_service(\n self.service_account_key, scopes=[\"https://www.googleapis.com/auth/drive\"], return_credentials=True\n )\n\n # Extract content and format\n content = self._extract_content_for_upload()\n file_format = self._get_file_format_for_location(\"Google Drive\")\n\n # Handle special Google Drive formats\n if file_format in [\"slides\", \"docs\"]:\n return await self._save_to_google_apps(drive_service, credentials, content, file_format)\n\n # Create temporary file\n file_path = f\"{self.file_name}.{file_format}\"\n with tempfile.NamedTemporaryFile(\n mode=\"w\",\n encoding=\"utf-8\",\n suffix=f\".{file_format}\",\n delete=False,\n ) as temp_file:\n temp_file.write(content)\n temp_file_path = temp_file.name\n\n try:\n # Upload to Google Drive\n # Note: We skip explicit folder verification since it requires broader permissions.\n # If the folder doesn't exist or isn't accessible, the create() call will fail with a clear error.\n file_metadata = {\"name\": file_path, \"parents\": [self.folder_id]}\n media = MediaFileUpload(temp_file_path, resumable=True)\n\n try:\n uploaded_file = (\n drive_service.files().create(body=file_metadata, media_body=media, fields=\"id\").execute()\n )\n except Exception as e:\n msg = (\n f\"Unable to upload file to Google Drive folder '{self.folder_id}'. \"\n f\"Error: {e!s}. \"\n \"Please ensure: 1) The folder ID is correct, 2) The folder exists, \"\n \"3) The service account has been granted access to this folder.\"\n )\n raise ValueError(msg) from e\n\n file_id = uploaded_file.get(\"id\")\n file_url = f\"https://drive.google.com/file/d/{file_id}/view\"\n return Message(text=f\"File successfully uploaded to Google Drive: {file_url}\")\n finally:\n # Clean up temp file\n if Path(temp_file_path).exists():\n Path(temp_file_path).unlink()\n\n async def _save_to_google_apps(self, drive_service, credentials, content: str, app_type: str) -> Message:\n \"\"\"Save content to Google Apps (Slides or Docs).\"\"\"\n import time\n\n if app_type == \"slides\":\n from googleapiclient.discovery import build\n\n slides_service = build(\"slides\", \"v1\", credentials=credentials)\n\n file_metadata = {\n \"name\": self.file_name,\n \"mimeType\": \"application/vnd.google-apps.presentation\",\n \"parents\": [self.folder_id],\n }\n\n created_file = drive_service.files().create(body=file_metadata, fields=\"id\").execute()\n presentation_id = created_file[\"id\"]\n\n time.sleep(2) # Wait for file to be available # noqa: ASYNC251\n\n presentation = slides_service.presentations().get(presentationId=presentation_id).execute()\n slide_id = presentation[\"slides\"][0][\"objectId\"]\n\n # Add content to slide\n requests = [\n {\n \"createShape\": {\n \"objectId\": \"TextBox_01\",\n \"shapeType\": \"TEXT_BOX\",\n \"elementProperties\": {\n \"pageObjectId\": slide_id,\n \"size\": {\n \"height\": {\"magnitude\": 3000000, \"unit\": \"EMU\"},\n \"width\": {\"magnitude\": 6000000, \"unit\": \"EMU\"},\n },\n \"transform\": {\n \"scaleX\": 1,\n \"scaleY\": 1,\n \"translateX\": 1000000,\n \"translateY\": 1000000,\n \"unit\": \"EMU\",\n },\n },\n }\n },\n {\"insertText\": {\"objectId\": \"TextBox_01\", \"insertionIndex\": 0, \"text\": content}},\n ]\n\n slides_service.presentations().batchUpdate(\n presentationId=presentation_id, body={\"requests\": requests}\n ).execute()\n file_url = f\"https://docs.google.com/presentation/d/{presentation_id}/edit\"\n\n elif app_type == \"docs\":\n from googleapiclient.discovery import build\n\n docs_service = build(\"docs\", \"v1\", credentials=credentials)\n\n file_metadata = {\n \"name\": self.file_name,\n \"mimeType\": \"application/vnd.google-apps.document\",\n \"parents\": [self.folder_id],\n }\n\n created_file = drive_service.files().create(body=file_metadata, fields=\"id\").execute()\n document_id = created_file[\"id\"]\n\n time.sleep(2) # Wait for file to be available # noqa: ASYNC251\n\n # Add content to document\n requests = [{\"insertText\": {\"location\": {\"index\": 1}, \"text\": content}}]\n docs_service.documents().batchUpdate(documentId=document_id, body={\"requests\": requests}).execute()\n file_url = f\"https://docs.google.com/document/d/{document_id}/edit\"\n\n return Message(text=f\"File successfully created in Google {app_type.title()}: {file_url}\")\n\n def _extract_content_for_upload(self) -> str:\n \"\"\"Extract content from input for upload to cloud services.\"\"\"\n if self._get_input_type() == \"DataFrame\":\n return self.input.to_csv(index=False)\n if self._get_input_type() == \"Data\":\n if hasattr(self.input, \"data\") and self.input.data:\n if isinstance(self.input.data, dict):\n import json\n\n return json.dumps(self.input.data, indent=2, ensure_ascii=False)\n return str(self.input.data)\n return str(self.input)\n if self._get_input_type() == \"Message\":\n return str(self.input.text) if self.input.text else str(self.input)\n return str(self.input)\n" }, "file_name": { "_input_type": "StrInput", @@ -2022,7 +2032,9 @@ "info": "The input to save.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json b/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json index b6f8317590fd..eb87e6a5d447 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json @@ -46,18 +46,20 @@ "id": "ChatOutput-o3obj", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-Agent-9dON7{œdataTypeœ:œAgentœ,œidœ:œAgent-9dON7œ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-o3obj{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-o3objœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-Agent-9dON7{œdataTypeœ:œAgentœ,œidœ:œAgent-9dON7œ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-o3obj{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-o3objœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "Agent-9dON7", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-9dON7œ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-o3obj", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-o3objœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-o3objœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -96,7 +98,7 @@ "id": "RemixDocumentation-DEIws", "name": "dataframe_output", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -109,10 +111,10 @@ "type": "other" } }, - "id": "reactflow__edge-RemixDocumentation-DEIws{œdataTypeœ:œRemixDocumentationœ,œidœ:œRemixDocumentation-DEIwsœ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œDataFrameœ]}-FAISS-Uz8O4{œfieldNameœ:œingest_dataœ,œidœ:œFAISS-Uz8O4œ,œinputTypesœ:[œDataœ,œDataFrameœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-RemixDocumentation-DEIws{œdataTypeœ:œRemixDocumentationœ,œidœ:œRemixDocumentation-DEIwsœ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œTableœ]}-FAISS-Uz8O4{œfieldNameœ:œingest_dataœ,œidœ:œFAISS-Uz8O4œ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ],œtypeœ:œotherœ}", "selected": false, "source": "RemixDocumentation-DEIws", - "sourceHandle": "{œdataTypeœ: œRemixDocumentationœ, œidœ: œRemixDocumentation-DEIwsœ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œRemixDocumentationœ, œidœ: œRemixDocumentation-DEIwsœ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œTableœ]}", "target": "FAISS-Uz8O4", "targetHandle": "{œfieldNameœ: œingest_dataœ, œidœ: œFAISS-Uz8O4œ, œinputTypesœ: [œDataœ, œDataFrameœ], œtypeœ: œotherœ}" }, @@ -507,7 +509,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -582,7 +584,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -638,7 +640,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1169,7 +1173,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -1580,7 +1587,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -1609,10 +1618,10 @@ "name": "dataframe_output", "options": null, "required_inputs": null, - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -1625,10 +1634,10 @@ "name": "data_output", "options": null, "required_inputs": null, - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -2088,7 +2097,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -2436,7 +2447,8 @@ "id": "MCPTools-beHVJ", "node": { "base_classes": [ - "DataFrame" + "DataFrame", + "Table" ], "beta": false, "category": "data", diff --git "a/src/backend/base/langflow/initial_setup/starter_projects/Pok\303\251dex Agent.json" "b/src/backend/base/langflow/initial_setup/starter_projects/Pok\303\251dex Agent.json" index 4ee110ce1b30..8f4d224776d2 100644 --- "a/src/backend/base/langflow/initial_setup/starter_projects/Pok\303\251dex Agent.json" +++ "b/src/backend/base/langflow/initial_setup/starter_projects/Pok\303\251dex Agent.json" @@ -66,17 +66,19 @@ "id": "ChatOutput-lbrgJ", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "xy-edge__Agent-R27kt{œdataTypeœ:œAgentœ,œidœ:œAgent-R27ktœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-lbrgJ{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-lbrgJœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "xy-edge__Agent-R27kt{œdataTypeœ:œAgentœ,œidœ:œAgent-R27ktœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-lbrgJ{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-lbrgJœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "source": "Agent-R27kt", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-R27ktœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-lbrgJ", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-lbrgJœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-lbrgJœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" } ], "nodes": [ @@ -389,7 +391,7 @@ "legacy": false, "lf_version": "1.2.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -465,7 +467,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -521,7 +523,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -734,7 +738,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "category": "data", @@ -762,7 +768,7 @@ "key": "APIRequest", "legacy": false, "metadata": { - "code_hash": "f102aadfb328", + "code_hash": "2af407885294", "dependencies": { "dependencies": [ { @@ -818,7 +824,8 @@ "dynamic": false, "info": "The body to send with the request as a dictionary (for POST, PATCH, PUT).", "input_types": [ - "Data" + "Data", + "JSON" ], "is_list": true, "list_add_label": "Add More", @@ -879,7 +886,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nimport re\nimport tempfile\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import parse_qsl, urlencode, urlparse, urlunparse\n\nimport aiofiles\nimport aiofiles.os as aiofiles_os\nimport httpx\nimport validators\n\nfrom lfx.base.curl.parse import parse_context\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import TabInput\nfrom lfx.io import (\n BoolInput,\n DataInput,\n DropdownInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n Output,\n TableInput,\n)\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.component_utils import set_current_fields, set_field_advanced, set_field_display\nfrom lfx.utils.ssrf_protection import SSRFProtectionError, validate_url_for_ssrf\n\n# Define fields for each mode\nMODE_FIELDS = {\n \"URL\": [\n \"url_input\",\n \"method\",\n ],\n \"cURL\": [\"curl_input\"],\n}\n\n# Fields that should always be visible\nDEFAULT_FIELDS = [\"mode\"]\n\n\nclass APIRequestComponent(Component):\n display_name = \"API Request\"\n description = \"Make HTTP requests using URL or cURL commands.\"\n documentation: str = \"https://docs.langflow.org/api-request\"\n icon = \"Globe\"\n name = \"APIRequest\"\n\n inputs = [\n MessageTextInput(\n name=\"url_input\",\n display_name=\"URL\",\n info=\"Enter the URL for the request.\",\n advanced=False,\n tool_mode=True,\n ),\n MultilineInput(\n name=\"curl_input\",\n display_name=\"cURL\",\n info=(\n \"Paste a curl command to populate the fields. \"\n \"This will fill in the dictionary fields for headers and body.\"\n ),\n real_time_refresh=True,\n tool_mode=True,\n advanced=True,\n show=False,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Method\",\n options=[\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"],\n value=\"GET\",\n info=\"The HTTP method to use.\",\n real_time_refresh=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"URL\", \"cURL\"],\n value=\"URL\",\n info=\"Enable cURL mode to populate fields from a cURL command.\",\n real_time_refresh=True,\n ),\n DataInput(\n name=\"query_params\",\n display_name=\"Query Parameters\",\n info=\"The query parameters to append to the URL.\",\n advanced=True,\n ),\n TableInput(\n name=\"body\",\n display_name=\"Body\",\n info=\"The body to send with the request as a dictionary (for POST, PATCH, PUT).\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Parameter name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"description\": \"Parameter value\",\n },\n ],\n value=[],\n input_types=[\"Data\"],\n advanced=True,\n real_time_refresh=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": \"Langflow/1.0\"}],\n advanced=True,\n input_types=[\"Data\"],\n real_time_refresh=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n value=30,\n info=\"The timeout to use for the request.\",\n advanced=True,\n ),\n BoolInput(\n name=\"follow_redirects\",\n display_name=\"Follow Redirects\",\n value=False,\n info=(\n \"Whether to follow HTTP redirects. \"\n \"WARNING: Enabling redirects may allow SSRF bypass attacks where a public URL \"\n \"redirects to internal resources. Only enable if you trust the target server. \"\n \"See OWASP SSRF Prevention Cheat Sheet for details.\"\n ),\n advanced=True,\n ),\n BoolInput(\n name=\"save_to_file\",\n display_name=\"Save to File\",\n value=False,\n info=\"Save the API response to a temporary file\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_httpx_metadata\",\n display_name=\"Include HTTPx Metadata\",\n value=False,\n info=(\n \"Include properties such as headers, status_code, response_headers, \"\n \"and redirection_history in the output.\"\n ),\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"API Response\", name=\"data\", method=\"make_api_request\"),\n ]\n\n def _parse_json_value(self, value: Any) -> Any:\n \"\"\"Parse a value that might be a JSON string.\"\"\"\n if not isinstance(value, str):\n return value\n\n try:\n parsed = json.loads(value)\n except json.JSONDecodeError:\n return value\n else:\n return parsed\n\n def _process_body(self, body: Any) -> dict:\n \"\"\"Process the body input into a valid dictionary.\"\"\"\n if body is None:\n return {}\n if hasattr(body, \"data\"):\n body = body.data\n if isinstance(body, dict):\n return self._process_dict_body(body)\n if isinstance(body, str):\n return self._process_string_body(body)\n if isinstance(body, list):\n return self._process_list_body(body)\n return {}\n\n def _process_dict_body(self, body: dict) -> dict:\n \"\"\"Process dictionary body by parsing JSON values.\"\"\"\n return {k: self._parse_json_value(v) for k, v in body.items()}\n\n def _process_string_body(self, body: str) -> dict:\n \"\"\"Process string body by attempting JSON parse.\"\"\"\n try:\n return self._process_body(json.loads(body))\n except json.JSONDecodeError:\n return {\"data\": body}\n\n def _process_list_body(self, body: list) -> dict:\n \"\"\"Process list body by converting to key-value dictionary.\"\"\"\n processed_dict = {}\n try:\n for item in body:\n # Unwrap Data objects\n current_item = item\n if hasattr(item, \"data\"):\n unwrapped_data = item.data\n # If the unwrapped data is a dict but not key-value format, use it directly\n if isinstance(unwrapped_data, dict) and not self._is_valid_key_value_item(unwrapped_data):\n return unwrapped_data\n current_item = unwrapped_data\n if not self._is_valid_key_value_item(current_item):\n continue\n key = current_item[\"key\"]\n value = self._parse_json_value(current_item[\"value\"])\n processed_dict[key] = value\n except (KeyError, TypeError, ValueError) as e:\n self.log(f\"Failed to process body list: {e}\")\n return {}\n return processed_dict\n\n def _is_valid_key_value_item(self, item: Any) -> bool:\n \"\"\"Check if an item is a valid key-value dictionary.\"\"\"\n return isinstance(item, dict) and \"key\" in item and \"value\" in item\n\n def parse_curl(self, curl: str, build_config: dotdict) -> dotdict:\n \"\"\"Parse a cURL command and update build configuration.\"\"\"\n try:\n parsed = parse_context(curl)\n\n # Update basic configuration\n url = parsed.url\n # Normalize URL before setting it\n url = self._normalize_url(url)\n\n build_config[\"url_input\"][\"value\"] = url\n build_config[\"method\"][\"value\"] = parsed.method.upper()\n\n # Process headers\n headers_list = [{\"key\": k, \"value\": v} for k, v in parsed.headers.items()]\n build_config[\"headers\"][\"value\"] = headers_list\n\n # Process body data\n if not parsed.data:\n build_config[\"body\"][\"value\"] = []\n elif parsed.data:\n try:\n json_data = json.loads(parsed.data)\n if isinstance(json_data, dict):\n body_list = [\n {\"key\": k, \"value\": json.dumps(v) if isinstance(v, dict | list) else str(v)}\n for k, v in json_data.items()\n ]\n build_config[\"body\"][\"value\"] = body_list\n else:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": json.dumps(json_data)}]\n except json.JSONDecodeError:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": parsed.data}]\n\n except Exception as exc:\n msg = f\"Error parsing curl: {exc}\"\n self.log(msg)\n raise ValueError(msg) from exc\n\n return build_config\n\n def _normalize_url(self, url: str) -> str:\n \"\"\"Normalize URL by adding https:// if no protocol is specified.\"\"\"\n if not url or not isinstance(url, str):\n msg = \"URL cannot be empty\"\n raise ValueError(msg)\n\n url = url.strip()\n if url.startswith((\"http://\", \"https://\")):\n return url\n return f\"https://{url}\"\n\n async def make_request(\n self,\n client: httpx.AsyncClient,\n method: str,\n url: str,\n headers: dict | None = None,\n body: Any = None,\n timeout: int = 5,\n *,\n follow_redirects: bool = True,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n method = method.upper()\n if method not in {\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"}:\n msg = f\"Unsupported method: {method}\"\n raise ValueError(msg)\n\n processed_body = self._process_body(body)\n redirection_history = []\n\n try:\n # Prepare request parameters\n request_params = {\n \"method\": method,\n \"url\": url,\n \"headers\": headers,\n \"timeout\": timeout,\n \"follow_redirects\": follow_redirects,\n }\n # Only include body for methods that support it (GET must not have a body per HTTP spec)\n if method in {\"POST\", \"PATCH\", \"PUT\", \"DELETE\"} and processed_body is not None:\n request_params[\"json\"] = processed_body\n response = await client.request(**request_params)\n\n redirection_history = [\n {\n \"url\": redirect.headers.get(\"Location\", str(redirect.url)),\n \"status_code\": redirect.status_code,\n }\n for redirect in response.history\n ]\n\n is_binary, file_path = await self._response_info(response, with_file_path=save_to_file)\n response_headers = self._headers_to_dict(response.headers)\n\n # Base metadata\n metadata = {\n \"source\": url,\n \"status_code\": response.status_code,\n \"response_headers\": response_headers,\n }\n\n if redirection_history:\n metadata[\"redirection_history\"] = redirection_history\n\n if save_to_file:\n mode = \"wb\" if is_binary else \"w\"\n encoding = response.encoding if mode == \"w\" else None\n if file_path:\n await aiofiles_os.makedirs(file_path.parent, exist_ok=True)\n if is_binary:\n async with aiofiles.open(file_path, \"wb\") as f:\n await f.write(response.content)\n await f.flush()\n else:\n async with aiofiles.open(file_path, \"w\", encoding=encoding) as f:\n await f.write(response.text)\n await f.flush()\n metadata[\"file_path\"] = str(file_path)\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n return Data(data=metadata)\n\n # Handle response content\n if is_binary:\n result = response.content\n else:\n try:\n result = response.json()\n except json.JSONDecodeError:\n self.log(\"Failed to decode JSON response\")\n result = response.text.encode(\"utf-8\")\n\n metadata[\"result\"] = result\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n\n return Data(data=metadata)\n except (httpx.HTTPError, httpx.RequestError, httpx.TimeoutException) as exc:\n self.log(f\"Error making request to {url}\")\n return Data(\n data={\n \"source\": url,\n \"headers\": headers,\n \"status_code\": 500,\n \"error\": str(exc),\n **({\"redirection_history\": redirection_history} if redirection_history else {}),\n },\n )\n\n def add_query_params(self, url: str, params: dict) -> str:\n \"\"\"Add query parameters to URL efficiently.\"\"\"\n if not params:\n return url\n url_parts = list(urlparse(url))\n query = dict(parse_qsl(url_parts[4]))\n query.update(params)\n url_parts[4] = urlencode(query)\n return urlunparse(url_parts)\n\n def _headers_to_dict(self, headers: httpx.Headers) -> dict[str, str]:\n \"\"\"Convert HTTP headers to a dictionary with lowercased keys.\"\"\"\n return {k.lower(): v for k, v in headers.items()}\n\n def _process_headers(self, headers: Any) -> dict:\n \"\"\"Process the headers input into a valid dictionary.\"\"\"\n if headers is None:\n return {}\n if isinstance(headers, dict):\n return headers\n if isinstance(headers, list):\n return {item[\"key\"]: item[\"value\"] for item in headers if self._is_valid_key_value_item(item)}\n return {}\n\n async def make_api_request(self) -> Data:\n \"\"\"Make HTTP request with optimized parameter handling.\"\"\"\n method = self.method\n url = self.url_input.strip() if isinstance(self.url_input, str) else \"\"\n headers = self.headers or {}\n body = self.body or {}\n timeout = self.timeout\n follow_redirects = self.follow_redirects\n save_to_file = self.save_to_file\n include_httpx_metadata = self.include_httpx_metadata\n\n # Security warning when redirects are enabled\n if follow_redirects:\n self.log(\n \"Security Warning: HTTP redirects are enabled. This may allow SSRF bypass attacks \"\n \"where a public URL redirects to internal resources (e.g., cloud metadata endpoints). \"\n \"Only enable this if you trust the target server.\"\n )\n\n # if self.mode == \"cURL\" and self.curl_input:\n # self._build_config = self.parse_curl(self.curl_input, dotdict())\n # # After parsing curl, get the normalized URL\n # url = self._build_config[\"url_input\"][\"value\"]\n\n # Normalize URL before validation\n url = self._normalize_url(url)\n\n # Validate URL\n if not validators.url(url):\n msg = f\"Invalid URL provided: {url}\"\n raise ValueError(msg)\n\n # SSRF Protection: Validate URL to prevent access to internal resources\n # TODO: In next major version (2.0), remove warn_only=True to enforce blocking\n try:\n validate_url_for_ssrf(url, warn_only=True)\n except SSRFProtectionError as e:\n # This will only raise if SSRF protection is enabled and warn_only=False\n msg = f\"SSRF Protection: {e}\"\n raise ValueError(msg) from e\n\n # Process query parameters\n if isinstance(self.query_params, str):\n query_params = dict(parse_qsl(self.query_params))\n else:\n query_params = self.query_params.data if self.query_params else {}\n\n # Process headers and body\n headers = self._process_headers(headers)\n body = self._process_body(body)\n url = self.add_query_params(url, query_params)\n\n async with httpx.AsyncClient() as client:\n result = await self.make_request(\n client,\n method,\n url,\n headers,\n body,\n timeout,\n follow_redirects=follow_redirects,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n self.status = result\n return result\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n \"\"\"Update the build config based on the selected mode.\"\"\"\n if field_name != \"mode\":\n if field_name == \"curl_input\" and self.mode == \"cURL\" and self.curl_input:\n return self.parse_curl(self.curl_input, build_config)\n return build_config\n\n if field_value == \"cURL\":\n set_field_display(build_config, \"curl_input\", value=True)\n if build_config[\"curl_input\"][\"value\"]:\n try:\n build_config = self.parse_curl(build_config[\"curl_input\"][\"value\"], build_config)\n except ValueError as e:\n self.log(f\"Failed to parse cURL input: {e}\")\n else:\n set_field_display(build_config, \"curl_input\", value=False)\n\n return set_current_fields(\n build_config=build_config,\n action_fields=MODE_FIELDS,\n selected_action=field_value,\n default_fields=DEFAULT_FIELDS,\n func=set_field_advanced,\n default_value=True,\n )\n\n async def _response_info(\n self, response: httpx.Response, *, with_file_path: bool = False\n ) -> tuple[bool, Path | None]:\n \"\"\"Determine the file path and whether the response content is binary.\n\n Args:\n response (Response): The HTTP response object.\n with_file_path (bool): Whether to save the response content to a file.\n\n Returns:\n Tuple[bool, Path | None]:\n A tuple containing a boolean indicating if the content is binary and the full file path (if applicable).\n \"\"\"\n content_type = response.headers.get(\"Content-Type\", \"\")\n is_binary = \"application/octet-stream\" in content_type or \"application/binary\" in content_type\n\n if not with_file_path:\n return is_binary, None\n\n component_temp_dir = Path(tempfile.gettempdir()) / self.__class__.__name__\n\n # Create directory asynchronously\n await aiofiles_os.makedirs(component_temp_dir, exist_ok=True)\n\n filename = None\n if \"Content-Disposition\" in response.headers:\n content_disposition = response.headers[\"Content-Disposition\"]\n filename_match = re.search(r'filename=\"(.+?)\"', content_disposition)\n if filename_match:\n extracted_filename = filename_match.group(1)\n filename = extracted_filename\n\n # Step 3: Infer file extension or use part of the request URL if no filename\n if not filename:\n # Extract the last segment of the URL path\n url_path = urlparse(str(response.request.url) if response.request else \"\").path\n base_name = Path(url_path).name # Get the last segment of the path\n if not base_name: # If the path ends with a slash or is empty\n base_name = \"response\"\n\n # Infer file extension\n content_type_to_extension = {\n \"text/plain\": \".txt\",\n \"application/json\": \".json\",\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"application/octet-stream\": \".bin\",\n }\n extension = content_type_to_extension.get(content_type, \".bin\" if is_binary else \".txt\")\n filename = f\"{base_name}{extension}\"\n\n # Step 4: Define the full file path\n file_path = component_temp_dir / filename\n\n # Step 5: Check if file exists asynchronously and handle accordingly\n try:\n # Try to create the file exclusively (x mode) to check existence\n async with aiofiles.open(file_path, \"x\") as _:\n pass # File created successfully, we can use this path\n except FileExistsError:\n # If file exists, append a timestamp to the filename\n timestamp = datetime.now(timezone.utc).strftime(\"%Y%m%d%H%M%S%f\")\n file_path = component_temp_dir / f\"{timestamp}-{filename}\"\n\n return is_binary, file_path\n" + "value": "import json\nimport re\nimport tempfile\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import parse_qsl, urlencode, urlparse, urlunparse\n\nimport aiofiles\nimport aiofiles.os as aiofiles_os\nimport httpx\nimport validators\n\nfrom lfx.base.curl.parse import parse_context\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import TabInput\nfrom lfx.io import (\n BoolInput,\n DataInput,\n DropdownInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n Output,\n TableInput,\n)\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.component_utils import set_current_fields, set_field_advanced, set_field_display\nfrom lfx.utils.ssrf_protection import SSRFProtectionError, validate_url_for_ssrf\n\n# Define fields for each mode\nMODE_FIELDS = {\n \"URL\": [\n \"url_input\",\n \"method\",\n ],\n \"cURL\": [\"curl_input\"],\n}\n\n# Fields that should always be visible\nDEFAULT_FIELDS = [\"mode\"]\n\n\nclass APIRequestComponent(Component):\n display_name = \"API Request\"\n description = \"Make HTTP requests using URL or cURL commands.\"\n documentation: str = \"https://docs.langflow.org/api-request\"\n icon = \"Globe\"\n name = \"APIRequest\"\n\n inputs = [\n MessageTextInput(\n name=\"url_input\",\n display_name=\"URL\",\n info=\"Enter the URL for the request.\",\n advanced=False,\n tool_mode=True,\n ),\n MultilineInput(\n name=\"curl_input\",\n display_name=\"cURL\",\n info=(\n \"Paste a curl command to populate the fields. \"\n \"This will fill in the dictionary fields for headers and body.\"\n ),\n real_time_refresh=True,\n tool_mode=True,\n advanced=True,\n show=False,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Method\",\n options=[\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"],\n value=\"GET\",\n info=\"The HTTP method to use.\",\n real_time_refresh=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"URL\", \"cURL\"],\n value=\"URL\",\n info=\"Enable cURL mode to populate fields from a cURL command.\",\n real_time_refresh=True,\n ),\n DataInput(\n name=\"query_params\",\n display_name=\"Query Parameters\",\n info=\"The query parameters to append to the URL.\",\n advanced=True,\n ),\n TableInput(\n name=\"body\",\n display_name=\"Body\",\n info=\"The body to send with the request as a dictionary (for POST, PATCH, PUT).\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Parameter name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"description\": \"Parameter value\",\n },\n ],\n value=[],\n input_types=[\"Data\", \"JSON\"],\n advanced=True,\n real_time_refresh=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": \"Langflow/1.0\"}],\n advanced=True,\n input_types=[\"Data\", \"JSON\"],\n real_time_refresh=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n value=30,\n info=\"The timeout to use for the request.\",\n advanced=True,\n ),\n BoolInput(\n name=\"follow_redirects\",\n display_name=\"Follow Redirects\",\n value=False,\n info=(\n \"Whether to follow HTTP redirects. \"\n \"WARNING: Enabling redirects may allow SSRF bypass attacks where a public URL \"\n \"redirects to internal resources. Only enable if you trust the target server. \"\n \"See OWASP SSRF Prevention Cheat Sheet for details.\"\n ),\n advanced=True,\n ),\n BoolInput(\n name=\"save_to_file\",\n display_name=\"Save to File\",\n value=False,\n info=\"Save the API response to a temporary file\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_httpx_metadata\",\n display_name=\"Include HTTPx Metadata\",\n value=False,\n info=(\n \"Include properties such as headers, status_code, response_headers, \"\n \"and redirection_history in the output.\"\n ),\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"API Response\", name=\"data\", method=\"make_api_request\"),\n ]\n\n def _parse_json_value(self, value: Any) -> Any:\n \"\"\"Parse a value that might be a JSON string.\"\"\"\n if not isinstance(value, str):\n return value\n\n try:\n parsed = json.loads(value)\n except json.JSONDecodeError:\n return value\n else:\n return parsed\n\n def _process_body(self, body: Any) -> dict:\n \"\"\"Process the body input into a valid dictionary.\"\"\"\n if body is None:\n return {}\n if hasattr(body, \"data\"):\n body = body.data\n if isinstance(body, dict):\n return self._process_dict_body(body)\n if isinstance(body, str):\n return self._process_string_body(body)\n if isinstance(body, list):\n return self._process_list_body(body)\n return {}\n\n def _process_dict_body(self, body: dict) -> dict:\n \"\"\"Process dictionary body by parsing JSON values.\"\"\"\n return {k: self._parse_json_value(v) for k, v in body.items()}\n\n def _process_string_body(self, body: str) -> dict:\n \"\"\"Process string body by attempting JSON parse.\"\"\"\n try:\n return self._process_body(json.loads(body))\n except json.JSONDecodeError:\n return {\"data\": body}\n\n def _process_list_body(self, body: list) -> dict:\n \"\"\"Process list body by converting to key-value dictionary.\"\"\"\n processed_dict = {}\n try:\n for item in body:\n # Unwrap Data objects\n current_item = item\n if hasattr(item, \"data\"):\n unwrapped_data = item.data\n # If the unwrapped data is a dict but not key-value format, use it directly\n if isinstance(unwrapped_data, dict) and not self._is_valid_key_value_item(unwrapped_data):\n return unwrapped_data\n current_item = unwrapped_data\n if not self._is_valid_key_value_item(current_item):\n continue\n key = current_item[\"key\"]\n value = self._parse_json_value(current_item[\"value\"])\n processed_dict[key] = value\n except (KeyError, TypeError, ValueError) as e:\n self.log(f\"Failed to process body list: {e}\")\n return {}\n return processed_dict\n\n def _is_valid_key_value_item(self, item: Any) -> bool:\n \"\"\"Check if an item is a valid key-value dictionary.\"\"\"\n return isinstance(item, dict) and \"key\" in item and \"value\" in item\n\n def parse_curl(self, curl: str, build_config: dotdict) -> dotdict:\n \"\"\"Parse a cURL command and update build configuration.\"\"\"\n try:\n parsed = parse_context(curl)\n\n # Update basic configuration\n url = parsed.url\n # Normalize URL before setting it\n url = self._normalize_url(url)\n\n build_config[\"url_input\"][\"value\"] = url\n build_config[\"method\"][\"value\"] = parsed.method.upper()\n\n # Process headers\n headers_list = [{\"key\": k, \"value\": v} for k, v in parsed.headers.items()]\n build_config[\"headers\"][\"value\"] = headers_list\n\n # Process body data\n if not parsed.data:\n build_config[\"body\"][\"value\"] = []\n elif parsed.data:\n try:\n json_data = json.loads(parsed.data)\n if isinstance(json_data, dict):\n body_list = [\n {\"key\": k, \"value\": json.dumps(v) if isinstance(v, dict | list) else str(v)}\n for k, v in json_data.items()\n ]\n build_config[\"body\"][\"value\"] = body_list\n else:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": json.dumps(json_data)}]\n except json.JSONDecodeError:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": parsed.data}]\n\n except Exception as exc:\n msg = f\"Error parsing curl: {exc}\"\n self.log(msg)\n raise ValueError(msg) from exc\n\n return build_config\n\n def _normalize_url(self, url: str) -> str:\n \"\"\"Normalize URL by adding https:// if no protocol is specified.\"\"\"\n if not url or not isinstance(url, str):\n msg = \"URL cannot be empty\"\n raise ValueError(msg)\n\n url = url.strip()\n if url.startswith((\"http://\", \"https://\")):\n return url\n return f\"https://{url}\"\n\n async def make_request(\n self,\n client: httpx.AsyncClient,\n method: str,\n url: str,\n headers: dict | None = None,\n body: Any = None,\n timeout: int = 5,\n *,\n follow_redirects: bool = True,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n method = method.upper()\n if method not in {\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"}:\n msg = f\"Unsupported method: {method}\"\n raise ValueError(msg)\n\n processed_body = self._process_body(body)\n redirection_history = []\n\n try:\n # Prepare request parameters\n request_params = {\n \"method\": method,\n \"url\": url,\n \"headers\": headers,\n \"timeout\": timeout,\n \"follow_redirects\": follow_redirects,\n }\n # Only include body for methods that support it (GET must not have a body per HTTP spec)\n if method in {\"POST\", \"PATCH\", \"PUT\", \"DELETE\"} and processed_body is not None:\n request_params[\"json\"] = processed_body\n response = await client.request(**request_params)\n\n redirection_history = [\n {\n \"url\": redirect.headers.get(\"Location\", str(redirect.url)),\n \"status_code\": redirect.status_code,\n }\n for redirect in response.history\n ]\n\n is_binary, file_path = await self._response_info(response, with_file_path=save_to_file)\n response_headers = self._headers_to_dict(response.headers)\n\n # Base metadata\n metadata = {\n \"source\": url,\n \"status_code\": response.status_code,\n \"response_headers\": response_headers,\n }\n\n if redirection_history:\n metadata[\"redirection_history\"] = redirection_history\n\n if save_to_file:\n mode = \"wb\" if is_binary else \"w\"\n encoding = response.encoding if mode == \"w\" else None\n if file_path:\n await aiofiles_os.makedirs(file_path.parent, exist_ok=True)\n if is_binary:\n async with aiofiles.open(file_path, \"wb\") as f:\n await f.write(response.content)\n await f.flush()\n else:\n async with aiofiles.open(file_path, \"w\", encoding=encoding) as f:\n await f.write(response.text)\n await f.flush()\n metadata[\"file_path\"] = str(file_path)\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n return Data(data=metadata)\n\n # Handle response content\n if is_binary:\n result = response.content\n else:\n try:\n result = response.json()\n except json.JSONDecodeError:\n self.log(\"Failed to decode JSON response\")\n result = response.text.encode(\"utf-8\")\n\n metadata[\"result\"] = result\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n\n return Data(data=metadata)\n except (httpx.HTTPError, httpx.RequestError, httpx.TimeoutException) as exc:\n self.log(f\"Error making request to {url}\")\n return Data(\n data={\n \"source\": url,\n \"headers\": headers,\n \"status_code\": 500,\n \"error\": str(exc),\n **({\"redirection_history\": redirection_history} if redirection_history else {}),\n },\n )\n\n def add_query_params(self, url: str, params: dict) -> str:\n \"\"\"Add query parameters to URL efficiently.\"\"\"\n if not params:\n return url\n url_parts = list(urlparse(url))\n query = dict(parse_qsl(url_parts[4]))\n query.update(params)\n url_parts[4] = urlencode(query)\n return urlunparse(url_parts)\n\n def _headers_to_dict(self, headers: httpx.Headers) -> dict[str, str]:\n \"\"\"Convert HTTP headers to a dictionary with lowercased keys.\"\"\"\n return {k.lower(): v for k, v in headers.items()}\n\n def _process_headers(self, headers: Any) -> dict:\n \"\"\"Process the headers input into a valid dictionary.\"\"\"\n if headers is None:\n return {}\n if isinstance(headers, dict):\n return headers\n if isinstance(headers, list):\n return {item[\"key\"]: item[\"value\"] for item in headers if self._is_valid_key_value_item(item)}\n return {}\n\n async def make_api_request(self) -> Data:\n \"\"\"Make HTTP request with optimized parameter handling.\"\"\"\n method = self.method\n url = self.url_input.strip() if isinstance(self.url_input, str) else \"\"\n headers = self.headers or {}\n body = self.body or {}\n timeout = self.timeout\n follow_redirects = self.follow_redirects\n save_to_file = self.save_to_file\n include_httpx_metadata = self.include_httpx_metadata\n\n # Security warning when redirects are enabled\n if follow_redirects:\n self.log(\n \"Security Warning: HTTP redirects are enabled. This may allow SSRF bypass attacks \"\n \"where a public URL redirects to internal resources (e.g., cloud metadata endpoints). \"\n \"Only enable this if you trust the target server.\"\n )\n\n # if self.mode == \"cURL\" and self.curl_input:\n # self._build_config = self.parse_curl(self.curl_input, dotdict())\n # # After parsing curl, get the normalized URL\n # url = self._build_config[\"url_input\"][\"value\"]\n\n # Normalize URL before validation\n url = self._normalize_url(url)\n\n # Validate URL\n if not validators.url(url):\n msg = f\"Invalid URL provided: {url}\"\n raise ValueError(msg)\n\n # SSRF Protection: Validate URL to prevent access to internal resources\n # TODO: In next major version (2.0), remove warn_only=True to enforce blocking\n try:\n validate_url_for_ssrf(url, warn_only=True)\n except SSRFProtectionError as e:\n # This will only raise if SSRF protection is enabled and warn_only=False\n msg = f\"SSRF Protection: {e}\"\n raise ValueError(msg) from e\n\n # Process query parameters\n if isinstance(self.query_params, str):\n query_params = dict(parse_qsl(self.query_params))\n else:\n query_params = self.query_params.data if self.query_params else {}\n\n # Process headers and body\n headers = self._process_headers(headers)\n body = self._process_body(body)\n url = self.add_query_params(url, query_params)\n\n async with httpx.AsyncClient() as client:\n result = await self.make_request(\n client,\n method,\n url,\n headers,\n body,\n timeout,\n follow_redirects=follow_redirects,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n self.status = result\n return result\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n \"\"\"Update the build config based on the selected mode.\"\"\"\n if field_name != \"mode\":\n if field_name == \"curl_input\" and self.mode == \"cURL\" and self.curl_input:\n return self.parse_curl(self.curl_input, build_config)\n return build_config\n\n if field_value == \"cURL\":\n set_field_display(build_config, \"curl_input\", value=True)\n if build_config[\"curl_input\"][\"value\"]:\n try:\n build_config = self.parse_curl(build_config[\"curl_input\"][\"value\"], build_config)\n except ValueError as e:\n self.log(f\"Failed to parse cURL input: {e}\")\n else:\n set_field_display(build_config, \"curl_input\", value=False)\n\n return set_current_fields(\n build_config=build_config,\n action_fields=MODE_FIELDS,\n selected_action=field_value,\n default_fields=DEFAULT_FIELDS,\n func=set_field_advanced,\n default_value=True,\n )\n\n async def _response_info(\n self, response: httpx.Response, *, with_file_path: bool = False\n ) -> tuple[bool, Path | None]:\n \"\"\"Determine the file path and whether the response content is binary.\n\n Args:\n response (Response): The HTTP response object.\n with_file_path (bool): Whether to save the response content to a file.\n\n Returns:\n Tuple[bool, Path | None]:\n A tuple containing a boolean indicating if the content is binary and the full file path (if applicable).\n \"\"\"\n content_type = response.headers.get(\"Content-Type\", \"\")\n is_binary = \"application/octet-stream\" in content_type or \"application/binary\" in content_type\n\n if not with_file_path:\n return is_binary, None\n\n component_temp_dir = Path(tempfile.gettempdir()) / self.__class__.__name__\n\n # Create directory asynchronously\n await aiofiles_os.makedirs(component_temp_dir, exist_ok=True)\n\n filename = None\n if \"Content-Disposition\" in response.headers:\n content_disposition = response.headers[\"Content-Disposition\"]\n filename_match = re.search(r'filename=\"(.+?)\"', content_disposition)\n if filename_match:\n extracted_filename = filename_match.group(1)\n filename = extracted_filename\n\n # Step 3: Infer file extension or use part of the request URL if no filename\n if not filename:\n # Extract the last segment of the URL path\n url_path = urlparse(str(response.request.url) if response.request else \"\").path\n base_name = Path(url_path).name # Get the last segment of the path\n if not base_name: # If the path ends with a slash or is empty\n base_name = \"response\"\n\n # Infer file extension\n content_type_to_extension = {\n \"text/plain\": \".txt\",\n \"application/json\": \".json\",\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"application/octet-stream\": \".bin\",\n }\n extension = content_type_to_extension.get(content_type, \".bin\" if is_binary else \".txt\")\n filename = f\"{base_name}{extension}\"\n\n # Step 4: Define the full file path\n file_path = component_temp_dir / filename\n\n # Step 5: Check if file exists asynchronously and handle accordingly\n try:\n # Try to create the file exclusively (x mode) to check existence\n async with aiofiles.open(file_path, \"x\") as _:\n pass # File created successfully, we can use this path\n except FileExistsError:\n # If file exists, append a timestamp to the filename\n timestamp = datetime.now(timezone.utc).strftime(\"%Y%m%d%H%M%S%f\")\n file_path = component_temp_dir / f\"{timestamp}-{filename}\"\n\n return is_binary, file_path\n" }, "curl_input": { "_input_type": "MultilineInput", @@ -932,7 +939,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "Data" + "Data", + "JSON" ], "is_list": true, "list_add_label": "Add More", @@ -1052,7 +1060,8 @@ "dynamic": false, "info": "The query parameters to append to the URL.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -1600,7 +1609,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json index 2dfbe258f820..4baa54810808 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json @@ -74,18 +74,20 @@ "id": "ChatOutput-ZuZUq", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-LanguageModelComponent-FtHFF{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-FtHFFœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-ZuZUq{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-ZuZUqœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-LanguageModelComponent-FtHFF{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-FtHFFœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-ZuZUq{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-ZuZUqœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "LanguageModelComponent-FtHFF", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-FtHFFœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-ZuZUq", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-ZuZUqœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-ZuZUqœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -124,7 +126,7 @@ "id": "StructuredOutput-jhZm4", "name": "dataframe_output", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -132,17 +134,19 @@ "id": "parser-0gPPL", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "reactflow__edge-StructuredOutput-jhZm4{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-jhZm4œ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œDataFrameœ]}-parser-0gPPL{œfieldNameœ:œinput_dataœ,œidœ:œparser-0gPPLœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-StructuredOutput-jhZm4{œdataTypeœ:œStructuredOutputœ,œidœ:œStructuredOutput-jhZm4œ,œnameœ:œdataframe_outputœ,œoutput_typesœ:[œTableœ]}-parser-0gPPL{œfieldNameœ:œinput_dataœ,œidœ:œparser-0gPPLœ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "StructuredOutput-jhZm4", - "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-jhZm4œ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œStructuredOutputœ, œidœ: œStructuredOutput-jhZm4œ, œnameœ: œdataframe_outputœ, œoutput_typesœ: [œTableœ]}", "target": "parser-0gPPL", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-0gPPLœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-0gPPLœ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -319,7 +323,7 @@ "legacy": false, "lf_version": "1.6.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -395,7 +399,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -451,7 +455,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -757,12 +763,14 @@ "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -1161,6 +1169,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, @@ -2283,7 +2292,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -2336,10 +2347,10 @@ "group_outputs": false, "method": "build_structured_output", "name": "structured_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -2350,10 +2361,10 @@ "group_outputs": false, "method": "build_structured_dataframe", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json b/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json index 066e25d18396..abdefcd50fab 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json @@ -91,17 +91,19 @@ "id": "ChatOutput-ykhew", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "xy-edge__Agent-03SGo{œdataTypeœ:œAgentœ,œidœ:œAgent-03SGoœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-ykhew{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-ykhewœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "xy-edge__Agent-03SGo{œdataTypeœ:œAgentœ,œidœ:œAgent-03SGoœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-ykhew{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-ykhewœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "source": "Agent-03SGo", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-03SGoœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-ykhew", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-ykhewœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-ykhewœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" } ], "nodes": [ @@ -413,7 +415,7 @@ "legacy": false, "lf_version": "1.3.2", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -489,7 +491,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -545,7 +547,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -674,6 +678,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -703,7 +708,7 @@ "legacy": false, "lf_version": "1.3.2", "metadata": { - "code_hash": "e602eaec8316", + "code_hash": "5638a305a99c", "dependencies": { "dependencies": [ { @@ -794,7 +799,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "days": { "_input_type": "IntInput", @@ -1091,7 +1096,8 @@ "id": "AgentQL-qCiM5", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "conditional_paths": [], @@ -1117,7 +1123,7 @@ "legacy": false, "lf_version": "1.3.2", "metadata": { - "code_hash": "37de3210aed9", + "code_hash": "3737ac221d7d", "dependencies": { "dependencies": [ { @@ -1190,7 +1196,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass AgentQL(Component):\n display_name = \"Extract Web Data\"\n description = \"Extracts structured data from a web page using an AgentQL query or a Natural Language description.\"\n documentation: str = \"https://docs.agentql.com/rest-api/api-reference\"\n icon = \"AgentQL\"\n name = \"AgentQL\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"AgentQL API Key\",\n required=True,\n password=True,\n info=\"Your AgentQL API key from dev.agentql.com\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL of the public web page you want to extract data from.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"query\",\n display_name=\"AgentQL Query\",\n required=False,\n info=\"The AgentQL query to execute. Learn more at https://docs.agentql.com/agentql-query or use a prompt.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n required=False,\n info=\"A Natural Language description of the data to extract from the page. Alternative to AgentQL query.\",\n tool_mode=True,\n ),\n BoolInput(\n name=\"is_stealth_mode_enabled\",\n display_name=\"Enable Stealth Mode (Beta)\",\n info=\"Enable experimental anti-bot evasion strategies. May not work for all websites at all times.\",\n value=False,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Seconds to wait for a request.\",\n value=900,\n advanced=True,\n ),\n DropdownInput(\n name=\"mode\",\n display_name=\"Request Mode\",\n info=\"'standard' uses deep data analysis, while 'fast' trades some depth of analysis for speed.\",\n options=[\"fast\", \"standard\"],\n value=\"fast\",\n advanced=True,\n ),\n IntInput(\n name=\"wait_for\",\n display_name=\"Wait For\",\n info=\"Seconds to wait for the page to load before extracting data.\",\n value=0,\n range_spec=RangeSpec(min=0, max=10, step_type=\"int\"),\n advanced=True,\n ),\n BoolInput(\n name=\"is_scroll_to_bottom_enabled\",\n display_name=\"Enable scroll to bottom\",\n info=\"Scroll to bottom of the page before extracting data.\",\n value=False,\n advanced=True,\n ),\n BoolInput(\n name=\"is_screenshot_enabled\",\n display_name=\"Enable screenshot\",\n info=\"Take a screenshot before extracting data. Returned in 'metadata' as a Base64 string.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"build_output\"),\n ]\n\n def build_output(self) -> Data:\n endpoint = \"https://api.agentql.com/v1/query-data\"\n headers = {\n \"X-API-Key\": self.api_key,\n \"Content-Type\": \"application/json\",\n \"X-TF-Request-Origin\": \"langflow\",\n }\n\n payload = {\n \"url\": self.url,\n \"query\": self.query,\n \"prompt\": self.prompt,\n \"params\": {\n \"mode\": self.mode,\n \"wait_for\": self.wait_for,\n \"is_scroll_to_bottom_enabled\": self.is_scroll_to_bottom_enabled,\n \"is_screenshot_enabled\": self.is_screenshot_enabled,\n },\n \"metadata\": {\n \"experimental_stealth_mode_enabled\": self.is_stealth_mode_enabled,\n },\n }\n\n if not self.prompt and not self.query:\n self.status = \"Either Query or Prompt must be provided.\"\n raise ValueError(self.status)\n if self.prompt and self.query:\n self.status = \"Both Query and Prompt can't be provided at the same time.\"\n raise ValueError(self.status)\n\n try:\n response = httpx.post(endpoint, headers=headers, json=payload, timeout=self.timeout)\n response.raise_for_status()\n\n json = response.json()\n data = Data(result=json[\"data\"], metadata=json[\"metadata\"])\n\n except httpx.HTTPStatusError as e:\n response = e.response\n if response.status_code == httpx.codes.UNAUTHORIZED:\n self.status = \"Please, provide a valid API Key. You can create one at https://dev.agentql.com.\"\n else:\n try:\n error_json = response.json()\n logger.error(\n f\"Failure response: '{response.status_code} {response.reason_phrase}' with body: {error_json}\"\n )\n msg = error_json[\"error_info\"] if \"error_info\" in error_json else error_json[\"detail\"]\n except (ValueError, TypeError):\n msg = f\"HTTP {e}.\"\n self.status = msg\n raise ValueError(self.status) from e\n\n else:\n self.status = data\n return data\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass AgentQL(Component):\n display_name = \"Extract Web Data\"\n description = \"Extracts structured data from a web page using an AgentQL query or a Natural Language description.\"\n documentation: str = \"https://docs.agentql.com/rest-api/api-reference\"\n icon = \"AgentQL\"\n name = \"AgentQL\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"AgentQL API Key\",\n required=True,\n password=True,\n info=\"Your AgentQL API key from dev.agentql.com\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL of the public web page you want to extract data from.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"query\",\n display_name=\"AgentQL Query\",\n required=False,\n info=\"The AgentQL query to execute. Learn more at https://docs.agentql.com/agentql-query or use a prompt.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n required=False,\n info=\"A Natural Language description of the data to extract from the page. Alternative to AgentQL query.\",\n tool_mode=True,\n ),\n BoolInput(\n name=\"is_stealth_mode_enabled\",\n display_name=\"Enable Stealth Mode (Beta)\",\n info=\"Enable experimental anti-bot evasion strategies. May not work for all websites at all times.\",\n value=False,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Seconds to wait for a request.\",\n value=900,\n advanced=True,\n ),\n DropdownInput(\n name=\"mode\",\n display_name=\"Request Mode\",\n info=\"'standard' uses deep data analysis, while 'fast' trades some depth of analysis for speed.\",\n options=[\"fast\", \"standard\"],\n value=\"fast\",\n advanced=True,\n ),\n IntInput(\n name=\"wait_for\",\n display_name=\"Wait For\",\n info=\"Seconds to wait for the page to load before extracting data.\",\n value=0,\n range_spec=RangeSpec(min=0, max=10, step_type=\"int\"),\n advanced=True,\n ),\n BoolInput(\n name=\"is_scroll_to_bottom_enabled\",\n display_name=\"Enable scroll to bottom\",\n info=\"Scroll to bottom of the page before extracting data.\",\n value=False,\n advanced=True,\n ),\n BoolInput(\n name=\"is_screenshot_enabled\",\n display_name=\"Enable screenshot\",\n info=\"Take a screenshot before extracting data. Returned in 'metadata' as a Base64 string.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"build_output\"),\n ]\n\n def build_output(self) -> Data:\n endpoint = \"https://api.agentql.com/v1/query-data\"\n headers = {\n \"X-API-Key\": self.api_key,\n \"Content-Type\": \"application/json\",\n \"X-TF-Request-Origin\": \"langflow\",\n }\n\n payload = {\n \"url\": self.url,\n \"query\": self.query,\n \"prompt\": self.prompt,\n \"params\": {\n \"mode\": self.mode,\n \"wait_for\": self.wait_for,\n \"is_scroll_to_bottom_enabled\": self.is_scroll_to_bottom_enabled,\n \"is_screenshot_enabled\": self.is_screenshot_enabled,\n },\n \"metadata\": {\n \"experimental_stealth_mode_enabled\": self.is_stealth_mode_enabled,\n },\n }\n\n if not self.prompt and not self.query:\n self.status = \"Either Query or Prompt must be provided.\"\n raise ValueError(self.status)\n if self.prompt and self.query:\n self.status = \"Both Query and Prompt can't be provided at the same time.\"\n raise ValueError(self.status)\n\n try:\n response = httpx.post(endpoint, headers=headers, json=payload, timeout=self.timeout)\n response.raise_for_status()\n\n json = response.json()\n data = Data(result=json[\"data\"], metadata=json[\"metadata\"])\n\n except httpx.HTTPStatusError as e:\n response = e.response\n if response.status_code == httpx.codes.UNAUTHORIZED:\n self.status = \"Please, provide a valid API Key. You can create one at https://dev.agentql.com.\"\n else:\n try:\n error_json = response.json()\n logger.error(\n f\"Failure response: '{response.status_code} {response.reason_phrase}' with body: {error_json}\"\n )\n msg = error_json[\"error_info\"] if \"error_info\" in error_json else error_json[\"detail\"]\n except (ValueError, TypeError):\n msg = f\"HTTP {e}.\"\n self.status = msg\n raise ValueError(self.status) from e\n\n else:\n self.status = data\n return data\n" }, "is_screenshot_enabled": { "_input_type": "BoolInput", @@ -1972,7 +1978,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json index 8686150f9517..b2ebd55eba92 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json @@ -188,18 +188,20 @@ "id": "ChatOutput-gZuRk", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-LanguageModelComponent-80mt4{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-80mt4œ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-gZuRk{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-gZuRkœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-LanguageModelComponent-80mt4{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-80mt4œ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-gZuRk{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-gZuRkœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "LanguageModelComponent-80mt4", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-80mt4œ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-gZuRk", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-gZuRkœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-gZuRkœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -1319,6 +1321,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -1348,7 +1351,7 @@ "legacy": false, "lf_version": "1.4.3", "metadata": { - "code_hash": "e602eaec8316", + "code_hash": "5638a305a99c", "dependencies": { "dependencies": [ { @@ -1439,7 +1442,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "days": { "_input_type": "IntInput", @@ -1760,7 +1763,7 @@ "legacy": false, "lf_version": "1.4.3", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -1836,7 +1839,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1892,7 +1895,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -3176,7 +3181,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json b/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json index 8ebfa1a31e26..a0b9a76c01a9 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json @@ -37,7 +37,7 @@ "id": "LoopComponent-GtPZT", "name": "item", "output_types": [ - "Data" + "JSON" ] }, "targetHandle": { @@ -45,16 +45,18 @@ "id": "ParserComponent-pXAMb", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "xy-edge__LoopComponent-GtPZT{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-GtPZTœ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ]}-ParserComponent-pXAMb{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-pXAMbœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "xy-edge__LoopComponent-GtPZT{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-GtPZTœ,œnameœ:œitemœ,œoutput_typesœ:[œJSONœ]}-ParserComponent-pXAMb{œfieldNameœ:œinput_dataœ,œidœ:œParserComponent-pXAMbœ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "source": "LoopComponent-GtPZT", - "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-GtPZTœ, œnameœ: œitemœ, œoutput_typesœ: [œDataœ]}", + "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-GtPZTœ, œnameœ: œitemœ, œoutput_typesœ: [œJSONœ]}", "target": "ParserComponent-pXAMb", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-pXAMbœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œParserComponent-pXAMbœ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" }, { "className": "", @@ -117,23 +119,24 @@ "id": "ArXivComponent-tAHR5", "name": "dataframe", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { "fieldName": "data", "id": "LoopComponent-GtPZT", "inputTypes": [ - "DataFrame" + "DataFrame", + "Table" ], "type": "other" } }, - "id": "xy-edge__ArXivComponent-tAHR5{œdataTypeœ:œArXivComponentœ,œidœ:œArXivComponent-tAHR5œ,œnameœ:œdataframeœ,œoutput_typesœ:[œDataFrameœ]}-LoopComponent-GtPZT{œfieldNameœ:œdataœ,œidœ:œLoopComponent-GtPZTœ,œinputTypesœ:[œDataFrameœ],œtypeœ:œotherœ}", + "id": "xy-edge__ArXivComponent-tAHR5{œdataTypeœ:œArXivComponentœ,œidœ:œArXivComponent-tAHR5œ,œnameœ:œdataframeœ,œoutput_typesœ:[œTableœ]}-LoopComponent-GtPZT{œfieldNameœ:œdataœ,œidœ:œLoopComponent-GtPZTœ,œinputTypesœ:[œDataFrameœ,œTableœ],œtypeœ:œotherœ}", "source": "ArXivComponent-tAHR5", - "sourceHandle": "{œdataTypeœ: œArXivComponentœ, œidœ: œArXivComponent-tAHR5œ, œnameœ: œdataframeœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œArXivComponentœ, œidœ: œArXivComponent-tAHR5œ, œnameœ: œdataframeœ, œoutput_typesœ: [œTableœ]}", "target": "LoopComponent-GtPZT", - "targetHandle": "{œfieldNameœ: œdataœ, œidœ: œLoopComponent-GtPZTœ, œinputTypesœ: [œDataFrameœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œdataœ, œidœ: œLoopComponent-GtPZTœ, œinputTypesœ: [œDataFrameœ, œTableœ], œtypeœ: œotherœ}" }, { "className": "", @@ -143,7 +146,7 @@ "id": "LoopComponent-GtPZT", "name": "done", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -151,17 +154,19 @@ "id": "ChatOutput-YAED9", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "xy-edge__LoopComponent-GtPZT{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-GtPZTœ,œnameœ:œdoneœ,œoutput_typesœ:[œDataFrameœ]}-ChatOutput-YAED9{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-YAED9œ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "xy-edge__LoopComponent-GtPZT{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-GtPZTœ,œnameœ:œdoneœ,œoutput_typesœ:[œTableœ]}-ChatOutput-YAED9{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-YAED9œ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "source": "LoopComponent-GtPZT", - "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-GtPZTœ, œnameœ: œdoneœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œLoopComponentœ, œidœ: œLoopComponent-GtPZTœ, œnameœ: œdoneœ, œoutput_typesœ: [œTableœ]}", "target": "ChatOutput-YAED9", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-YAED9œ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-YAED9œ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -170,7 +175,8 @@ "id": "ArXivComponent-tAHR5", "node": { "base_classes": [ - "DataFrame" + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -189,7 +195,7 @@ "legacy": false, "lf_version": "1.7.0", "metadata": { - "code_hash": "219239ee2b48", + "code_hash": "2d892beaf98b", "dependencies": { "dependencies": [ { @@ -211,14 +217,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "search_papers_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -242,7 +248,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import urllib.request\nfrom urllib.parse import urlparse\nfrom xml.etree.ElementTree import Element\n\nfrom defusedxml.ElementTree import fromstring\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass ArXivComponent(Component):\n display_name = \"arXiv\"\n description = \"Search and retrieve papers from arXiv.org\"\n icon = \"arXiv\"\n\n inputs = [\n MessageTextInput(\n name=\"search_query\",\n display_name=\"Search Query\",\n info=\"The search query for arXiv papers (e.g., 'quantum computing')\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_type\",\n display_name=\"Search Field\",\n info=\"The field to search in\",\n options=[\"all\", \"title\", \"abstract\", \"author\", \"cat\"], # cat is for category\n value=\"all\",\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"Maximum number of results to return\",\n value=10,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"search_papers_dataframe\"),\n ]\n\n def build_query_url(self) -> str:\n \"\"\"Build the arXiv API query URL.\"\"\"\n base_url = \"http://export.arxiv.org/api/query?\"\n\n # Build the search query based on search type\n if self.search_type == \"all\":\n search_query = self.search_query # No prefix for all fields\n else:\n # Map dropdown values to ArXiv API prefixes\n prefix_map = {\"title\": \"ti\", \"abstract\": \"abs\", \"author\": \"au\", \"cat\": \"cat\"}\n prefix = prefix_map.get(self.search_type, \"\")\n search_query = f\"{prefix}:{self.search_query}\"\n\n # URL parameters\n params = {\n \"search_query\": search_query,\n \"max_results\": str(self.max_results),\n }\n\n # Convert params to URL query string\n query_string = \"&\".join([f\"{k}={urllib.parse.quote(str(v))}\" for k, v in params.items()])\n\n return base_url + query_string\n\n def parse_atom_response(self, response_text: str) -> list[dict]:\n \"\"\"Parse the Atom XML response from arXiv.\"\"\"\n # Parse XML safely using defusedxml\n root = fromstring(response_text)\n\n # Define namespace dictionary for XML parsing\n ns = {\"atom\": \"http://www.w3.org/2005/Atom\", \"arxiv\": \"http://arxiv.org/schemas/atom\"}\n\n papers = []\n # Process each entry (paper)\n for entry in root.findall(\"atom:entry\", ns):\n paper = {\n \"id\": self._get_text(entry, \"atom:id\", ns),\n \"title\": self._get_text(entry, \"atom:title\", ns),\n \"summary\": self._get_text(entry, \"atom:summary\", ns),\n \"published\": self._get_text(entry, \"atom:published\", ns),\n \"updated\": self._get_text(entry, \"atom:updated\", ns),\n \"authors\": [author.find(\"atom:name\", ns).text for author in entry.findall(\"atom:author\", ns)],\n \"arxiv_url\": self._get_link(entry, \"alternate\", ns),\n \"pdf_url\": self._get_link(entry, \"related\", ns),\n \"comment\": self._get_text(entry, \"arxiv:comment\", ns),\n \"journal_ref\": self._get_text(entry, \"arxiv:journal_ref\", ns),\n \"primary_category\": self._get_category(entry, ns),\n \"categories\": [cat.get(\"term\") for cat in entry.findall(\"atom:category\", ns)],\n }\n papers.append(paper)\n\n return papers\n\n def _get_text(self, element: Element, path: str, ns: dict) -> str | None:\n \"\"\"Safely extract text from an XML element.\"\"\"\n el = element.find(path, ns)\n return el.text.strip() if el is not None and el.text else None\n\n def _get_link(self, element: Element, rel: str, ns: dict) -> str | None:\n \"\"\"Get link URL based on relation type.\"\"\"\n for link in element.findall(\"atom:link\", ns):\n if link.get(\"rel\") == rel:\n return link.get(\"href\")\n return None\n\n def _get_category(self, element: Element, ns: dict) -> str | None:\n \"\"\"Get primary category.\"\"\"\n cat = element.find(\"arxiv:primary_category\", ns)\n return cat.get(\"term\") if cat is not None else None\n\n def run_model(self) -> DataFrame:\n return self.search_papers_dataframe()\n\n def search_papers(self) -> list[Data]:\n \"\"\"Search arXiv and return results.\"\"\"\n try:\n # Build the query URL\n url = self.build_query_url()\n\n # Validate URL scheme and host\n parsed_url = urlparse(url)\n if parsed_url.scheme not in {\"http\", \"https\"}:\n error_msg = f\"Invalid URL scheme: {parsed_url.scheme}\"\n raise ValueError(error_msg)\n if parsed_url.hostname != \"export.arxiv.org\":\n error_msg = f\"Invalid host: {parsed_url.hostname}\"\n raise ValueError(error_msg)\n\n # Create a custom opener that only allows http/https schemes\n class RestrictedHTTPHandler(urllib.request.HTTPHandler):\n def http_open(self, req):\n return super().http_open(req)\n\n class RestrictedHTTPSHandler(urllib.request.HTTPSHandler):\n def https_open(self, req):\n return super().https_open(req)\n\n # Build opener with restricted handlers\n opener = urllib.request.build_opener(RestrictedHTTPHandler, RestrictedHTTPSHandler)\n urllib.request.install_opener(opener)\n\n # Make the request with validated URL using restricted opener\n response = opener.open(url)\n response_text = response.read().decode(\"utf-8\")\n\n # Parse the response\n papers = self.parse_atom_response(response_text)\n\n # Convert to Data objects\n results = [Data(data=paper) for paper in papers]\n self.status = results\n except (urllib.error.URLError, ValueError) as e:\n error_data = Data(data={\"error\": f\"Request error: {e!s}\"})\n self.status = error_data\n return [error_data]\n else:\n return results\n\n def search_papers_dataframe(self) -> DataFrame:\n \"\"\"Convert the Arxiv search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.search_papers()\n return DataFrame(data)\n" + "value": "import urllib.request\nfrom urllib.parse import urlparse\nfrom xml.etree.ElementTree import Element\n\nfrom defusedxml.ElementTree import fromstring\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass ArXivComponent(Component):\n display_name = \"arXiv\"\n description = \"Search and retrieve papers from arXiv.org\"\n icon = \"arXiv\"\n\n inputs = [\n MessageTextInput(\n name=\"search_query\",\n display_name=\"Search Query\",\n info=\"The search query for arXiv papers (e.g., 'quantum computing')\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_type\",\n display_name=\"Search Field\",\n info=\"The field to search in\",\n options=[\"all\", \"title\", \"abstract\", \"author\", \"cat\"], # cat is for category\n value=\"all\",\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"Maximum number of results to return\",\n value=10,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"search_papers_dataframe\"),\n ]\n\n def build_query_url(self) -> str:\n \"\"\"Build the arXiv API query URL.\"\"\"\n base_url = \"http://export.arxiv.org/api/query?\"\n\n # Build the search query based on search type\n if self.search_type == \"all\":\n search_query = self.search_query # No prefix for all fields\n else:\n # Map dropdown values to ArXiv API prefixes\n prefix_map = {\"title\": \"ti\", \"abstract\": \"abs\", \"author\": \"au\", \"cat\": \"cat\"}\n prefix = prefix_map.get(self.search_type, \"\")\n search_query = f\"{prefix}:{self.search_query}\"\n\n # URL parameters\n params = {\n \"search_query\": search_query,\n \"max_results\": str(self.max_results),\n }\n\n # Convert params to URL query string\n query_string = \"&\".join([f\"{k}={urllib.parse.quote(str(v))}\" for k, v in params.items()])\n\n return base_url + query_string\n\n def parse_atom_response(self, response_text: str) -> list[dict]:\n \"\"\"Parse the Atom XML response from arXiv.\"\"\"\n # Parse XML safely using defusedxml\n root = fromstring(response_text)\n\n # Define namespace dictionary for XML parsing\n ns = {\"atom\": \"http://www.w3.org/2005/Atom\", \"arxiv\": \"http://arxiv.org/schemas/atom\"}\n\n papers = []\n # Process each entry (paper)\n for entry in root.findall(\"atom:entry\", ns):\n paper = {\n \"id\": self._get_text(entry, \"atom:id\", ns),\n \"title\": self._get_text(entry, \"atom:title\", ns),\n \"summary\": self._get_text(entry, \"atom:summary\", ns),\n \"published\": self._get_text(entry, \"atom:published\", ns),\n \"updated\": self._get_text(entry, \"atom:updated\", ns),\n \"authors\": [author.find(\"atom:name\", ns).text for author in entry.findall(\"atom:author\", ns)],\n \"arxiv_url\": self._get_link(entry, \"alternate\", ns),\n \"pdf_url\": self._get_link(entry, \"related\", ns),\n \"comment\": self._get_text(entry, \"arxiv:comment\", ns),\n \"journal_ref\": self._get_text(entry, \"arxiv:journal_ref\", ns),\n \"primary_category\": self._get_category(entry, ns),\n \"categories\": [cat.get(\"term\") for cat in entry.findall(\"atom:category\", ns)],\n }\n papers.append(paper)\n\n return papers\n\n def _get_text(self, element: Element, path: str, ns: dict) -> str | None:\n \"\"\"Safely extract text from an XML element.\"\"\"\n el = element.find(path, ns)\n return el.text.strip() if el is not None and el.text else None\n\n def _get_link(self, element: Element, rel: str, ns: dict) -> str | None:\n \"\"\"Get link URL based on relation type.\"\"\"\n for link in element.findall(\"atom:link\", ns):\n if link.get(\"rel\") == rel:\n return link.get(\"href\")\n return None\n\n def _get_category(self, element: Element, ns: dict) -> str | None:\n \"\"\"Get primary category.\"\"\"\n cat = element.find(\"arxiv:primary_category\", ns)\n return cat.get(\"term\") if cat is not None else None\n\n def run_model(self) -> DataFrame:\n return self.search_papers_dataframe()\n\n def search_papers(self) -> list[Data]:\n \"\"\"Search arXiv and return results.\"\"\"\n try:\n # Build the query URL\n url = self.build_query_url()\n\n # Validate URL scheme and host\n parsed_url = urlparse(url)\n if parsed_url.scheme not in {\"http\", \"https\"}:\n error_msg = f\"Invalid URL scheme: {parsed_url.scheme}\"\n raise ValueError(error_msg)\n if parsed_url.hostname != \"export.arxiv.org\":\n error_msg = f\"Invalid host: {parsed_url.hostname}\"\n raise ValueError(error_msg)\n\n # Create a custom opener that only allows http/https schemes\n class RestrictedHTTPHandler(urllib.request.HTTPHandler):\n def http_open(self, req):\n return super().http_open(req)\n\n class RestrictedHTTPSHandler(urllib.request.HTTPSHandler):\n def https_open(self, req):\n return super().https_open(req)\n\n # Build opener with restricted handlers\n opener = urllib.request.build_opener(RestrictedHTTPHandler, RestrictedHTTPSHandler)\n urllib.request.install_opener(opener)\n\n # Make the request with validated URL using restricted opener\n response = opener.open(url)\n response_text = response.read().decode(\"utf-8\")\n\n # Parse the response\n papers = self.parse_atom_response(response_text)\n\n # Convert to Data objects\n results = [Data(data=paper) for paper in papers]\n self.status = results\n except (urllib.error.URLError, ValueError) as e:\n error_data = Data(data={\"error\": f\"Request error: {e!s}\"})\n self.status = error_data\n return [error_data]\n else:\n return results\n\n def search_papers_dataframe(self) -> DataFrame:\n \"\"\"Convert the Arxiv search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.search_papers()\n return DataFrame(data)\n" }, "max_results": { "_input_type": "IntInput", @@ -361,7 +367,7 @@ "legacy": false, "lf_version": "1.7.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -436,7 +442,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -492,7 +498,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1441,7 +1449,9 @@ "node": { "base_classes": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -1457,7 +1467,7 @@ "icon": "infinity", "legacy": false, "metadata": { - "code_hash": "e516ea99611c", + "code_hash": "f789817c7cd3", "dependencies": { "dependencies": [ { @@ -1482,10 +1492,10 @@ ], "method": "item_output", "name": "item", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -1496,10 +1506,10 @@ "group_outputs": true, "method": "done_output", "name": "done", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -1523,7 +1533,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.flow_controls.loop_utils import (\n execute_loop_body,\n extract_loop_output,\n get_loop_body_start_edge,\n get_loop_body_start_vertex,\n get_loop_body_vertices,\n validate_data_input,\n)\nfrom lfx.components.processing.converter import convert_to_data\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass LoopComponent(Component):\n display_name = \"Loop\"\n description = (\n \"Iterates through Data or Message objects, processing items individually \"\n \"and aggregating results from loop inputs.\"\n )\n documentation: str = \"https://docs.langflow.org/loop\"\n icon = \"infinity\"\n\n inputs = [\n HandleInput(\n name=\"data\",\n display_name=\"Inputs\",\n info=\"The initial DataFrame to iterate over.\",\n input_types=[\"DataFrame\"],\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Item\",\n name=\"item\",\n method=\"item_output\",\n allows_loop=True,\n loop_types=[\"Message\"],\n group_outputs=True,\n ),\n Output(display_name=\"Done\", name=\"done\", method=\"done_output\", group_outputs=True),\n ]\n\n def initialize_data(self) -> None:\n \"\"\"Initialize the data list, context index, and aggregated list.\"\"\"\n if self.ctx.get(f\"{self._id}_initialized\", False):\n return\n\n # Ensure data is a list of Data objects\n data_list = self._validate_data(self.data)\n\n # Store the initial data and context variables\n self.update_ctx(\n {\n f\"{self._id}_data\": data_list,\n f\"{self._id}_index\": 0,\n f\"{self._id}_aggregated\": [],\n f\"{self._id}_initialized\": True,\n }\n )\n\n def _convert_message_to_data(self, message: Message) -> Data:\n \"\"\"Convert a Message object to a Data object using Type Convert logic.\"\"\"\n return convert_to_data(message, auto_parse=False)\n\n def _validate_data(self, data):\n \"\"\"Validate and return a list of Data objects.\"\"\"\n return validate_data_input(data)\n\n def get_loop_body_vertices(self) -> set[str]:\n \"\"\"Identify vertices in this loop's body via graph traversal.\n\n Traverses from the loop's \"item\" output to the vertex that feeds back\n to the loop's \"item\" input, collecting all vertices in between.\n This naturally handles nested loops by stopping at this loop's feedback edge.\n\n Returns:\n Set of vertex IDs that form this loop's body\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return set()\n\n return get_loop_body_vertices(\n vertex=self._vertex,\n graph=self.graph,\n get_incoming_edge_by_target_param_fn=self.get_incoming_edge_by_target_param,\n )\n\n def _get_loop_body_start_vertex(self) -> str | None:\n \"\"\"Get the first vertex in the loop body (connected to loop's item output).\n\n Returns:\n The vertex ID of the first vertex in the loop body, or None if not found\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return None\n\n return get_loop_body_start_vertex(vertex=self._vertex)\n\n def _extract_loop_output(self, results: list) -> Data:\n \"\"\"Extract the output from subgraph execution results.\n\n Args:\n results: List of VertexBuildResult objects from subgraph execution\n\n Returns:\n Data object containing the loop iteration output\n \"\"\"\n # Get the vertex ID that feeds back to the item input (end of loop body)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n return extract_loop_output(results=results, end_vertex_id=end_vertex_id)\n\n async def execute_loop_body(self, data_list: list[Data], event_manager=None) -> list[Data]:\n \"\"\"Execute loop body for each data item.\n\n Creates an isolated subgraph for the loop body and executes it\n for each item in the data list, collecting results.\n\n Args:\n data_list: List of Data objects to iterate over\n event_manager: Optional event manager to pass to subgraph execution for UI events\n\n Returns:\n List of Data objects containing results from each iteration\n \"\"\"\n # Get the loop body configuration once\n loop_body_vertex_ids = self.get_loop_body_vertices()\n start_vertex_id = self._get_loop_body_start_vertex()\n start_edge = get_loop_body_start_edge(self._vertex)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n\n return await execute_loop_body(\n graph=self.graph,\n data_list=data_list,\n loop_body_vertex_ids=loop_body_vertex_ids,\n start_vertex_id=start_vertex_id,\n start_edge=start_edge,\n end_vertex_id=end_vertex_id,\n event_manager=event_manager,\n )\n\n def item_output(self) -> Data:\n \"\"\"Output is no longer used - loop executes internally now.\n\n This method is kept for backward compatibility but does nothing.\n The actual loop execution happens in done_output().\n \"\"\"\n self.stop(\"item\")\n return Data(text=\"\")\n\n async def done_output(self) -> DataFrame:\n \"\"\"Execute the loop body for all items and return aggregated results.\n\n This is now the main execution point for the loop. It:\n 1. Gets the data list to iterate over\n 2. Executes the loop body as an isolated subgraph for each item\n 3. Returns the aggregated results\n\n Args:\n event_manager: Optional event manager for UI event emission\n \"\"\"\n self.initialize_data()\n\n # Get data list\n data_list = self.ctx.get(f\"{self._id}_data\", [])\n\n if not data_list:\n return DataFrame([])\n\n # Execute loop body for all items\n try:\n aggregated_results = await self.execute_loop_body(data_list, event_manager=self._event_manager)\n return DataFrame(aggregated_results)\n except Exception as e:\n # Log error and return empty DataFrame\n from lfx.log.logger import logger\n\n await logger.aerror(f\"Error executing loop body: {e}\")\n raise\n" + "value": "from lfx.base.flow_controls.loop_utils import (\n execute_loop_body,\n extract_loop_output,\n get_loop_body_start_edge,\n get_loop_body_start_vertex,\n get_loop_body_vertices,\n validate_data_input,\n)\nfrom lfx.components.processing.converter import convert_to_data\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass LoopComponent(Component):\n display_name = \"Loop\"\n description = (\n \"Iterates through Data or Message objects, processing items individually \"\n \"and aggregating results from loop inputs.\"\n )\n documentation: str = \"https://docs.langflow.org/loop\"\n icon = \"infinity\"\n\n inputs = [\n HandleInput(\n name=\"data\",\n display_name=\"Inputs\",\n info=\"The initial DataFrame to iterate over.\",\n input_types=[\"DataFrame\", \"Table\"],\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Item\",\n name=\"item\",\n method=\"item_output\",\n allows_loop=True,\n loop_types=[\"Message\"],\n group_outputs=True,\n ),\n Output(display_name=\"Done\", name=\"done\", method=\"done_output\", group_outputs=True),\n ]\n\n def initialize_data(self) -> None:\n \"\"\"Initialize the data list, context index, and aggregated list.\"\"\"\n if self.ctx.get(f\"{self._id}_initialized\", False):\n return\n\n # Ensure data is a list of Data objects\n data_list = self._validate_data(self.data)\n\n # Store the initial data and context variables\n self.update_ctx(\n {\n f\"{self._id}_data\": data_list,\n f\"{self._id}_index\": 0,\n f\"{self._id}_aggregated\": [],\n f\"{self._id}_initialized\": True,\n }\n )\n\n def _convert_message_to_data(self, message: Message) -> Data:\n \"\"\"Convert a Message object to a Data object using Type Convert logic.\"\"\"\n return convert_to_data(message, auto_parse=False)\n\n def _validate_data(self, data):\n \"\"\"Validate and return a list of Data objects.\"\"\"\n return validate_data_input(data)\n\n def get_loop_body_vertices(self) -> set[str]:\n \"\"\"Identify vertices in this loop's body via graph traversal.\n\n Traverses from the loop's \"item\" output to the vertex that feeds back\n to the loop's \"item\" input, collecting all vertices in between.\n This naturally handles nested loops by stopping at this loop's feedback edge.\n\n Returns:\n Set of vertex IDs that form this loop's body\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return set()\n\n return get_loop_body_vertices(\n vertex=self._vertex,\n graph=self.graph,\n get_incoming_edge_by_target_param_fn=self.get_incoming_edge_by_target_param,\n )\n\n def _get_loop_body_start_vertex(self) -> str | None:\n \"\"\"Get the first vertex in the loop body (connected to loop's item output).\n\n Returns:\n The vertex ID of the first vertex in the loop body, or None if not found\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return None\n\n return get_loop_body_start_vertex(vertex=self._vertex)\n\n def _extract_loop_output(self, results: list) -> Data:\n \"\"\"Extract the output from subgraph execution results.\n\n Args:\n results: List of VertexBuildResult objects from subgraph execution\n\n Returns:\n Data object containing the loop iteration output\n \"\"\"\n # Get the vertex ID that feeds back to the item input (end of loop body)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n return extract_loop_output(results=results, end_vertex_id=end_vertex_id)\n\n async def execute_loop_body(self, data_list: list[Data], event_manager=None) -> list[Data]:\n \"\"\"Execute loop body for each data item.\n\n Creates an isolated subgraph for the loop body and executes it\n for each item in the data list, collecting results.\n\n Args:\n data_list: List of Data objects to iterate over\n event_manager: Optional event manager to pass to subgraph execution for UI events\n\n Returns:\n List of Data objects containing results from each iteration\n \"\"\"\n # Get the loop body configuration once\n loop_body_vertex_ids = self.get_loop_body_vertices()\n start_vertex_id = self._get_loop_body_start_vertex()\n start_edge = get_loop_body_start_edge(self._vertex)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n\n return await execute_loop_body(\n graph=self.graph,\n data_list=data_list,\n loop_body_vertex_ids=loop_body_vertex_ids,\n start_vertex_id=start_vertex_id,\n start_edge=start_edge,\n end_vertex_id=end_vertex_id,\n event_manager=event_manager,\n )\n\n def item_output(self) -> Data:\n \"\"\"Output is no longer used - loop executes internally now.\n\n This method is kept for backward compatibility but does nothing.\n The actual loop execution happens in done_output().\n \"\"\"\n self.stop(\"item\")\n return Data(text=\"\")\n\n async def done_output(self) -> DataFrame:\n \"\"\"Execute the loop body for all items and return aggregated results.\n\n This is now the main execution point for the loop. It:\n 1. Gets the data list to iterate over\n 2. Executes the loop body as an isolated subgraph for each item\n 3. Returns the aggregated results\n\n Args:\n event_manager: Optional event manager for UI event emission\n \"\"\"\n self.initialize_data()\n\n # Get data list\n data_list = self.ctx.get(f\"{self._id}_data\", [])\n\n if not data_list:\n return DataFrame([])\n\n # Execute loop body for all items\n try:\n aggregated_results = await self.execute_loop_body(data_list, event_manager=self._event_manager)\n return DataFrame(aggregated_results)\n except Exception as e:\n # Log error and return empty DataFrame\n from lfx.log.logger import logger\n\n await logger.aerror(f\"Error executing loop body: {e}\")\n raise\n" }, "data": { "_input_type": "HandleInput", @@ -1532,7 +1542,8 @@ "dynamic": false, "info": "The initial DataFrame to iterate over.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -1591,7 +1602,7 @@ "icon": "braces", "legacy": false, "metadata": { - "code_hash": "3cda25c3f7b5", + "code_hash": "cda7b997a730", "dependencies": { "dependencies": [ { @@ -1640,17 +1651,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"JSON or Table\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"JSON\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json index a378eba5c4f4..062abdd0db43 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json @@ -46,7 +46,9 @@ "id": "ChatOutput-S7Bzs", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" @@ -57,7 +59,7 @@ "source": "LanguageModelComponent-zY7m0", "sourceHandle": "{\"dataType\": \"LanguageModelComponent\", \"id\": \"LanguageModelComponent-zY7m0\", \"name\": \"text_output\", \"output_types\": [\"Message\"]}", "target": "ChatOutput-S7Bzs", - "targetHandle": "{\"fieldName\": \"input_value\", \"id\": \"ChatOutput-S7Bzs\", \"inputTypes\": [\"Data\", \"DataFrame\", \"Message\"], \"type\": \"str\"}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-S7Bzsœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "data": { @@ -621,7 +623,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -695,7 +697,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -750,7 +752,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json b/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json index 1b2e9850b6a1..1bbc69283b18 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json @@ -70,17 +70,19 @@ "id": "ChatOutput-Bdpjz", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "xy-edge__Agent-bNGtH{œdataTypeœ:œAgentœ,œidœ:œAgent-bNGtHœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Bdpjz{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Bdpjzœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "xy-edge__Agent-bNGtH{œdataTypeœ:œAgentœ,œidœ:œAgent-bNGtHœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Bdpjz{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Bdpjzœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "source": "Agent-bNGtH", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-bNGtHœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-Bdpjz", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Bdpjzœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Bdpjzœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -398,7 +400,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -473,7 +475,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -529,7 +531,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -693,7 +697,8 @@ "id": "CalculatorComponent-nctUz", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "category": "tools", @@ -712,7 +717,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "acbe2603b034", + "code_hash": "37caa1aba62c", "dependencies": { "dependencies": [ { @@ -765,7 +770,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" + "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" }, "expression": { "_input_type": "MessageTextInput", @@ -1258,7 +1263,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json index 81aaa2e69d56..392b5b531dad 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json @@ -16,17 +16,19 @@ "id": "ChatOutput-Pygov", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "xy-edge__Agent-9JGgQ{œdataTypeœ:œAgentœ,œidœ:œAgent-9JGgQœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Pygov{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Pygovœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "xy-edge__Agent-9JGgQ{œdataTypeœ:œAgentœ,œidœ:œAgent-9JGgQœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Pygov{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Pygovœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "source": "Agent-9JGgQ", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-9JGgQœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-Pygov", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Pygovœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Pygovœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "data": { @@ -85,7 +87,8 @@ "id": "ScrapeGraphSearchApi-ww6QN", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "conditional_paths": [], @@ -103,7 +106,7 @@ "legacy": false, "lf_version": "1.1.5", "metadata": { - "code_hash": "002d2af653ef", + "code_hash": "4caa0e09ea85", "dependencies": { "dependencies": [ { @@ -176,7 +179,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n MessageTextInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass ScrapeGraphSearchApi(Component):\n display_name: str = \"ScrapeGraph Search API\"\n description: str = \"Given a search prompt, it will return search results using ScrapeGraph's search functionality.\"\n name = \"ScrapeGraphSearchApi\"\n\n documentation: str = \"https://docs.scrapegraphai.com/services/searchscraper\"\n icon = \"ScrapeGraph\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"ScrapeGraph API Key\",\n required=True,\n password=True,\n info=\"The API key to use ScrapeGraph API.\",\n ),\n MessageTextInput(\n name=\"user_prompt\",\n display_name=\"Search Prompt\",\n tool_mode=True,\n info=\"The search prompt to use.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"search\"),\n ]\n\n def search(self) -> list[Data]:\n try:\n from scrapegraph_py import Client\n from scrapegraph_py.logger import sgai_logger\n except ImportError as e:\n msg = \"Could not import scrapegraph-py package. Please install it with `pip install scrapegraph-py`.\"\n raise ImportError(msg) from e\n\n # Set logging level\n sgai_logger.set_logging(level=\"INFO\")\n\n # Initialize the client with API key\n sgai_client = Client(api_key=self.api_key)\n\n try:\n # SearchScraper request\n response = sgai_client.searchscraper(\n user_prompt=self.user_prompt,\n )\n\n # Close the client\n sgai_client.close()\n\n return Data(data=response)\n except Exception:\n sgai_client.close()\n raise\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n MessageTextInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass ScrapeGraphSearchApi(Component):\n display_name: str = \"ScrapeGraph Search API\"\n description: str = \"Given a search prompt, it will return search results using ScrapeGraph's search functionality.\"\n name = \"ScrapeGraphSearchApi\"\n\n documentation: str = \"https://docs.scrapegraphai.com/services/searchscraper\"\n icon = \"ScrapeGraph\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"ScrapeGraph API Key\",\n required=True,\n password=True,\n info=\"The API key to use ScrapeGraph API.\",\n ),\n MessageTextInput(\n name=\"user_prompt\",\n display_name=\"Search Prompt\",\n tool_mode=True,\n info=\"The search prompt to use.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"search\"),\n ]\n\n def search(self) -> list[Data]:\n try:\n from scrapegraph_py import Client\n from scrapegraph_py.logger import sgai_logger\n except ImportError as e:\n msg = \"Could not import scrapegraph-py package. Please install it with `pip install scrapegraph-py`.\"\n raise ImportError(msg) from e\n\n # Set logging level\n sgai_logger.set_logging(level=\"INFO\")\n\n # Initialize the client with API key\n sgai_client = Client(api_key=self.api_key)\n\n try:\n # SearchScraper request\n response = sgai_client.searchscraper(\n user_prompt=self.user_prompt,\n )\n\n # Close the client\n sgai_client.close()\n\n return Data(data=response)\n except Exception:\n sgai_client.close()\n raise\n" }, "tools_metadata": { "_input_type": "ToolsInput", @@ -563,7 +566,7 @@ "legacy": false, "lf_version": "1.1.5", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -638,7 +641,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -694,7 +697,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1305,7 +1310,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json index 41e1054913fb..77e1b7da8d70 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json @@ -300,18 +300,20 @@ "id": "ChatOutput-gbqPo", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-Agent-X1iAT{œdataTypeœ:œAgentœ,œidœ:œAgent-X1iATœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-gbqPo{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-gbqPoœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-Agent-X1iAT{œdataTypeœ:œAgentœ,œidœ:œAgent-X1iATœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-gbqPo{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-gbqPoœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "Agent-X1iAT", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-X1iATœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-gbqPo", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-gbqPoœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-gbqPoœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -726,7 +728,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -1311,7 +1316,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -2753,7 +2761,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -2935,7 +2946,9 @@ "node": { "base_classes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "beta": false, @@ -2954,7 +2967,7 @@ "icon": "trending-up", "legacy": false, "metadata": { - "code_hash": "d6bf628ab821", + "code_hash": "14ca8af63c82", "dependencies": { "dependencies": [ { @@ -3018,7 +3031,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport pprint\nfrom enum import Enum\n\nimport yfinance as yf\nfrom langchain_core.tools import ToolException\nfrom pydantic import BaseModel, Field\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DropdownInput, IntInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass YahooFinanceMethod(Enum):\n GET_INFO = \"get_info\"\n GET_NEWS = \"get_news\"\n GET_ACTIONS = \"get_actions\"\n GET_ANALYSIS = \"get_analysis\"\n GET_BALANCE_SHEET = \"get_balance_sheet\"\n GET_CALENDAR = \"get_calendar\"\n GET_CASHFLOW = \"get_cashflow\"\n GET_INSTITUTIONAL_HOLDERS = \"get_institutional_holders\"\n GET_RECOMMENDATIONS = \"get_recommendations\"\n GET_SUSTAINABILITY = \"get_sustainability\"\n GET_MAJOR_HOLDERS = \"get_major_holders\"\n GET_MUTUALFUND_HOLDERS = \"get_mutualfund_holders\"\n GET_INSIDER_PURCHASES = \"get_insider_purchases\"\n GET_INSIDER_TRANSACTIONS = \"get_insider_transactions\"\n GET_INSIDER_ROSTER_HOLDERS = \"get_insider_roster_holders\"\n GET_DIVIDENDS = \"get_dividends\"\n GET_CAPITAL_GAINS = \"get_capital_gains\"\n GET_SPLITS = \"get_splits\"\n GET_SHARES = \"get_shares\"\n GET_FAST_INFO = \"get_fast_info\"\n GET_SEC_FILINGS = \"get_sec_filings\"\n GET_RECOMMENDATIONS_SUMMARY = \"get_recommendations_summary\"\n GET_UPGRADES_DOWNGRADES = \"get_upgrades_downgrades\"\n GET_EARNINGS = \"get_earnings\"\n GET_INCOME_STMT = \"get_income_stmt\"\n\n\nclass YahooFinanceSchema(BaseModel):\n symbol: str = Field(..., description=\"The stock symbol to retrieve data for.\")\n method: YahooFinanceMethod = Field(YahooFinanceMethod.GET_INFO, description=\"The type of data to retrieve.\")\n num_news: int | None = Field(5, description=\"The number of news articles to retrieve.\")\n\n\nclass YfinanceComponent(Component):\n display_name = \"Yahoo! Finance\"\n description = \"\"\"Uses [yfinance](https://pypi.org/project/yfinance/) (unofficial package) \\\nto access financial data and market information from Yahoo! Finance.\"\"\"\n icon = \"trending-up\"\n\n inputs = [\n MessageTextInput(\n name=\"symbol\",\n display_name=\"Stock Symbol\",\n info=\"The stock symbol to retrieve data for (e.g., AAPL, GOOG).\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Data Method\",\n info=\"The type of data to retrieve.\",\n options=list(YahooFinanceMethod),\n value=\"get_news\",\n ),\n IntInput(\n name=\"num_news\",\n display_name=\"Number of News\",\n info=\"The number of news articles to retrieve (only applicable for get_news).\",\n value=5,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def _fetch_yfinance_data(self, ticker: yf.Ticker, method: YahooFinanceMethod, num_news: int | None) -> str:\n try:\n if method == YahooFinanceMethod.GET_INFO:\n result = ticker.info\n elif method == YahooFinanceMethod.GET_NEWS:\n result = ticker.news[:num_news]\n else:\n result = getattr(ticker, method.value)()\n return pprint.pformat(result)\n except Exception as e:\n error_message = f\"Error retrieving data: {e}\"\n logger.debug(error_message)\n self.status = error_message\n raise ToolException(error_message) from e\n\n def fetch_content(self) -> list[Data]:\n try:\n return self._yahoo_finance_tool(\n self.symbol,\n YahooFinanceMethod(self.method),\n self.num_news,\n )\n except ToolException:\n raise\n except Exception as e:\n error_message = f\"Unexpected error: {e}\"\n logger.debug(error_message)\n self.status = error_message\n raise ToolException(error_message) from e\n\n def _yahoo_finance_tool(\n self,\n symbol: str,\n method: YahooFinanceMethod,\n num_news: int | None = 5,\n ) -> list[Data]:\n ticker = yf.Ticker(symbol)\n result = self._fetch_yfinance_data(ticker, method, num_news)\n\n if method == YahooFinanceMethod.GET_NEWS:\n data_list = [\n Data(text=f\"{article['title']}: {article['link']}\", data=article)\n for article in ast.literal_eval(result)\n ]\n else:\n data_list = [Data(text=result, data={\"result\": result})]\n\n return data_list\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import ast\nimport pprint\nfrom enum import Enum\n\nimport yfinance as yf\nfrom langchain_core.tools import ToolException\nfrom pydantic import BaseModel, Field\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DropdownInput, IntInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass YahooFinanceMethod(Enum):\n GET_INFO = \"get_info\"\n GET_NEWS = \"get_news\"\n GET_ACTIONS = \"get_actions\"\n GET_ANALYSIS = \"get_analysis\"\n GET_BALANCE_SHEET = \"get_balance_sheet\"\n GET_CALENDAR = \"get_calendar\"\n GET_CASHFLOW = \"get_cashflow\"\n GET_INSTITUTIONAL_HOLDERS = \"get_institutional_holders\"\n GET_RECOMMENDATIONS = \"get_recommendations\"\n GET_SUSTAINABILITY = \"get_sustainability\"\n GET_MAJOR_HOLDERS = \"get_major_holders\"\n GET_MUTUALFUND_HOLDERS = \"get_mutualfund_holders\"\n GET_INSIDER_PURCHASES = \"get_insider_purchases\"\n GET_INSIDER_TRANSACTIONS = \"get_insider_transactions\"\n GET_INSIDER_ROSTER_HOLDERS = \"get_insider_roster_holders\"\n GET_DIVIDENDS = \"get_dividends\"\n GET_CAPITAL_GAINS = \"get_capital_gains\"\n GET_SPLITS = \"get_splits\"\n GET_SHARES = \"get_shares\"\n GET_FAST_INFO = \"get_fast_info\"\n GET_SEC_FILINGS = \"get_sec_filings\"\n GET_RECOMMENDATIONS_SUMMARY = \"get_recommendations_summary\"\n GET_UPGRADES_DOWNGRADES = \"get_upgrades_downgrades\"\n GET_EARNINGS = \"get_earnings\"\n GET_INCOME_STMT = \"get_income_stmt\"\n\n\nclass YahooFinanceSchema(BaseModel):\n symbol: str = Field(..., description=\"The stock symbol to retrieve data for.\")\n method: YahooFinanceMethod = Field(YahooFinanceMethod.GET_INFO, description=\"The type of data to retrieve.\")\n num_news: int | None = Field(5, description=\"The number of news articles to retrieve.\")\n\n\nclass YfinanceComponent(Component):\n display_name = \"Yahoo! Finance\"\n description = \"\"\"Uses [yfinance](https://pypi.org/project/yfinance/) (unofficial package) \\\nto access financial data and market information from Yahoo! Finance.\"\"\"\n icon = \"trending-up\"\n\n inputs = [\n MessageTextInput(\n name=\"symbol\",\n display_name=\"Stock Symbol\",\n info=\"The stock symbol to retrieve data for (e.g., AAPL, GOOG).\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Data Method\",\n info=\"The type of data to retrieve.\",\n options=list(YahooFinanceMethod),\n value=\"get_news\",\n ),\n IntInput(\n name=\"num_news\",\n display_name=\"Number of News\",\n info=\"The number of news articles to retrieve (only applicable for get_news).\",\n value=5,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def _fetch_yfinance_data(self, ticker: yf.Ticker, method: YahooFinanceMethod, num_news: int | None) -> str:\n try:\n if method == YahooFinanceMethod.GET_INFO:\n result = ticker.info\n elif method == YahooFinanceMethod.GET_NEWS:\n result = ticker.news[:num_news]\n else:\n result = getattr(ticker, method.value)()\n return pprint.pformat(result)\n except Exception as e:\n error_message = f\"Error retrieving data: {e}\"\n logger.debug(error_message)\n self.status = error_message\n raise ToolException(error_message) from e\n\n def fetch_content(self) -> list[Data]:\n try:\n return self._yahoo_finance_tool(\n self.symbol,\n YahooFinanceMethod(self.method),\n self.num_news,\n )\n except ToolException:\n raise\n except Exception as e:\n error_message = f\"Unexpected error: {e}\"\n logger.debug(error_message)\n self.status = error_message\n raise ToolException(error_message) from e\n\n def _yahoo_finance_tool(\n self,\n symbol: str,\n method: YahooFinanceMethod,\n num_news: int | None = 5,\n ) -> list[Data]:\n ticker = yf.Ticker(symbol)\n result = self._fetch_yfinance_data(ticker, method, num_news)\n\n if method == YahooFinanceMethod.GET_NEWS:\n data_list = [\n Data(text=f\"{article['title']}: {article['link']}\", data=article)\n for article in ast.literal_eval(result)\n ]\n else:\n data_list = [Data(text=result, data={\"result\": result})]\n\n return data_list\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "method": { "_input_type": "DropdownInput", @@ -3171,7 +3184,8 @@ "id": "CalculatorComponent-0P2yI", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "category": "tools", @@ -3189,7 +3203,7 @@ "key": "CalculatorComponent", "legacy": false, "metadata": { - "code_hash": "acbe2603b034", + "code_hash": "37caa1aba62c", "dependencies": { "dependencies": [ { @@ -3242,7 +3256,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" + "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" }, "expression": { "_input_type": "MessageTextInput", @@ -3332,6 +3346,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -3360,7 +3375,7 @@ "icon": "TavilyIcon", "legacy": false, "metadata": { - "code_hash": "e602eaec8316", + "code_hash": "5638a305a99c", "dependencies": { "dependencies": [ { @@ -3451,7 +3466,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "days": { "_input_type": "IntInput", @@ -3771,7 +3786,7 @@ "key": "ChatOutput", "legacy": false, "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -3847,7 +3862,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -3903,7 +3918,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json index 9b9ec18137aa..802c923c4222 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json @@ -18,18 +18,20 @@ "id": "ChatOutput-z90NZ", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-Agent-oYRYa{œdataTypeœ:œAgentœ,œidœ:œAgent-oYRYaœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-z90NZ{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-z90NZœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-Agent-oYRYa{œdataTypeœ:œAgentœ,œidœ:œAgent-oYRYaœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-z90NZ{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-z90NZœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "Agent-oYRYa", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-oYRYaœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-z90NZ", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-z90NZœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-z90NZœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -177,7 +179,8 @@ "id": "CalculatorComponent-Nbeeo", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "category": "tools", @@ -197,7 +200,7 @@ "legacy": false, "lf_version": "1.2.0", "metadata": { - "code_hash": "acbe2603b034", + "code_hash": "37caa1aba62c", "dependencies": { "dependencies": [ { @@ -251,7 +254,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" + "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" }, "expression": { "_input_type": "MessageTextInput", @@ -644,7 +647,7 @@ "key": "ChatOutput", "legacy": false, "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -720,7 +723,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -776,7 +779,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1619,7 +1624,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -1797,6 +1805,7 @@ "node": { "base_classes": [ "DataFrame", + "Table", "Message" ], "beta": false, @@ -1826,7 +1835,7 @@ "last_updated": "2026-02-12T20:48:13.882Z", "legacy": false, "metadata": { - "code_hash": "f773f55e3820", + "code_hash": "7c2b0b18854e", "dependencies": { "dependencies": [ { @@ -1932,7 +1941,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import importlib\nimport io\nimport re\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom langchain_community.document_loaders import RecursiveUrlLoader\nfrom markitdown import MarkItDown\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.data import safe_convert\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SliderInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.request_utils import get_user_agent\n\n# Constants\nDEFAULT_TIMEOUT = 30\nDEFAULT_MAX_DEPTH = 1\nDEFAULT_FORMAT = \"Text\"\n\n\nURL_REGEX = re.compile(\n r\"^(https?:\\/\\/)?\" r\"(www\\.)?\" r\"([a-zA-Z0-9.-]+)\" r\"(\\.[a-zA-Z]{2,})?\" r\"(:\\d+)?\" r\"(\\/[^\\s]*)?$\",\n re.IGNORECASE,\n)\n\nUSER_AGENT = None\n# Check if langflow is installed using importlib.util.find_spec(name))\nif importlib.util.find_spec(\"langflow\"):\n langflow_installed = True\n USER_AGENT = get_user_agent()\nelse:\n langflow_installed = False\n USER_AGENT = \"lfx\"\n\n\nclass URLComponent(Component):\n \"\"\"A component that loads and parses content from web pages recursively.\n\n This component allows fetching content from one or more URLs, with options to:\n - Control crawl depth\n - Prevent crawling outside the root domain\n - Use async loading for better performance\n - Extract either raw HTML or clean text\n - Configure request headers and timeouts\n \"\"\"\n\n display_name = \"URL\"\n description = \"Fetch content from one or more web pages, following links recursively.\"\n documentation: str = \"https://docs.langflow.org/url\"\n icon = \"layout-template\"\n name = \"URLComponent\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs to crawl recursively, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n input_types=[],\n ),\n SliderInput(\n name=\"max_depth\",\n display_name=\"Depth\",\n info=(\n \"Controls how many 'clicks' away from the initial page the crawler will go:\\n\"\n \"- depth 1: only the initial page\\n\"\n \"- depth 2: initial page + all pages linked directly from it\\n\"\n \"- depth 3: initial page + direct links + links found on those direct link pages\\n\"\n \"Note: This is about link traversal, not URL path depth.\"\n ),\n value=DEFAULT_MAX_DEPTH,\n range_spec=RangeSpec(min=1, max=5, step=1),\n required=False,\n min_label=\" \",\n max_label=\" \",\n min_label_icon=\"None\",\n max_label_icon=\"None\",\n # slider_input=True\n ),\n BoolInput(\n name=\"prevent_outside\",\n display_name=\"Prevent Outside\",\n info=(\n \"If enabled, only crawls URLs within the same domain as the root URL. \"\n \"This helps prevent the crawler from going to external websites.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"use_async\",\n display_name=\"Use Async\",\n info=(\n \"If enabled, uses asynchronous loading which can be significantly faster \"\n \"but might use more system resources.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=(\n \"Output Format. Use 'Text' to extract the text from the HTML, \"\n \"'Markdown' to parse the HTML into Markdown format, or 'HTML' \"\n \"for the raw HTML content.\"\n ),\n options=[\"Text\", \"HTML\", \"Markdown\"],\n value=DEFAULT_FORMAT,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout for the request in seconds.\",\n value=DEFAULT_TIMEOUT,\n required=False,\n advanced=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": USER_AGENT}],\n advanced=True,\n input_types=[\"DataFrame\"],\n ),\n BoolInput(\n name=\"filter_text_html\",\n display_name=\"Filter Text/HTML\",\n info=\"If enabled, filters out text/css content type from the results.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"continue_on_failure\",\n display_name=\"Continue on Failure\",\n info=\"If enabled, continues crawling even if some requests fail.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"check_response_status\",\n display_name=\"Check Response Status\",\n info=\"If enabled, checks the response status of the request.\",\n value=False,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"autoset_encoding\",\n display_name=\"Autoset Encoding\",\n info=\"If enabled, automatically sets the encoding of the request.\",\n value=True,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Extracted Pages\", name=\"page_results\", method=\"fetch_content\"),\n Output(display_name=\"Raw Content\", name=\"raw_results\", method=\"fetch_content_as_message\", tool_mode=False),\n ]\n\n @staticmethod\n def _html_extractor(x: str) -> str:\n \"\"\"Extract raw HTML content.\"\"\"\n return x\n\n @staticmethod\n def _text_extractor(x: str) -> str:\n \"\"\"Extract clean text from HTML.\"\"\"\n return BeautifulSoup(x, \"lxml\").get_text()\n\n @staticmethod\n def _markdown_extractor(x: str) -> str:\n \"\"\"Convert HTML to Markdown format.\"\"\"\n stream = io.BytesIO(x.encode(\"utf-8\"))\n result = MarkItDown(enable_plugins=False).convert_stream(stream)\n return result.markdown\n\n @staticmethod\n def validate_url(url: str) -> bool:\n \"\"\"Validates if the given string matches URL pattern.\n\n Args:\n url: The URL string to validate\n\n Returns:\n bool: True if the URL is valid, False otherwise\n \"\"\"\n return bool(URL_REGEX.match(url))\n\n def ensure_url(self, url: str) -> str:\n \"\"\"Ensures the given string is a valid URL.\n\n Args:\n url: The URL string to validate and normalize\n\n Returns:\n str: The normalized URL\n\n Raises:\n ValueError: If the URL is invalid\n \"\"\"\n url = url.strip()\n if not url.startswith((\"http://\", \"https://\")):\n url = \"https://\" + url\n\n if not self.validate_url(url):\n msg = f\"Invalid URL: {url}\"\n raise ValueError(msg)\n\n return url\n\n def _create_loader(self, url: str) -> RecursiveUrlLoader:\n \"\"\"Creates a RecursiveUrlLoader instance with the configured settings.\n\n Args:\n url: The URL to load\n\n Returns:\n RecursiveUrlLoader: Configured loader instance\n \"\"\"\n headers_dict = {header[\"key\"]: header[\"value\"] for header in self.headers if header[\"value\"] is not None}\n extractors = {\n \"HTML\": self._html_extractor,\n \"Markdown\": self._markdown_extractor,\n \"Text\": self._text_extractor,\n }\n extractor = extractors.get(self.format, self._text_extractor)\n\n return RecursiveUrlLoader(\n url=url,\n max_depth=self.max_depth,\n prevent_outside=self.prevent_outside,\n use_async=self.use_async,\n extractor=extractor,\n timeout=self.timeout,\n headers=headers_dict,\n check_response_status=self.check_response_status,\n continue_on_failure=self.continue_on_failure,\n base_url=url, # Add base_url to ensure consistent domain crawling\n autoset_encoding=self.autoset_encoding, # Enable automatic encoding detection\n exclude_dirs=[], # Allow customization of excluded directories\n link_regex=None, # Allow customization of link filtering\n )\n\n def fetch_url_contents(self) -> list[dict]:\n \"\"\"Load documents from the configured URLs.\n\n Returns:\n List[Data]: List of Data objects containing the fetched content\n\n Raises:\n ValueError: If no valid URLs are provided or if there's an error loading documents\n \"\"\"\n try:\n urls = list({self.ensure_url(url) for url in self.urls if url.strip()})\n logger.debug(f\"URLs: {urls}\")\n if not urls:\n msg = \"No valid URLs provided.\"\n raise ValueError(msg)\n\n all_docs = []\n for url in urls:\n logger.debug(f\"Loading documents from {url}\")\n\n try:\n loader = self._create_loader(url)\n docs = loader.load()\n\n if not docs:\n logger.warning(f\"No documents found for {url}\")\n continue\n\n logger.debug(f\"Found {len(docs)} documents from {url}\")\n all_docs.extend(docs)\n\n except requests.exceptions.RequestException as e:\n logger.exception(f\"Error loading documents from {url}: {e}\")\n continue\n\n if not all_docs:\n msg = \"No documents were successfully loaded from any URL\"\n raise ValueError(msg)\n\n # data = [Data(text=doc.page_content, **doc.metadata) for doc in all_docs]\n data = [\n {\n \"text\": safe_convert(doc.page_content, clean_data=True),\n \"url\": doc.metadata.get(\"source\", \"\"),\n \"title\": doc.metadata.get(\"title\", \"\"),\n \"description\": doc.metadata.get(\"description\", \"\"),\n \"content_type\": doc.metadata.get(\"content_type\", \"\"),\n \"language\": doc.metadata.get(\"language\", \"\"),\n }\n for doc in all_docs\n ]\n except Exception as e:\n error_msg = e.message if hasattr(e, \"message\") else e\n msg = f\"Error loading documents: {error_msg!s}\"\n logger.exception(msg)\n raise ValueError(msg) from e\n return data\n\n def fetch_content(self) -> DataFrame:\n \"\"\"Convert the documents to a DataFrame.\"\"\"\n return DataFrame(data=self.fetch_url_contents())\n\n def fetch_content_as_message(self) -> Message:\n \"\"\"Convert the documents to a Message.\"\"\"\n url_contents = self.fetch_url_contents()\n return Message(text=\"\\n\\n\".join([x[\"text\"] for x in url_contents]), data={\"data\": url_contents})\n" + "value": "import importlib\nimport io\nimport re\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom langchain_community.document_loaders import RecursiveUrlLoader\nfrom markitdown import MarkItDown\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.data import safe_convert\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SliderInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.request_utils import get_user_agent\n\n# Constants\nDEFAULT_TIMEOUT = 30\nDEFAULT_MAX_DEPTH = 1\nDEFAULT_FORMAT = \"Text\"\n\n\nURL_REGEX = re.compile(\n r\"^(https?:\\/\\/)?\" r\"(www\\.)?\" r\"([a-zA-Z0-9.-]+)\" r\"(\\.[a-zA-Z]{2,})?\" r\"(:\\d+)?\" r\"(\\/[^\\s]*)?$\",\n re.IGNORECASE,\n)\n\nUSER_AGENT = None\n# Check if langflow is installed using importlib.util.find_spec(name))\nif importlib.util.find_spec(\"langflow\"):\n langflow_installed = True\n USER_AGENT = get_user_agent()\nelse:\n langflow_installed = False\n USER_AGENT = \"lfx\"\n\n\nclass URLComponent(Component):\n \"\"\"A component that loads and parses content from web pages recursively.\n\n This component allows fetching content from one or more URLs, with options to:\n - Control crawl depth\n - Prevent crawling outside the root domain\n - Use async loading for better performance\n - Extract either raw HTML or clean text\n - Configure request headers and timeouts\n \"\"\"\n\n display_name = \"URL\"\n description = \"Fetch content from one or more web pages, following links recursively.\"\n documentation: str = \"https://docs.langflow.org/url\"\n icon = \"layout-template\"\n name = \"URLComponent\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs to crawl recursively, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n input_types=[],\n ),\n SliderInput(\n name=\"max_depth\",\n display_name=\"Depth\",\n info=(\n \"Controls how many 'clicks' away from the initial page the crawler will go:\\n\"\n \"- depth 1: only the initial page\\n\"\n \"- depth 2: initial page + all pages linked directly from it\\n\"\n \"- depth 3: initial page + direct links + links found on those direct link pages\\n\"\n \"Note: This is about link traversal, not URL path depth.\"\n ),\n value=DEFAULT_MAX_DEPTH,\n range_spec=RangeSpec(min=1, max=5, step=1),\n required=False,\n min_label=\" \",\n max_label=\" \",\n min_label_icon=\"None\",\n max_label_icon=\"None\",\n # slider_input=True\n ),\n BoolInput(\n name=\"prevent_outside\",\n display_name=\"Prevent Outside\",\n info=(\n \"If enabled, only crawls URLs within the same domain as the root URL. \"\n \"This helps prevent the crawler from going to external websites.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"use_async\",\n display_name=\"Use Async\",\n info=(\n \"If enabled, uses asynchronous loading which can be significantly faster \"\n \"but might use more system resources.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=(\n \"Output Format. Use 'Text' to extract the text from the HTML, \"\n \"'Markdown' to parse the HTML into Markdown format, or 'HTML' \"\n \"for the raw HTML content.\"\n ),\n options=[\"Text\", \"HTML\", \"Markdown\"],\n value=DEFAULT_FORMAT,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout for the request in seconds.\",\n value=DEFAULT_TIMEOUT,\n required=False,\n advanced=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": USER_AGENT}],\n advanced=True,\n input_types=[\"DataFrame\", \"Table\"],\n ),\n BoolInput(\n name=\"filter_text_html\",\n display_name=\"Filter Text/HTML\",\n info=\"If enabled, filters out text/css content type from the results.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"continue_on_failure\",\n display_name=\"Continue on Failure\",\n info=\"If enabled, continues crawling even if some requests fail.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"check_response_status\",\n display_name=\"Check Response Status\",\n info=\"If enabled, checks the response status of the request.\",\n value=False,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"autoset_encoding\",\n display_name=\"Autoset Encoding\",\n info=\"If enabled, automatically sets the encoding of the request.\",\n value=True,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Extracted Pages\", name=\"page_results\", method=\"fetch_content\"),\n Output(display_name=\"Raw Content\", name=\"raw_results\", method=\"fetch_content_as_message\", tool_mode=False),\n ]\n\n @staticmethod\n def _html_extractor(x: str) -> str:\n \"\"\"Extract raw HTML content.\"\"\"\n return x\n\n @staticmethod\n def _text_extractor(x: str) -> str:\n \"\"\"Extract clean text from HTML.\"\"\"\n return BeautifulSoup(x, \"lxml\").get_text()\n\n @staticmethod\n def _markdown_extractor(x: str) -> str:\n \"\"\"Convert HTML to Markdown format.\"\"\"\n stream = io.BytesIO(x.encode(\"utf-8\"))\n result = MarkItDown(enable_plugins=False).convert_stream(stream)\n return result.markdown\n\n @staticmethod\n def validate_url(url: str) -> bool:\n \"\"\"Validates if the given string matches URL pattern.\n\n Args:\n url: The URL string to validate\n\n Returns:\n bool: True if the URL is valid, False otherwise\n \"\"\"\n return bool(URL_REGEX.match(url))\n\n def ensure_url(self, url: str) -> str:\n \"\"\"Ensures the given string is a valid URL.\n\n Args:\n url: The URL string to validate and normalize\n\n Returns:\n str: The normalized URL\n\n Raises:\n ValueError: If the URL is invalid\n \"\"\"\n url = url.strip()\n if not url.startswith((\"http://\", \"https://\")):\n url = \"https://\" + url\n\n if not self.validate_url(url):\n msg = f\"Invalid URL: {url}\"\n raise ValueError(msg)\n\n return url\n\n def _create_loader(self, url: str) -> RecursiveUrlLoader:\n \"\"\"Creates a RecursiveUrlLoader instance with the configured settings.\n\n Args:\n url: The URL to load\n\n Returns:\n RecursiveUrlLoader: Configured loader instance\n \"\"\"\n headers_dict = {header[\"key\"]: header[\"value\"] for header in self.headers if header[\"value\"] is not None}\n extractors = {\n \"HTML\": self._html_extractor,\n \"Markdown\": self._markdown_extractor,\n \"Text\": self._text_extractor,\n }\n extractor = extractors.get(self.format, self._text_extractor)\n\n return RecursiveUrlLoader(\n url=url,\n max_depth=self.max_depth,\n prevent_outside=self.prevent_outside,\n use_async=self.use_async,\n extractor=extractor,\n timeout=self.timeout,\n headers=headers_dict,\n check_response_status=self.check_response_status,\n continue_on_failure=self.continue_on_failure,\n base_url=url, # Add base_url to ensure consistent domain crawling\n autoset_encoding=self.autoset_encoding, # Enable automatic encoding detection\n exclude_dirs=[], # Allow customization of excluded directories\n link_regex=None, # Allow customization of link filtering\n )\n\n def fetch_url_contents(self) -> list[dict]:\n \"\"\"Load documents from the configured URLs.\n\n Returns:\n List[Data]: List of Data objects containing the fetched content\n\n Raises:\n ValueError: If no valid URLs are provided or if there's an error loading documents\n \"\"\"\n try:\n urls = list({self.ensure_url(url) for url in self.urls if url.strip()})\n logger.debug(f\"URLs: {urls}\")\n if not urls:\n msg = \"No valid URLs provided.\"\n raise ValueError(msg)\n\n all_docs = []\n for url in urls:\n logger.debug(f\"Loading documents from {url}\")\n\n try:\n loader = self._create_loader(url)\n docs = loader.load()\n\n if not docs:\n logger.warning(f\"No documents found for {url}\")\n continue\n\n logger.debug(f\"Found {len(docs)} documents from {url}\")\n all_docs.extend(docs)\n\n except requests.exceptions.RequestException as e:\n logger.exception(f\"Error loading documents from {url}: {e}\")\n continue\n\n if not all_docs:\n msg = \"No documents were successfully loaded from any URL\"\n raise ValueError(msg)\n\n # data = [Data(text=doc.page_content, **doc.metadata) for doc in all_docs]\n data = [\n {\n \"text\": safe_convert(doc.page_content, clean_data=True),\n \"url\": doc.metadata.get(\"source\", \"\"),\n \"title\": doc.metadata.get(\"title\", \"\"),\n \"description\": doc.metadata.get(\"description\", \"\"),\n \"content_type\": doc.metadata.get(\"content_type\", \"\"),\n \"language\": doc.metadata.get(\"language\", \"\"),\n }\n for doc in all_docs\n ]\n except Exception as e:\n error_msg = e.message if hasattr(e, \"message\") else e\n msg = f\"Error loading documents: {error_msg!s}\"\n logger.exception(msg)\n raise ValueError(msg) from e\n return data\n\n def fetch_content(self) -> DataFrame:\n \"\"\"Convert the documents to a DataFrame.\"\"\"\n return DataFrame(data=self.fetch_url_contents())\n\n def fetch_content_as_message(self) -> Message:\n \"\"\"Convert the documents to a Message.\"\"\"\n url_contents = self.fetch_url_contents()\n return Message(text=\"\\n\\n\".join([x[\"text\"] for x in url_contents]), data={\"data\": url_contents})\n" }, "continue_on_failure": { "_input_type": "BoolInput", @@ -2002,7 +2011,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "is_list": true, "list_add_label": "Add More", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json index a9ff1a12a2ec..fe8ba2cb9873 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json @@ -102,18 +102,20 @@ "id": "ChatOutput-Lgpwq", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-Agent-0vMrI{œdataTypeœ:œAgentœ,œidœ:œAgent-0vMrIœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Lgpwq{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Lgpwqœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-Agent-0vMrI{œdataTypeœ:œAgentœ,œidœ:œAgent-0vMrIœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-Lgpwq{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-Lgpwqœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "Agent-0vMrI", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-0vMrIœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-Lgpwq", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Lgpwqœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-Lgpwqœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -123,6 +125,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Tool" ], "beta": false, @@ -185,7 +188,7 @@ "required_inputs": null, "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -354,6 +357,7 @@ "node": { "base_classes": [ "Data", + "JSON", "Tool" ], "beta": false, @@ -416,7 +420,7 @@ "required_inputs": null, "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -967,7 +971,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -1042,7 +1046,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1098,7 +1102,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1653,7 +1659,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json index 236ba0bad1c5..6bef5d96338f 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json @@ -102,18 +102,20 @@ "id": "ChatOutput-h5fAd", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-LanguageModelComponent-ZLKtg{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-ZLKtgœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-h5fAd{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-h5fAdœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-LanguageModelComponent-ZLKtg{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-ZLKtgœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-h5fAd{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-h5fAdœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "LanguageModelComponent-ZLKtg", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-ZLKtgœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-h5fAd", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-h5fAdœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-h5fAdœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -160,18 +162,20 @@ "id": "ChatOutput-DVXkn", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-LanguageModelComponent-dZixZ{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-dZixZœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-DVXkn{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-DVXknœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-LanguageModelComponent-dZixZ{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-dZixZœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-DVXkn{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-DVXknœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "LanguageModelComponent-dZixZ", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-dZixZœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-DVXkn", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-DVXknœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-DVXknœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -817,7 +821,7 @@ "icon": "MessagesSquare", "legacy": false, "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -892,7 +896,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -948,7 +952,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1097,7 +1103,7 @@ "icon": "MessagesSquare", "legacy": false, "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -1172,7 +1178,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1228,7 +1234,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -3814,6 +3822,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json b/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json index 3064651618be..0a52789952f6 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json @@ -130,18 +130,20 @@ "id": "ChatOutput-TzFZY", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-Agent-C8zRS{œdataTypeœ:œAgentœ,œidœ:œAgent-C8zRSœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-TzFZY{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-TzFZYœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-Agent-C8zRS{œdataTypeœ:œAgentœ,œidœ:œAgent-C8zRSœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-TzFZY{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-TzFZYœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "Agent-C8zRS", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-C8zRSœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-TzFZY", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-TzFZYœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-TzFZYœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -496,7 +498,7 @@ "legacy": false, "lf_version": "1.2.0", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -570,7 +572,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -625,7 +627,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -785,7 +789,9 @@ "node": { "base_classes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "beta": false, @@ -951,7 +957,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "is_list": true, "list_add_label": "Add More", @@ -1195,7 +1202,8 @@ "id": "CalculatorComponent-FyfwW", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "category": "tools", @@ -1214,7 +1222,7 @@ "legacy": false, "lf_version": "1.2.0", "metadata": { - "code_hash": "acbe2603b034", + "code_hash": "37caa1aba62c", "dependencies": { "dependencies": [ { @@ -1267,7 +1275,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" + "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" }, "expression": { "_input_type": "MessageTextInput", @@ -1358,7 +1366,9 @@ "node": { "base_classes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "beta": false, @@ -1381,7 +1391,7 @@ "legacy": false, "lf_version": "1.2.0", "metadata": { - "code_hash": "625d1f5b3290", + "code_hash": "766aee1dff00", "dependencies": { "dependencies": [ { @@ -1454,7 +1464,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_community.utilities.searchapi import SearchApiAPIWrapper\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SearchComponent(Component):\n display_name: str = \"SearchApi\"\n description: str = \"Calls the SearchApi API with result limiting. Supports Google, Bing and DuckDuckGo.\"\n documentation: str = \"https://www.searchapi.io/docs/google\"\n icon = \"SearchAPI\"\n\n inputs = [\n DropdownInput(name=\"engine\", display_name=\"Engine\", value=\"google\", options=[\"google\", \"bing\", \"duckduckgo\"]),\n SecretStrInput(name=\"api_key\", display_name=\"SearchAPI API Key\", required=True),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n DictInput(name=\"search_params\", display_name=\"Search parameters\", advanced=True, is_list=True),\n IntInput(name=\"max_results\", display_name=\"Max Results\", value=5, advanced=True),\n IntInput(name=\"max_snippet_length\", display_name=\"Max Snippet Length\", value=100, advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def _build_wrapper(self):\n return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key)\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n\n def search_func(\n query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100\n ) -> list[Data]:\n params = params or {}\n full_results = wrapper.results(query=query, **params)\n organic_results = full_results.get(\"organic_results\", [])[:max_results]\n\n return [\n Data(\n text=result.get(\"snippet\", \"\"),\n data={\n \"title\": result.get(\"title\", \"\")[:max_snippet_length],\n \"link\": result.get(\"link\", \"\"),\n \"snippet\": result.get(\"snippet\", \"\")[:max_snippet_length],\n },\n )\n for result in organic_results\n ]\n\n results = search_func(\n self.input_value,\n self.search_params or {},\n self.max_results,\n self.max_snippet_length,\n )\n self.status = results\n return results\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "from typing import Any\n\nfrom langchain_community.utilities.searchapi import SearchApiAPIWrapper\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SearchComponent(Component):\n display_name: str = \"SearchApi\"\n description: str = \"Calls the SearchApi API with result limiting. Supports Google, Bing and DuckDuckGo.\"\n documentation: str = \"https://www.searchapi.io/docs/google\"\n icon = \"SearchAPI\"\n\n inputs = [\n DropdownInput(name=\"engine\", display_name=\"Engine\", value=\"google\", options=[\"google\", \"bing\", \"duckduckgo\"]),\n SecretStrInput(name=\"api_key\", display_name=\"SearchAPI API Key\", required=True),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n DictInput(name=\"search_params\", display_name=\"Search parameters\", advanced=True, is_list=True),\n IntInput(name=\"max_results\", display_name=\"Max Results\", value=5, advanced=True),\n IntInput(name=\"max_snippet_length\", display_name=\"Max Snippet Length\", value=100, advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def _build_wrapper(self):\n return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key)\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n\n def search_func(\n query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100\n ) -> list[Data]:\n params = params or {}\n full_results = wrapper.results(query=query, **params)\n organic_results = full_results.get(\"organic_results\", [])[:max_results]\n\n return [\n Data(\n text=result.get(\"snippet\", \"\"),\n data={\n \"title\": result.get(\"title\", \"\")[:max_snippet_length],\n \"link\": result.get(\"link\", \"\"),\n \"snippet\": result.get(\"snippet\", \"\")[:max_snippet_length],\n },\n )\n for result in organic_results\n ]\n\n results = search_func(\n self.input_value,\n self.search_params or {},\n self.max_results,\n self.max_snippet_length,\n )\n self.status = results\n return results\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" }, "engine": { "_input_type": "DropdownInput", @@ -2027,7 +2037,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -2607,7 +2620,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -3187,7 +3203,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json index 899ad4428f38..76905e095025 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json @@ -239,18 +239,20 @@ "id": "ChatOutput-GgTGu", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "xy-edge__LanguageModelComponent-URKKz{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-URKKzœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-GgTGu{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-GgTGuœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "xy-edge__LanguageModelComponent-URKKz{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-URKKzœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-GgTGu{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-GgTGuœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "LanguageModelComponent-URKKz", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-URKKzœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-GgTGu", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-GgTGuœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-GgTGuœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -699,7 +701,7 @@ "icon": "MessagesSquare", "legacy": false, "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -774,7 +776,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -830,7 +832,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json b/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json index 9c8aa8c4fc9d..49889a5a2fd9 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json @@ -76,18 +76,20 @@ "id": "SplitText-dINDR", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "other" } }, - "id": "reactflow__edge-File-CByFc{œdataTypeœ:œFileœ,œidœ:œFile-CByFcœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-SplitText-dINDR{œfieldNameœ:œdata_inputsœ,œidœ:œSplitText-dINDRœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-File-CByFc{œdataTypeœ:œFileœ,œidœ:œFile-CByFcœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-SplitText-dINDR{œfieldNameœ:œdata_inputsœ,œidœ:œSplitText-dINDRœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œotherœ}", "selected": false, "source": "File-CByFc", "sourceHandle": "{œdataTypeœ: œFileœ, œidœ: œFile-CByFcœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", "target": "SplitText-dINDR", - "targetHandle": "{œfieldNameœ: œdata_inputsœ, œidœ: œSplitText-dINDRœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œdata_inputsœ, œidœ: œSplitText-dINDRœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -134,18 +136,20 @@ "id": "ChatOutput-RZFDG", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-LanguageModelComponent-GRGRg{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-GRGRgœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-RZFDG{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-RZFDGœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-LanguageModelComponent-GRGRg{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-GRGRgœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-RZFDG{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-RZFDGœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "LanguageModelComponent-GRGRg", "sourceHandle": "{œdataTypeœ: œLanguageModelComponentœ, œidœ: œLanguageModelComponent-GRGRgœ, œnameœ: œtext_outputœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-RZFDG", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-RZFDGœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-RZFDGœ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -183,7 +187,7 @@ "id": "SplitText-dINDR", "name": "dataframe", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -196,9 +200,9 @@ "type": "other" } }, - "id": "xy-edge__SplitText-dINDR{œdataTypeœ:œSplitTextœ,œidœ:œSplitText-dINDRœ,œnameœ:œdataframeœ,œoutput_typesœ:[œDataFrameœ]}-AstraDB-mGHwS{œfieldNameœ:œingest_dataœ,œidœ:œAstraDB-mGHwSœ,œinputTypesœ:[œDataœ,œDataFrameœ],œtypeœ:œotherœ}", + "id": "xy-edge__SplitText-dINDR{œdataTypeœ:œSplitTextœ,œidœ:œSplitText-dINDRœ,œnameœ:œdataframeœ,œoutput_typesœ:[œTableœ]}-AstraDB-mGHwS{œfieldNameœ:œingest_dataœ,œidœ:œAstraDB-mGHwSœ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ],œtypeœ:œotherœ}", "source": "SplitText-dINDR", - "sourceHandle": "{œdataTypeœ: œSplitTextœ, œidœ: œSplitText-dINDRœ, œnameœ: œdataframeœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œSplitTextœ, œidœ: œSplitText-dINDRœ, œnameœ: œdataframeœ, œoutput_typesœ: [œTableœ]}", "target": "AstraDB-mGHwS", "targetHandle": "{œfieldNameœ: œingest_dataœ, œidœ: œAstraDB-mGHwSœ, œinputTypesœ: [œDataœ, œDataFrameœ], œtypeœ: œotherœ}" } @@ -655,7 +659,8 @@ "id": "SplitText-dINDR", "node": { "base_classes": [ - "Data" + "Data", + "JSON" ], "beta": false, "conditional_paths": [], @@ -678,7 +683,7 @@ "legacy": false, "lf_version": "1.1.1", "metadata": { - "code_hash": "29ae597d2d86", + "code_hash": "859adebdf672", "dependencies": { "dependencies": [ { @@ -703,10 +708,10 @@ "group_outputs": false, "method": "split_text", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -780,7 +785,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from langchain_text_splitters import CharacterTextSplitter\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.util import unescape_string\n\n\nclass SplitTextComponent(Component):\n display_name: str = \"Split Text\"\n description: str = \"Split text into chunks based on specified criteria.\"\n documentation: str = \"https://docs.langflow.org/split-text\"\n icon = \"scissors-line-dashed\"\n name = \"SplitText\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Input\",\n info=\"The data with texts to split in chunks.\",\n input_types=[\"Data\", \"DataFrame\", \"Message\"],\n required=True,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"Number of characters to overlap between chunks.\",\n value=200,\n ),\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=(\n \"The maximum length of each chunk. Text is first split by separator, \"\n \"then chunks are merged up to this size. \"\n \"Individual splits larger than this won't be further divided.\"\n ),\n value=1000,\n ),\n MessageTextInput(\n name=\"separator\",\n display_name=\"Separator\",\n info=(\n \"The character to split on. Use \\\\n for newline. \"\n \"Examples: \\\\n\\\\n for paragraphs, \\\\n for lines, . for sentences\"\n ),\n value=\"\\n\",\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"The key to use for the text column.\",\n value=\"text\",\n advanced=True,\n ),\n DropdownInput(\n name=\"keep_separator\",\n display_name=\"Keep Separator\",\n info=\"Whether to keep the separator in the output chunks and where to place it.\",\n options=[\"False\", \"True\", \"Start\", \"End\"],\n value=\"False\",\n advanced=True,\n ),\n BoolInput(\n name=\"clean_output\",\n display_name=\"Clean Output\",\n info=\"When enabled, only the text column is included in the output. Metadata columns are removed.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Chunks\", name=\"dataframe\", method=\"split_text\"),\n ]\n\n def _docs_to_data(self, docs, *, clean: bool = False) -> list[Data]:\n return [\n Data(text=doc.page_content) if clean else Data(text=doc.page_content, data=doc.metadata) for doc in docs\n ]\n\n def _fix_separator(self, separator: str) -> str:\n \"\"\"Fix common separator issues and convert to proper format.\"\"\"\n if separator == \"/n\":\n return \"\\n\"\n if separator == \"/t\":\n return \"\\t\"\n return separator\n\n def split_text_base(self):\n separator = self._fix_separator(self.separator)\n separator = unescape_string(separator)\n\n if isinstance(self.data_inputs, DataFrame):\n if not len(self.data_inputs):\n msg = \"DataFrame is empty\"\n raise TypeError(msg)\n\n self.data_inputs.text_key = self.text_key\n try:\n documents = self.data_inputs.to_lc_documents()\n except Exception as e:\n msg = f\"Error converting DataFrame to documents: {e}\"\n raise TypeError(msg) from e\n elif isinstance(self.data_inputs, Message):\n self.data_inputs = [self.data_inputs.to_data()]\n return self.split_text_base()\n else:\n if not self.data_inputs:\n msg = \"No data inputs provided\"\n raise TypeError(msg)\n\n documents = []\n if isinstance(self.data_inputs, Data):\n self.data_inputs.text_key = self.text_key\n documents = [self.data_inputs.to_lc_document()]\n else:\n try:\n documents = [input_.to_lc_document() for input_ in self.data_inputs if isinstance(input_, Data)]\n if not documents:\n msg = f\"No valid Data inputs found in {type(self.data_inputs)}\"\n raise TypeError(msg)\n except AttributeError as e:\n msg = f\"Invalid input type in collection: {e}\"\n raise TypeError(msg) from e\n try:\n # Convert string 'False'/'True' to boolean\n keep_sep = self.keep_separator\n if isinstance(keep_sep, str):\n if keep_sep.lower() == \"false\":\n keep_sep = False\n elif keep_sep.lower() == \"true\":\n keep_sep = True\n # 'start' and 'end' are kept as strings\n\n splitter = CharacterTextSplitter(\n chunk_overlap=self.chunk_overlap,\n chunk_size=self.chunk_size,\n separator=separator,\n keep_separator=keep_sep,\n )\n return splitter.split_documents(documents)\n except Exception as e:\n msg = f\"Error splitting text: {e}\"\n raise TypeError(msg) from e\n\n def split_text(self) -> DataFrame:\n docs = self.split_text_base()\n df = DataFrame(self._docs_to_data(docs, clean=self.clean_output))\n return df if self.clean_output else df.smart_column_order()\n" + "value": "from langchain_text_splitters import CharacterTextSplitter\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.util import unescape_string\n\n\nclass SplitTextComponent(Component):\n display_name: str = \"Split Text\"\n description: str = \"Split text into chunks based on specified criteria.\"\n documentation: str = \"https://docs.langflow.org/split-text\"\n icon = \"scissors-line-dashed\"\n name = \"SplitText\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Input\",\n info=\"The data with texts to split in chunks.\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\", \"Message\"],\n required=True,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"Number of characters to overlap between chunks.\",\n value=200,\n ),\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=(\n \"The maximum length of each chunk. Text is first split by separator, \"\n \"then chunks are merged up to this size. \"\n \"Individual splits larger than this won't be further divided.\"\n ),\n value=1000,\n ),\n MessageTextInput(\n name=\"separator\",\n display_name=\"Separator\",\n info=(\n \"The character to split on. Use \\\\n for newline. \"\n \"Examples: \\\\n\\\\n for paragraphs, \\\\n for lines, . for sentences\"\n ),\n value=\"\\n\",\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"The key to use for the text column.\",\n value=\"text\",\n advanced=True,\n ),\n DropdownInput(\n name=\"keep_separator\",\n display_name=\"Keep Separator\",\n info=\"Whether to keep the separator in the output chunks and where to place it.\",\n options=[\"False\", \"True\", \"Start\", \"End\"],\n value=\"False\",\n advanced=True,\n ),\n BoolInput(\n name=\"clean_output\",\n display_name=\"Clean Output\",\n info=\"When enabled, only the text column is included in the output. Metadata columns are removed.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Chunks\", name=\"dataframe\", method=\"split_text\"),\n ]\n\n def _docs_to_data(self, docs, *, clean: bool = False) -> list[Data]:\n return [\n Data(text=doc.page_content) if clean else Data(text=doc.page_content, data=doc.metadata) for doc in docs\n ]\n\n def _fix_separator(self, separator: str) -> str:\n \"\"\"Fix common separator issues and convert to proper format.\"\"\"\n if separator == \"/n\":\n return \"\\n\"\n if separator == \"/t\":\n return \"\\t\"\n return separator\n\n def split_text_base(self):\n separator = self._fix_separator(self.separator)\n separator = unescape_string(separator)\n\n if isinstance(self.data_inputs, DataFrame):\n if not len(self.data_inputs):\n msg = \"DataFrame is empty\"\n raise TypeError(msg)\n\n self.data_inputs.text_key = self.text_key\n try:\n documents = self.data_inputs.to_lc_documents()\n except Exception as e:\n msg = f\"Error converting DataFrame to documents: {e}\"\n raise TypeError(msg) from e\n elif isinstance(self.data_inputs, Message):\n self.data_inputs = [self.data_inputs.to_data()]\n return self.split_text_base()\n else:\n if not self.data_inputs:\n msg = \"No data inputs provided\"\n raise TypeError(msg)\n\n documents = []\n if isinstance(self.data_inputs, Data):\n self.data_inputs.text_key = self.text_key\n documents = [self.data_inputs.to_lc_document()]\n else:\n try:\n documents = [input_.to_lc_document() for input_ in self.data_inputs if isinstance(input_, Data)]\n if not documents:\n msg = f\"No valid Data inputs found in {type(self.data_inputs)}\"\n raise TypeError(msg)\n except AttributeError as e:\n msg = f\"Invalid input type in collection: {e}\"\n raise TypeError(msg) from e\n try:\n # Convert string 'False'/'True' to boolean\n keep_sep = self.keep_separator\n if isinstance(keep_sep, str):\n if keep_sep.lower() == \"false\":\n keep_sep = False\n elif keep_sep.lower() == \"true\":\n keep_sep = True\n # 'start' and 'end' are kept as strings\n\n splitter = CharacterTextSplitter(\n chunk_overlap=self.chunk_overlap,\n chunk_size=self.chunk_size,\n separator=separator,\n keep_separator=keep_sep,\n )\n return splitter.split_documents(documents)\n except Exception as e:\n msg = f\"Error splitting text: {e}\"\n raise TypeError(msg) from e\n\n def split_text(self) -> DataFrame:\n docs = self.split_text_base()\n df = DataFrame(self._docs_to_data(docs, clean=self.clean_output))\n return df if self.clean_output else df.smart_column_order()\n" }, "data_inputs": { "advanced": false, @@ -789,7 +794,9 @@ "info": "The data with texts to split in chunks.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -997,7 +1004,7 @@ "legacy": false, "lf_version": "1.1.1", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -1071,7 +1078,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1126,7 +1133,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1382,12 +1391,14 @@ "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -1785,6 +1796,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, @@ -2793,7 +2805,9 @@ "node": { "base_classes": [ "Data", + "JSON", "DataFrame", + "Table", "VectorStore" ], "beta": false, @@ -2866,24 +2880,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -3872,7 +3886,9 @@ "node": { "base_classes": [ "Data", + "JSON", "DataFrame", + "Table", "VectorStore" ], "beta": false, @@ -3945,24 +3961,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json index 7847d69d4db3..dab28c76485a 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json @@ -46,18 +46,20 @@ "id": "ChatOutput-JZAp9", "inputTypes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "type": "str" } }, - "id": "reactflow__edge-Agent-2FN2V{œdataTypeœ:œAgentœ,œidœ:œAgent-2FN2Vœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-JZAp9{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-JZAp9œ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-Agent-2FN2V{œdataTypeœ:œAgentœ,œidœ:œAgent-2FN2Vœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-JZAp9{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-JZAp9œ,œinputTypesœ:[œDataœ,œJSONœ,œDataFrameœ,œTableœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, "source": "Agent-2FN2V", "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-2FN2Vœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", "target": "ChatOutput-JZAp9", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-JZAp9œ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-JZAp9œ, œinputTypesœ: [œDataœ, œJSONœ, œDataFrameœ, œTableœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -180,24 +182,25 @@ "id": "YouTubeCommentsComponent-ZkD9X", "name": "comments", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { "fieldName": "df", "id": "BatchRunComponent-hJ8qJ", "inputTypes": [ - "DataFrame" + "DataFrame", + "Table" ], "type": "other" } }, - "id": "xy-edge__YouTubeCommentsComponent-ZkD9X{œdataTypeœ:œYouTubeCommentsComponentœ,œidœ:œYouTubeCommentsComponent-ZkD9Xœ,œnameœ:œcommentsœ,œoutput_typesœ:[œDataFrameœ]}-BatchRunComponent-hJ8qJ{œfieldNameœ:œdfœ,œidœ:œBatchRunComponent-hJ8qJœ,œinputTypesœ:[œDataFrameœ],œtypeœ:œotherœ}", + "id": "xy-edge__YouTubeCommentsComponent-ZkD9X{œdataTypeœ:œYouTubeCommentsComponentœ,œidœ:œYouTubeCommentsComponent-ZkD9Xœ,œnameœ:œcommentsœ,œoutput_typesœ:[œTableœ]}-BatchRunComponent-hJ8qJ{œfieldNameœ:œdfœ,œidœ:œBatchRunComponent-hJ8qJœ,œinputTypesœ:[œDataFrameœ,œTableœ],œtypeœ:œotherœ}", "selected": false, "source": "YouTubeCommentsComponent-ZkD9X", - "sourceHandle": "{œdataTypeœ: œYouTubeCommentsComponentœ, œidœ: œYouTubeCommentsComponent-ZkD9Xœ, œnameœ: œcommentsœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œYouTubeCommentsComponentœ, œidœ: œYouTubeCommentsComponent-ZkD9Xœ, œnameœ: œcommentsœ, œoutput_typesœ: [œTableœ]}", "target": "BatchRunComponent-hJ8qJ", - "targetHandle": "{œfieldNameœ: œdfœ, œidœ: œBatchRunComponent-hJ8qJœ, œinputTypesœ: [œDataFrameœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œdfœ, œidœ: œBatchRunComponent-hJ8qJœ, œinputTypesœ: [œDataFrameœ, œTableœ], œtypeœ: œotherœ}" }, { "animated": false, @@ -208,7 +211,7 @@ "id": "BatchRunComponent-hJ8qJ", "name": "batch_results", "output_types": [ - "DataFrame" + "Table" ] }, "targetHandle": { @@ -216,17 +219,19 @@ "id": "parser-ogxGV", "inputTypes": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "type": "other" } }, - "id": "reactflow__edge-BatchRunComponent-hJ8qJ{œdataTypeœ:œBatchRunComponentœ,œidœ:œBatchRunComponent-hJ8qJœ,œnameœ:œbatch_resultsœ,œoutput_typesœ:[œDataFrameœ]}-parser-ogxGV{œfieldNameœ:œinput_dataœ,œidœ:œparser-ogxGVœ,œinputTypesœ:[œDataFrameœ,œDataœ],œtypeœ:œotherœ}", + "id": "reactflow__edge-BatchRunComponent-hJ8qJ{œdataTypeœ:œBatchRunComponentœ,œidœ:œBatchRunComponent-hJ8qJœ,œnameœ:œbatch_resultsœ,œoutput_typesœ:[œTableœ]}-parser-ogxGV{œfieldNameœ:œinput_dataœ,œidœ:œparser-ogxGVœ,œinputTypesœ:[œDataFrameœ,œTableœ,œDataœ,œJSONœ],œtypeœ:œotherœ}", "selected": false, "source": "BatchRunComponent-hJ8qJ", - "sourceHandle": "{œdataTypeœ: œBatchRunComponentœ, œidœ: œBatchRunComponent-hJ8qJœ, œnameœ: œbatch_resultsœ, œoutput_typesœ: [œDataFrameœ]}", + "sourceHandle": "{œdataTypeœ: œBatchRunComponentœ, œidœ: œBatchRunComponent-hJ8qJœ, œnameœ: œbatch_resultsœ, œoutput_typesœ: [œTableœ]}", "target": "parser-ogxGV", - "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-ogxGVœ, œinputTypesœ: [œDataFrameœ, œDataœ], œtypeœ: œotherœ}" + "targetHandle": "{œfieldNameœ: œinput_dataœ, œidœ: œparser-ogxGVœ, œinputTypesœ: [œDataFrameœ, œTableœ, œDataœ, œJSONœ], œtypeœ: œotherœ}" } ], "nodes": [ @@ -235,7 +240,8 @@ "id": "YouTubeCommentsComponent-ZkD9X", "node": { "base_classes": [ - "DataFrame" + "DataFrame", + "Table" ], "beta": false, "category": "youtube", @@ -289,10 +295,10 @@ "group_outputs": false, "method": "get_video_comments", "name": "comments", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -1026,7 +1032,10 @@ "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": [], + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -1440,7 +1449,7 @@ "legacy": false, "lf_version": "1.4.3", "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -1516,7 +1525,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -1572,7 +1581,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -1701,7 +1712,9 @@ "node": { "base_classes": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "beta": false, @@ -2055,12 +2068,14 @@ "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -2446,7 +2461,8 @@ "id": "BatchRunComponent-hJ8qJ", "node": { "base_classes": [ - "DataFrame" + "DataFrame", + "Table" ], "beta": false, "conditional_paths": [], @@ -2469,7 +2485,7 @@ "last_updated": "2025-12-22T21:08:20.144Z", "legacy": false, "metadata": { - "code_hash": "8b1ec3b03475", + "code_hash": "f20d52a329ad", "dependencies": { "dependencies": [ { @@ -2499,10 +2515,10 @@ "group_outputs": false, "method": "run_batch", "name": "batch_results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -2546,7 +2562,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport toml # type: ignore[import-untyped]\n\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_model_class,\n update_model_options_in_build_config,\n)\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DataFrameInput, MessageTextInput, ModelInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\nif TYPE_CHECKING:\n from langchain_core.runnables import Runnable\n\n\nclass BatchRunComponent(Component):\n display_name = \"Batch Run\"\n description = \"Runs an LLM on each row of a DataFrame column. If no column is specified, all columns are used.\"\n documentation: str = \"https://docs.langflow.org/batch-run\"\n icon = \"List\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"Instructions\",\n info=\"Multi-line system instruction for all rows in the DataFrame.\",\n required=False,\n ),\n DataFrameInput(\n name=\"df\",\n display_name=\"DataFrame\",\n info=\"The DataFrame whose column (specified by 'column_name') we'll treat as text messages.\",\n required=True,\n ),\n MessageTextInput(\n name=\"column_name\",\n display_name=\"Column Name\",\n info=(\n \"The name of the DataFrame column to treat as text messages. \"\n \"If empty, all columns will be formatted in TOML.\"\n ),\n required=False,\n advanced=False,\n ),\n MessageTextInput(\n name=\"output_column_name\",\n display_name=\"Output Column Name\",\n info=\"Name of the column where the model's response will be stored.\",\n value=\"model_response\",\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"enable_metadata\",\n display_name=\"Enable Metadata\",\n info=\"If True, add metadata to the output DataFrame.\",\n value=False,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"LLM Results\",\n name=\"batch_results\",\n method=\"run_batch\",\n info=\"A DataFrame with all original columns plus the model's response column.\",\n ),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n def _format_row_as_toml(self, row: dict[str, Any]) -> str:\n \"\"\"Convert a dictionary (row) into a TOML-formatted string.\"\"\"\n formatted_dict = {str(col): {\"value\": str(val)} for col, val in row.items()}\n return toml.dumps(formatted_dict)\n\n def _create_base_row(\n self, original_row: dict[str, Any], model_response: str = \"\", batch_index: int = -1\n ) -> dict[str, Any]:\n \"\"\"Create a base row with original columns and additional metadata.\"\"\"\n row = original_row.copy()\n row[self.output_column_name] = model_response\n row[\"batch_index\"] = batch_index\n return row\n\n def _add_metadata(\n self, row: dict[str, Any], *, success: bool = True, system_msg: str = \"\", error: str | None = None\n ) -> None:\n \"\"\"Add metadata to a row if enabled.\"\"\"\n if not self.enable_metadata:\n return\n\n if success:\n row[\"metadata\"] = {\n \"has_system_message\": bool(system_msg),\n \"input_length\": len(row.get(\"text_input\", \"\")),\n \"response_length\": len(row[self.output_column_name]),\n \"processing_status\": \"success\",\n }\n else:\n row[\"metadata\"] = {\n \"error\": error,\n \"processing_status\": \"failed\",\n }\n\n async def run_batch(self) -> DataFrame:\n \"\"\"Process each row in df[column_name] with the language model asynchronously.\"\"\"\n # Check if model is already an instance (for testing) or needs to be instantiated\n if isinstance(self.model, list):\n # Extract model configuration\n model_selection = self.model[0]\n model_name = model_selection.get(\"name\")\n provider = model_selection.get(\"provider\")\n metadata = model_selection.get(\"metadata\", {})\n\n # Get model class and parameters from metadata\n model_class_name = metadata.get(\"model_class\")\n if not model_class_name:\n msg = f\"No model class defined for {model_name}\"\n raise ValueError(msg)\n model_class = get_model_class(model_class_name)\n\n api_key_param = metadata.get(\"api_key_param\", \"api_key\")\n model_name_param = metadata.get(\"model_name_param\", \"model\")\n\n # Get API key from global variables\n from lfx.base.models.unified_models import get_api_key_for_provider\n\n api_key = get_api_key_for_provider(self.user_id, provider, self.api_key)\n\n if not api_key and provider != \"Ollama\":\n msg = f\"{provider} API key is required. Please configure it globally.\"\n raise ValueError(msg)\n\n # Instantiate the model\n kwargs = {\n model_name_param: model_name,\n api_key_param: api_key,\n }\n model: Runnable = model_class(**kwargs)\n else:\n # Model is already an instance (typically in tests)\n model = self.model\n\n system_msg = self.system_message or \"\"\n df: DataFrame = self.df\n col_name = self.column_name or \"\"\n\n # Validate inputs first\n if not isinstance(df, DataFrame):\n msg = f\"Expected DataFrame input, got {type(df)}\"\n raise TypeError(msg)\n\n if col_name and col_name not in df.columns:\n msg = f\"Column '{col_name}' not found in the DataFrame. Available columns: {', '.join(df.columns)}\"\n raise ValueError(msg)\n\n try:\n # Determine text input for each row\n if col_name:\n user_texts = df[col_name].astype(str).tolist()\n else:\n user_texts = [\n self._format_row_as_toml(cast(\"dict[str, Any]\", row)) for row in df.to_dict(orient=\"records\")\n ]\n\n total_rows = len(user_texts)\n await logger.ainfo(f\"Processing {total_rows} rows with batch run\")\n\n # Prepare the batch of conversations\n conversations = [\n [{\"role\": \"system\", \"content\": system_msg}, {\"role\": \"user\", \"content\": text}]\n if system_msg\n else [{\"role\": \"user\", \"content\": text}]\n for text in user_texts\n ]\n\n # Configure the model with project info and callbacks\n # Some models (e.g., ChatWatsonx) may have serialization issues with with_config()\n # due to SecretStr or other non-serializable attributes\n try:\n model = model.with_config(\n {\n \"run_name\": self.display_name,\n \"project_name\": self.get_project_name(),\n \"callbacks\": self.get_langchain_callbacks(),\n }\n )\n except (TypeError, ValueError, AttributeError) as e:\n # Log warning and continue without configuration\n await logger.awarning(\n f\"Could not configure model with callbacks and project info: {e!s}. \"\n \"Proceeding with batch processing without configuration.\"\n )\n # Process batches and track progress\n responses_with_idx = list(\n zip(\n range(len(conversations)),\n await model.abatch(list(conversations)),\n strict=True,\n )\n )\n\n # Sort by index to maintain order\n responses_with_idx.sort(key=lambda x: x[0])\n\n # Build the final data with enhanced metadata\n rows: list[dict[str, Any]] = []\n for idx, (original_row, response) in enumerate(\n zip(df.to_dict(orient=\"records\"), responses_with_idx, strict=False)\n ):\n response_text = response[1].content if hasattr(response[1], \"content\") else str(response[1])\n row = self._create_base_row(\n cast(\"dict[str, Any]\", original_row), model_response=response_text, batch_index=idx\n )\n self._add_metadata(row, success=True, system_msg=system_msg)\n rows.append(row)\n\n # Log progress\n if (idx + 1) % max(1, total_rows // 10) == 0:\n await logger.ainfo(f\"Processed {idx + 1}/{total_rows} rows\")\n\n await logger.ainfo(\"Batch processing completed successfully\")\n return DataFrame(rows)\n\n except (KeyError, AttributeError) as e:\n # Handle data structure and attribute access errors\n await logger.aerror(f\"Data processing error: {e!s}\")\n error_row = self._create_base_row(dict.fromkeys(df.columns, \"\"), model_response=\"\", batch_index=-1)\n self._add_metadata(error_row, success=False, error=str(e))\n return DataFrame([error_row])\n" + "value": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport toml # type: ignore[import-untyped]\n\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_model_class,\n update_model_options_in_build_config,\n)\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DataFrameInput, MessageTextInput, ModelInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\nif TYPE_CHECKING:\n from langchain_core.runnables import Runnable\n\n\nclass BatchRunComponent(Component):\n display_name = \"Batch Run\"\n description = \"Runs an LLM on each row of a DataFrame column. If no column is specified, all columns are used.\"\n documentation: str = \"https://docs.langflow.org/batch-run\"\n icon = \"List\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"Instructions\",\n info=\"Multi-line system instruction for all rows in the DataFrame.\",\n required=False,\n ),\n DataFrameInput(\n name=\"df\",\n display_name=\"Table\",\n info=\"The DataFrame whose column (specified by 'column_name') we'll treat as text messages.\",\n required=True,\n ),\n MessageTextInput(\n name=\"column_name\",\n display_name=\"Column Name\",\n info=(\n \"The name of the DataFrame column to treat as text messages. \"\n \"If empty, all columns will be formatted in TOML.\"\n ),\n required=False,\n advanced=False,\n ),\n MessageTextInput(\n name=\"output_column_name\",\n display_name=\"Output Column Name\",\n info=\"Name of the column where the model's response will be stored.\",\n value=\"model_response\",\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"enable_metadata\",\n display_name=\"Enable Metadata\",\n info=\"If True, add metadata to the output DataFrame.\",\n value=False,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"LLM Results\",\n name=\"batch_results\",\n method=\"run_batch\",\n info=\"A DataFrame with all original columns plus the model's response column.\",\n ),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n def _format_row_as_toml(self, row: dict[str, Any]) -> str:\n \"\"\"Convert a dictionary (row) into a TOML-formatted string.\"\"\"\n formatted_dict = {str(col): {\"value\": str(val)} for col, val in row.items()}\n return toml.dumps(formatted_dict)\n\n def _create_base_row(\n self, original_row: dict[str, Any], model_response: str = \"\", batch_index: int = -1\n ) -> dict[str, Any]:\n \"\"\"Create a base row with original columns and additional metadata.\"\"\"\n row = original_row.copy()\n row[self.output_column_name] = model_response\n row[\"batch_index\"] = batch_index\n return row\n\n def _add_metadata(\n self, row: dict[str, Any], *, success: bool = True, system_msg: str = \"\", error: str | None = None\n ) -> None:\n \"\"\"Add metadata to a row if enabled.\"\"\"\n if not self.enable_metadata:\n return\n\n if success:\n row[\"metadata\"] = {\n \"has_system_message\": bool(system_msg),\n \"input_length\": len(row.get(\"text_input\", \"\")),\n \"response_length\": len(row[self.output_column_name]),\n \"processing_status\": \"success\",\n }\n else:\n row[\"metadata\"] = {\n \"error\": error,\n \"processing_status\": \"failed\",\n }\n\n async def run_batch(self) -> DataFrame:\n \"\"\"Process each row in df[column_name] with the language model asynchronously.\"\"\"\n # Check if model is already an instance (for testing) or needs to be instantiated\n if isinstance(self.model, list):\n # Extract model configuration\n model_selection = self.model[0]\n model_name = model_selection.get(\"name\")\n provider = model_selection.get(\"provider\")\n metadata = model_selection.get(\"metadata\", {})\n\n # Get model class and parameters from metadata\n model_class_name = metadata.get(\"model_class\")\n if not model_class_name:\n msg = f\"No model class defined for {model_name}\"\n raise ValueError(msg)\n model_class = get_model_class(model_class_name)\n\n api_key_param = metadata.get(\"api_key_param\", \"api_key\")\n model_name_param = metadata.get(\"model_name_param\", \"model\")\n\n # Get API key from global variables\n from lfx.base.models.unified_models import get_api_key_for_provider\n\n api_key = get_api_key_for_provider(self.user_id, provider, self.api_key)\n\n if not api_key and provider != \"Ollama\":\n msg = f\"{provider} API key is required. Please configure it globally.\"\n raise ValueError(msg)\n\n # Instantiate the model\n kwargs = {\n model_name_param: model_name,\n api_key_param: api_key,\n }\n model: Runnable = model_class(**kwargs)\n else:\n # Model is already an instance (typically in tests)\n model = self.model\n\n system_msg = self.system_message or \"\"\n df: DataFrame = self.df\n col_name = self.column_name or \"\"\n\n # Validate inputs first\n if not isinstance(df, DataFrame):\n msg = f\"Expected DataFrame input, got {type(df)}\"\n raise TypeError(msg)\n\n if col_name and col_name not in df.columns:\n msg = f\"Column '{col_name}' not found in the DataFrame. Available columns: {', '.join(df.columns)}\"\n raise ValueError(msg)\n\n try:\n # Determine text input for each row\n if col_name:\n user_texts = df[col_name].astype(str).tolist()\n else:\n user_texts = [\n self._format_row_as_toml(cast(\"dict[str, Any]\", row)) for row in df.to_dict(orient=\"records\")\n ]\n\n total_rows = len(user_texts)\n await logger.ainfo(f\"Processing {total_rows} rows with batch run\")\n\n # Prepare the batch of conversations\n conversations = [\n [{\"role\": \"system\", \"content\": system_msg}, {\"role\": \"user\", \"content\": text}]\n if system_msg\n else [{\"role\": \"user\", \"content\": text}]\n for text in user_texts\n ]\n\n # Configure the model with project info and callbacks\n # Some models (e.g., ChatWatsonx) may have serialization issues with with_config()\n # due to SecretStr or other non-serializable attributes\n try:\n model = model.with_config(\n {\n \"run_name\": self.display_name,\n \"project_name\": self.get_project_name(),\n \"callbacks\": self.get_langchain_callbacks(),\n }\n )\n except (TypeError, ValueError, AttributeError) as e:\n # Log warning and continue without configuration\n await logger.awarning(\n f\"Could not configure model with callbacks and project info: {e!s}. \"\n \"Proceeding with batch processing without configuration.\"\n )\n # Process batches and track progress\n responses_with_idx = list(\n zip(\n range(len(conversations)),\n await model.abatch(list(conversations)),\n strict=True,\n )\n )\n\n # Sort by index to maintain order\n responses_with_idx.sort(key=lambda x: x[0])\n\n # Build the final data with enhanced metadata\n rows: list[dict[str, Any]] = []\n for idx, (original_row, response) in enumerate(\n zip(df.to_dict(orient=\"records\"), responses_with_idx, strict=False)\n ):\n response_text = response[1].content if hasattr(response[1], \"content\") else str(response[1])\n row = self._create_base_row(\n cast(\"dict[str, Any]\", original_row), model_response=response_text, batch_index=idx\n )\n self._add_metadata(row, success=True, system_msg=system_msg)\n rows.append(row)\n\n # Log progress\n if (idx + 1) % max(1, total_rows // 10) == 0:\n await logger.ainfo(f\"Processed {idx + 1}/{total_rows} rows\")\n\n await logger.ainfo(\"Batch processing completed successfully\")\n return DataFrame(rows)\n\n except (KeyError, AttributeError) as e:\n # Handle data structure and attribute access errors\n await logger.aerror(f\"Data processing error: {e!s}\")\n error_row = self._create_base_row(dict.fromkeys(df.columns, \"\"), model_response=\"\", batch_index=-1)\n self._add_metadata(error_row, success=False, error=str(e))\n return DataFrame([error_row])\n" }, "column_name": { "_input_type": "MessageTextInput", @@ -2576,11 +2592,12 @@ "df": { "_input_type": "DataFrameInput", "advanced": false, - "display_name": "DataFrame", + "display_name": "Table", "dynamic": false, "info": "The DataFrame whose column (specified by 'column_name') we'll treat as text messages.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", diff --git a/src/backend/base/langflow/schema/data.py b/src/backend/base/langflow/schema/data.py index 2d232f779d5d..b5bc71a42133 100644 --- a/src/backend/base/langflow/schema/data.py +++ b/src/backend/base/langflow/schema/data.py @@ -1,8 +1,9 @@ -"""Data class for langflow - imports from lfx. +"""JSON and Data classes for langflow - imports from lfx. This maintains backward compatibility while using the lfx implementation. +JSON is the new base type; Data is an alias for backwards compatibility. """ -from lfx.schema.data import Data, custom_serializer, serialize_data +from lfx.schema.data import JSON, Data, custom_serializer, serialize_data -__all__ = ["Data", "custom_serializer", "serialize_data"] +__all__ = ["JSON", "Data", "custom_serializer", "serialize_data"] diff --git a/src/backend/tests/unit/components/flow_controls/test_listen.py b/src/backend/tests/unit/components/flow_controls/test_listen.py index 9ead70e3a9a3..ff4566c8ccc5 100644 --- a/src/backend/tests/unit/components/flow_controls/test_listen.py +++ b/src/backend/tests/unit/components/flow_controls/test_listen.py @@ -54,7 +54,7 @@ async def test_outputs_configuration(self, component_class, default_kwargs): output = component.outputs[0] assert output.name == "data" - assert output.display_name == "Data" + assert output.display_name == "JSON" assert output.method == "listen_for_data" assert output.cache is False diff --git a/src/backend/tests/unit/components/flow_controls/test_notify_component.py b/src/backend/tests/unit/components/flow_controls/test_notify_component.py index 2eb8712c10d8..5f9f819c7320 100644 --- a/src/backend/tests/unit/components/flow_controls/test_notify_component.py +++ b/src/backend/tests/unit/components/flow_controls/test_notify_component.py @@ -62,7 +62,8 @@ async def test_input_value_input_configuration(self, component_class, default_kw assert input_value_input is not None assert input_value_input.display_name == "Input Data" assert input_value_input.required is False - assert input_value_input.input_types == ["Data", "Message", "DataFrame"] + # JSON is the new name for Data, Table is the new name for DataFrame (backward compatible) + assert input_value_input.input_types == ["Data", "JSON", "Message", "DataFrame", "Table"] async def test_append_input_configuration(self, component_class, default_kwargs): """Test append input configuration.""" diff --git a/src/backend/tests/unit/components/tools/test_python_repl_tool.py b/src/backend/tests/unit/components/tools/test_python_repl_tool.py index 50c1330631d1..2243a4aca579 100644 --- a/src/backend/tests/unit/components/tools/test_python_repl_tool.py +++ b/src/backend/tests/unit/components/tools/test_python_repl_tool.py @@ -50,5 +50,5 @@ def test_component_initialization(self, component_class, default_kwargs): assert python_code["value"] == "print('Hello, World!')" assert python_code["required"] is True - # Test base configuration - assert "Data" in node_data["base_classes"] + # Test base configuration - JSON is the new name (Data is alias for backward compatibility) + assert "JSON" in node_data["base_classes"] diff --git a/src/frontend/src/utils/__tests__/typeCompatibility.test.ts b/src/frontend/src/utils/__tests__/typeCompatibility.test.ts new file mode 100644 index 000000000000..86ca11f8c230 --- /dev/null +++ b/src/frontend/src/utils/__tests__/typeCompatibility.test.ts @@ -0,0 +1,260 @@ +import { + handlesMatch, + scapedJSONStringfy, + typeIsCompatibleWith, + typesAreCompatible, +} from "../reactflowUtils"; + +// --- Named constants (no magic values) --- + +// Migration pairs: old -> new +const OLD_TYPE_DATA = "Data"; +const NEW_TYPE_JSON = "JSON"; +const OLD_TYPE_DATAFRAME = "DataFrame"; +const NEW_TYPE_TABLE = "Table"; + +// Unrelated types (not part of any migration) +const TYPE_MESSAGE = "Message"; +const TYPE_STRING = "str"; + +// Handle identity fields +const SOURCE_DATA_TYPE = "AstraDB"; +const SOURCE_ID = "AstraDB-abc123"; +const SOURCE_NAME = "dataframe"; + +const TARGET_FIELD_NAME = "input_data"; +const TARGET_ID = "Parser-abc123"; +const TARGET_TYPE = "other"; + +// --- Helper to build encoded handle strings --- + +function makeSourceHandle(overrides: Record = {}): string { + const base = { + dataType: SOURCE_DATA_TYPE, + id: SOURCE_ID, + name: SOURCE_NAME, + output_types: [OLD_TYPE_DATAFRAME], + }; + return scapedJSONStringfy({ ...base, ...overrides }); +} + +function makeTargetHandle(overrides: Record = {}): string { + const base = { + fieldName: TARGET_FIELD_NAME, + id: TARGET_ID, + inputTypes: [OLD_TYPE_DATAFRAME, OLD_TYPE_DATA], + type: TARGET_TYPE, + }; + return scapedJSONStringfy({ ...base, ...overrides }); +} + +// ============================================================ +// typeIsCompatibleWith +// ============================================================ + +describe("typeIsCompatibleWith", () => { + // --- SUCCESS cases --- + + it("should match identical types (Data -> [Data])", () => { + expect(typeIsCompatibleWith(OLD_TYPE_DATA, [OLD_TYPE_DATA])).toBe(true); + }); + + it("should match old output to new input (Data -> [JSON])", () => { + expect(typeIsCompatibleWith(OLD_TYPE_DATA, [NEW_TYPE_JSON])).toBe(true); + }); + + it("should match new output to old input (JSON -> [Data])", () => { + expect(typeIsCompatibleWith(NEW_TYPE_JSON, [OLD_TYPE_DATA])).toBe(true); + }); + + it("should match new to new (JSON -> [JSON])", () => { + expect(typeIsCompatibleWith(NEW_TYPE_JSON, [NEW_TYPE_JSON])).toBe(true); + }); + + it("should match DataFrame to Table (old -> new)", () => { + expect(typeIsCompatibleWith(OLD_TYPE_DATAFRAME, [NEW_TYPE_TABLE])).toBe( + true, + ); + }); + + it("should match Table to DataFrame (new -> old)", () => { + expect(typeIsCompatibleWith(NEW_TYPE_TABLE, [OLD_TYPE_DATAFRAME])).toBe( + true, + ); + }); + + it("should match when target has multiple types and one matches", () => { + expect( + typeIsCompatibleWith(OLD_TYPE_DATA, [TYPE_MESSAGE, NEW_TYPE_JSON]), + ).toBe(true); + }); + + it("should match non-migrated types (Message -> [Message])", () => { + expect(typeIsCompatibleWith(TYPE_MESSAGE, [TYPE_MESSAGE])).toBe(true); + }); + + // --- NEGATIVE cases --- + + it("should not match completely different types (Data -> [Message])", () => { + expect(typeIsCompatibleWith(OLD_TYPE_DATA, [TYPE_MESSAGE])).toBe(false); + }); + + it("should not match cross-family (Data -> [Table])", () => { + expect(typeIsCompatibleWith(OLD_TYPE_DATA, [NEW_TYPE_TABLE])).toBe(false); + }); + + it("should not match JSON to Table", () => { + expect(typeIsCompatibleWith(NEW_TYPE_JSON, [NEW_TYPE_TABLE])).toBe(false); + }); + + it("should not match empty target array", () => { + expect(typeIsCompatibleWith(OLD_TYPE_DATA, [])).toBe(false); + }); + + it("should not match case-sensitive ('data' vs [Data])", () => { + const LOWERCASE_DATA = "data"; + expect(typeIsCompatibleWith(LOWERCASE_DATA, [OLD_TYPE_DATA])).toBe(false); + }); +}); + +// ============================================================ +// typesAreCompatible +// ============================================================ + +describe("typesAreCompatible", () => { + // --- SUCCESS cases --- + + it("should match when any source matches any target", () => { + expect( + typesAreCompatible([TYPE_MESSAGE, OLD_TYPE_DATA], [NEW_TYPE_JSON]), + ).toBe(true); + }); + + it("should match with mixed old/new types in both lists", () => { + expect( + typesAreCompatible( + [OLD_TYPE_DATAFRAME, TYPE_STRING], + [TYPE_MESSAGE, NEW_TYPE_TABLE], + ), + ).toBe(true); + }); + + // --- NEGATIVE cases --- + + it("should not match when no types overlap", () => { + expect( + typesAreCompatible( + [OLD_TYPE_DATA, OLD_TYPE_DATAFRAME], + [TYPE_MESSAGE, TYPE_STRING], + ), + ).toBe(false); + }); + + it("should not match empty source list", () => { + expect(typesAreCompatible([], [OLD_TYPE_DATA, TYPE_MESSAGE])).toBe(false); + }); + + it("should not match when both empty", () => { + expect(typesAreCompatible([], [])).toBe(false); + }); +}); + +// ============================================================ +// handlesMatch +// ============================================================ + +describe("handlesMatch", () => { + // --- SUCCESS cases --- + + it("should match identical handles (same string)", () => { + const handle = makeSourceHandle(); + expect(handlesMatch(handle, handle)).toBe(true); + }); + + it("should match source handles where output_types differ by migration (DataFrame vs Table)", () => { + const expectedHandle = makeSourceHandle({ + output_types: [OLD_TYPE_DATAFRAME], + }); + const actualHandle = makeSourceHandle({ + output_types: [NEW_TYPE_TABLE], + }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(true); + }); + + it("should match target handles where inputTypes differ by migration (Data vs JSON)", () => { + const expectedHandle = makeTargetHandle({ + inputTypes: [OLD_TYPE_DATA], + }); + const actualHandle = makeTargetHandle({ + inputTypes: [NEW_TYPE_JSON], + }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(true); + }); + + // --- NEGATIVE cases --- + + it("should not match handles with different ids", () => { + const DIFFERENT_ID = "AstraDB-different"; + const expectedHandle = makeSourceHandle({ id: SOURCE_ID }); + const actualHandle = makeSourceHandle({ id: DIFFERENT_ID }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(false); + }); + + it("should not match handles with different names", () => { + const DIFFERENT_NAME = "other_output"; + const expectedHandle = makeSourceHandle({ name: SOURCE_NAME }); + const actualHandle = makeSourceHandle({ name: DIFFERENT_NAME }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(false); + }); + + it("should not match handles with different dataTypes", () => { + const DIFFERENT_DATA_TYPE = "OpenAI"; + const expectedHandle = makeSourceHandle({ dataType: SOURCE_DATA_TYPE }); + const actualHandle = makeSourceHandle({ dataType: DIFFERENT_DATA_TYPE }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(false); + }); + + it("should not match invalid/malformed handle strings", () => { + const MALFORMED_HANDLE = "œœœ{not-valid-jsonœœœ"; + const validHandle = makeSourceHandle(); + expect(handlesMatch(validHandle, MALFORMED_HANDLE)).toBe(false); + }); + + it("should not match handles where types differ and are not migration-related", () => { + const expectedHandle = makeSourceHandle({ + output_types: [TYPE_MESSAGE], + }); + const actualHandle = makeSourceHandle({ + output_types: [TYPE_STRING], + }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(false); + }); + + // --- EDGE CASES --- + + it("should return false when parsing fails (garbage strings)", () => { + const GARBAGE_A = "completely-garbage-string-no-json"; + const GARBAGE_B = "another-garbage!!!"; + expect(handlesMatch(GARBAGE_A, GARBAGE_B)).toBe(false); + }); + + it("should handle handles with empty type arrays", () => { + const expectedHandle = makeSourceHandle({ output_types: [] }); + const actualHandle = makeSourceHandle({ output_types: [] }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(true); + }); + + it("should not match target handles with different fieldNames", () => { + const DIFFERENT_FIELD = "other_field"; + const expectedHandle = makeTargetHandle({ fieldName: TARGET_FIELD_NAME }); + const actualHandle = makeTargetHandle({ fieldName: DIFFERENT_FIELD }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(false); + }); + + it("should not match target handles with different type property", () => { + const DIFFERENT_TYPE = "str"; + const expectedHandle = makeTargetHandle({ type: TARGET_TYPE }); + const actualHandle = makeTargetHandle({ type: DIFFERENT_TYPE }); + expect(handlesMatch(expectedHandle, actualHandle)).toBe(false); + }); +}); diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index f45f15a42c5b..3dbbd96c9c5e 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -196,8 +196,10 @@ export function cleanEdges(nodes: AllNodeType[], edges: EdgeType[]) { ); const isLoopInput = targetOutput?.allows_loop === true; + // Backward compatibility: old flows may have Data/DataFrame types that need to match JSON/Table + const expectedTargetHandle = scapedJSONStringfy(id); if ( - (scapedJSONStringfy(id) !== targetHandle || + (!handlesMatch(expectedTargetHandle, targetHandle) || (targetNode.data.node?.tool_mode && isToolMode) || isAdvanced) && !isLoopInput @@ -237,7 +239,12 @@ export function cleanEdges(nodes: AllNodeType[], edges: EdgeType[]) { // Skip edge cleanup for outputs with allows_loop=true const hasAllowsLoop = output?.allows_loop === true; - if (scapedJSONStringfy(id) !== sourceHandle && !hasAllowsLoop) { + // Backward compatibility: old flows may have Data/DataFrame types that need to match JSON/Table + const expectedSourceHandle = scapedJSONStringfy(id); + if ( + !handlesMatch(expectedSourceHandle, sourceHandle) && + !hasAllowsLoop + ) { newEdges = newEdges.filter((e) => e.id !== edge.id); brokenEdges.push(generateAlertObject(sourceNode, targetNode, edge)); } @@ -369,29 +376,42 @@ export function isValidConnection( // For loop inputs, check if source types match any of the configured target output_types // (which already includes original type + loop_types from the output configuration) + // Backward compatibility: old flows may have Data/DataFrame types that need to match JSON/Table const loopInputTypeCheck = isLoopInput && - (sourceHandleObject.output_types.some((t) => - targetHandleObject.output_types?.includes(t), + (typesAreCompatible( + sourceHandleObject.output_types, + targetHandleObject.output_types || [], ) || - targetHandleObject.output_types?.includes(sourceHandleObject.dataType)); + typeIsCompatibleWith( + sourceHandleObject.dataType, + targetHandleObject.output_types || [], + )); if ( - targetHandleObject.inputTypes?.some( - (n) => n === sourceHandleObject.dataType, + typeIsCompatibleWith( + sourceHandleObject.dataType, + targetHandleObject.inputTypes || [], ) || loopInputTypeCheck || (targetHandleObject.output_types && !loopInputTypeCheck && - (targetHandleObject.output_types?.some( - (n) => n === sourceHandleObject.dataType, + (typeIsCompatibleWith( + sourceHandleObject.dataType, + targetHandleObject.output_types || [], ) || - sourceHandleObject.output_types.some((t) => - targetHandleObject.output_types?.some((n) => n === t), + typesAreCompatible( + sourceHandleObject.output_types, + targetHandleObject.output_types || [], ))) || - sourceHandleObject.output_types.some( - (t) => - targetHandleObject.inputTypes?.some((n) => n === t) || - t === targetHandleObject.type, + typesAreCompatible( + sourceHandleObject.output_types, + targetHandleObject.inputTypes || [], + ) || + typeIsCompatibleWith(sourceHandleObject.dataType, [ + targetHandleObject.type, + ]) || + sourceHandleObject.output_types.some((t) => + typeIsCompatibleWith(t, [targetHandleObject.type]), ) ) { const targetNode = nodesArray.find((node) => node.id === target!); @@ -1069,6 +1089,243 @@ export function scapeJSONParse(json: string): any { return JSON.parse(parsed); } +/** + * Map of old types to new types for migration compatibility. + * Allows edges saved with old types (Data/DataFrame) to work with new types (JSON/Table). + */ +const TYPE_MIGRATIONS: Record = { + Data: "JSON", + DataFrame: "Table", +}; + +/** + * Check if a single type is compatible with any type in a list, considering migrations. + * @param sourceType The type from the source output + * @param targetTypes The list of acceptable types from the target input + * @returns true if sourceType matches any targetType directly or via migration + */ +export function typeIsCompatibleWith( + sourceType: string, + targetTypes: string[], +): boolean { + const migratedSource = TYPE_MIGRATIONS[sourceType] || sourceType; + return targetTypes.some((targetType) => { + const migratedTarget = TYPE_MIGRATIONS[targetType] || targetType; + return ( + sourceType === targetType || + migratedSource === targetType || + sourceType === migratedTarget || + migratedSource === migratedTarget + ); + }); +} + +/** + * Check if any type from sourceTypes is compatible with any type in targetTypes. + * @param sourceTypes The types from the source output + * @param targetTypes The list of acceptable types from the target input + * @returns true if any sourceType matches any targetType directly or via migration + */ +export function typesAreCompatible( + sourceTypes: string[], + targetTypes: string[], +): boolean { + return sourceTypes.some((sourceType) => + typeIsCompatibleWith(sourceType, targetTypes), + ); +} + +/** + * Check if two handles match, considering type migrations (Data→JSON, DataFrame→Table). + * This allows edges saved with old types to be compatible with new types. + */ +export function handlesMatch( + expectedHandle: string, + actualHandle: string, +): boolean { + if (expectedHandle === actualHandle) { + return true; + } + + try { + // Parse both handles to compare their components + const expected = scapeJSONParse(expectedHandle); + const actual = scapeJSONParse(actualHandle); + + // Check all properties except output_types/inputTypes + if (expected.id !== actual.id || expected.name !== actual.name) { + return false; + } + + // For source handles (have dataType) + if ( + expected.dataType !== undefined && + expected.dataType !== actual.dataType + ) { + return false; + } + + // For target handles (have fieldName) + if ( + expected.fieldName !== undefined && + expected.fieldName !== actual.fieldName + ) { + return false; + } + if (expected.type !== undefined && expected.type !== actual.type) { + return false; + } + + // Compare output_types with migration tolerance + const expectedTypes = expected.output_types || expected.inputTypes || []; + const actualTypes = actual.output_types || actual.inputTypes || []; + + if (expectedTypes.length !== actualTypes.length) { + // Allow mismatch if one is migrated version of other + return typesMatchWithMigration(expectedTypes, actualTypes); + } + + // Check if types match exactly or via migration + return typesMatchWithMigration(expectedTypes, actualTypes); + } catch { + // If parsing fails, fall back to direct comparison + return false; + } +} + +/** + * Check if two type arrays match, considering type migrations. + */ +function typesMatchWithMigration( + expectedTypes: string[], + actualTypes: string[], +): boolean { + if (expectedTypes.length === 0 && actualTypes.length === 0) { + return true; + } + + // Check each expected type against actual types + for (const expectedType of expectedTypes) { + const migratedExpected = TYPE_MIGRATIONS[expectedType] || expectedType; + const found = actualTypes.some((actualType) => { + const migratedActual = TYPE_MIGRATIONS[actualType] || actualType; + return ( + expectedType === actualType || + migratedExpected === actualType || + expectedType === migratedActual || + migratedExpected === migratedActual + ); + }); + if (found) { + return true; + } + } + + return false; +} + +/** + * Migrate TypeConverter nodes to sync outputs with output_type value. + * This fixes the bug where loading a template shows "Message Output" even when output_type is "JSON". + */ +export function migrateTypeConverterNodes( + nodes: AllNodeType[], + edges?: EdgeType[], +): void { + // Track which nodes were migrated and their new output types + const migratedNodes = new Map(); + + nodes.forEach((node) => { + // Only migrate TypeConverter components + if (node.data?.node?.display_name !== "Type Convert") { + return; + } + + // Type guard: ensure this is a NodeDataType (not NoteDataType) + if (!("node" in node.data)) { + return; + } + + const outputType = node.data.node?.template?.output_type?.value; + const currentOutputs = node.data.node?.outputs || []; + + // Determine correct output based on output_type + let correctOutput: OutputFieldType; + let selectedOutput: string; + let outputTypes: string[]; + + if (outputType === "JSON" || outputType === "Data") { + outputTypes = ["JSON"]; + correctOutput = { + types: outputTypes, + selected: "JSON", + name: "data_output", + display_name: "JSON Output", + method: "convert_to_data", + }; + selectedOutput = "data_output"; + } else if (outputType === "Table" || outputType === "DataFrame") { + outputTypes = ["Table"]; + correctOutput = { + types: outputTypes, + selected: "Table", + name: "dataframe_output", + display_name: "Table Output", + method: "convert_to_dataframe", + }; + selectedOutput = "dataframe_output"; + } else { + // Default to Message + outputTypes = ["Message"]; + correctOutput = { + types: outputTypes, + selected: "Message", + name: "message_output", + display_name: "Message Output", + method: "convert_to_message", + }; + selectedOutput = "message_output"; + } + + // Check if migration is needed + const needsMigration = + currentOutputs.length !== 1 || + currentOutputs[0]?.name !== correctOutput.name; + + if (needsMigration) { + // Update outputs + node.data.node.outputs = [correctOutput]; + (node.data as any).selected_output = selectedOutput; + + // Track this migration for edge updates + migratedNodes.set(node.id, outputTypes); + } + }); + + // Update edges that connect to migrated nodes + if (edges && migratedNodes.size > 0) { + edges.forEach((edge) => { + // Check if source node was migrated + const sourceOutputTypes = migratedNodes.get(edge.source); + if (sourceOutputTypes && edge.sourceHandle) { + try { + const sourceHandle = scapeJSONParse(edge.sourceHandle); + // Update output_types in the handle + sourceHandle.output_types = sourceOutputTypes; + edge.sourceHandle = scapedJSONStringfy(sourceHandle); + + // Also update in edge.data if it exists + if (edge.data?.sourceHandle) { + edge.data.sourceHandle.output_types = sourceOutputTypes; + } + } catch { + // Handle parse error silently + } + } + }); + } +} + // this function receives an array of edges and return true if any of the handles are not a json string export function checkOldEdgesHandles(edges: Edge[]): boolean { return edges.some( diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 0b8f4a23611c..0b0baf6b054e 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -122,6 +122,7 @@ export const nodeColors: { [char: string]: string } = { unknown: "#9CA3AF", Document: "#65a30d", Data: "#dc2626", + JSON: "#dc2626", Message: "#4f46e5", number: "#7E22CF", Prompt: "#7c3aed", @@ -131,6 +132,8 @@ export const nodeColors: { [char: string]: string } = { Agent: "#903BBE", AgentExecutor: "#903BBE", Tool: "#00fbfc", + DataFrame: "#ec4899", + Table: "#ec4899", }; export const nodeColorsName: { [char: string]: string } = { @@ -174,6 +177,7 @@ export const nodeColorsName: { [char: string]: string } = { unknown: "gray", Document: "lime", Data: "red", + JSON: "red", Message: "indigo", Prompt: "violet", Embeddings: "emerald", @@ -186,6 +190,7 @@ export const nodeColorsName: { [char: string]: string } = { BaseChatMessageHistory: "orange", Memory: "orange", DataFrame: "pink", + Table: "pink", }; export const FILE_ICONS = { diff --git a/src/frontend/tests/core/features/composio.spec.ts b/src/frontend/tests/core/features/composio.spec.ts index 71b3b6eba320..3d171d02d87f 100644 --- a/src/frontend/tests/core/features/composio.spec.ts +++ b/src/frontend/tests/core/features/composio.spec.ts @@ -64,7 +64,7 @@ test( }); await page - .getByTestId("output-inspection-dataframe-composiogmailapicomponent") + .getByTestId("output-inspection-table-composiogmailapicomponent") .click(); const colNumber: number = await page.getByRole("gridcell").count(); diff --git a/src/frontend/tests/core/features/filterSidebar.spec.ts b/src/frontend/tests/core/features/filterSidebar.spec.ts index 0aa9d113275d..70a43381c318 100644 --- a/src/frontend/tests/core/features/filterSidebar.spec.ts +++ b/src/frontend/tests/core/features/filterSidebar.spec.ts @@ -135,7 +135,7 @@ test( await expect(page.getByTestId("flow_controlsSub Flow")).toBeVisible(); - await expect(page.getByTestId("processingData Operations")).toBeVisible(); + await expect(page.getByTestId("processingJSON Operations")).toBeVisible(); await page.getByTestId("icon-X").first().click(); diff --git a/src/frontend/tests/core/features/stop-building.spec.ts b/src/frontend/tests/core/features/stop-building.spec.ts index 1619097756c7..b2d6a22deb72 100644 --- a/src/frontend/tests/core/features/stop-building.spec.ts +++ b/src/frontend/tests/core/features/stop-building.spec.ts @@ -92,7 +92,7 @@ test( //connection 3 await page.getByTestId("handle-splittext-shownode-chunks-right").click(); - await page.getByTestId("handle-parsedata-shownode-data-left").click(); + await page.getByTestId("handle-parsedata-shownode-json-left").click(); //connection 4 await page.getByTestId("handle-parsedata-shownode-message-right").click(); diff --git a/src/frontend/tests/core/integrations/Image Sentiment Analysis.spec.ts b/src/frontend/tests/core/integrations/Image Sentiment Analysis.spec.ts index 093b8d72bb42..effdff30aa19 100644 --- a/src/frontend/tests/core/integrations/Image Sentiment Analysis.spec.ts +++ b/src/frontend/tests/core/integrations/Image Sentiment Analysis.spec.ts @@ -36,9 +36,7 @@ withEventDeliveryModes( .getByTestId("handle-structuredoutput-shownode-structured output-right") .click(); - await page - .getByTestId("handle-parser-shownode-data or dataframe-left") - .click(); + await page.getByTestId("handle-parser-shownode-json or table-left").click(); await page.getByTestId("tab_1_stringify").click(); await page.getByRole("button", { name: "Playground", exact: true }).click(); diff --git a/src/frontend/tests/core/integrations/Market Research.spec.ts b/src/frontend/tests/core/integrations/Market Research.spec.ts index f3f30e3d2457..09b6d54fdfb6 100644 --- a/src/frontend/tests/core/integrations/Market Research.spec.ts +++ b/src/frontend/tests/core/integrations/Market Research.spec.ts @@ -52,7 +52,7 @@ withEventDeliveryModes( await unselectNodes(page); await page - .getByTestId("handle-parsercomponent-shownode-data or dataframe-left") + .getByTestId("handle-parsercomponent-shownode-json or table-left") .click(); await page.getByTestId("tab_1_stringify").click(); diff --git a/src/frontend/tests/core/integrations/decisionFlow.spec.ts b/src/frontend/tests/core/integrations/decisionFlow.spec.ts index ca95ee70fc73..4edd685c465a 100644 --- a/src/frontend/tests/core/integrations/decisionFlow.spec.ts +++ b/src/frontend/tests/core/integrations/decisionFlow.spec.ts @@ -247,19 +247,19 @@ test( await page.getByText("Check & Save").last().click(); //---------------------------------- MAKE CONNECTIONS await page - .getByTestId("handle-createlist-shownode-data list-right") + .getByTestId("handle-createlist-shownode-json list-right") .nth(0) .click(); await page - .getByTestId("handle-parsedata-shownode-data-left") + .getByTestId("handle-parsedata-shownode-json-left") .nth(0) .click(); await page - .getByTestId("handle-createlist-shownode-data list-right") + .getByTestId("handle-createlist-shownode-json list-right") .nth(1) .click(); await page - .getByTestId("handle-parsedata-shownode-data-left") + .getByTestId("handle-parsedata-shownode-json-left") .nth(1) .click(); await page diff --git a/src/frontend/tests/core/integrations/similarity.spec.ts b/src/frontend/tests/core/integrations/similarity.spec.ts index 8804aaaf26b9..65ef0fdc0cd7 100644 --- a/src/frontend/tests/core/integrations/similarity.spec.ts +++ b/src/frontend/tests/core/integrations/similarity.spec.ts @@ -218,7 +218,7 @@ test( await embeddingSimilarityOutput.hover(); await page.mouse.down(); const filterDataInput = await page - .getByTestId("handle-filterdata-shownode-data-left") + .getByTestId("handle-filterdata-shownode-json-left") .nth(0); await filterDataInput.hover(); await page.mouse.up(); @@ -230,7 +230,7 @@ test( await filterDataOutput.hover(); await page.mouse.down(); const parseDataInput = await page - .getByTestId("handle-parsedata-shownode-data-left") + .getByTestId("handle-parsedata-shownode-json-left") .nth(0); await parseDataInput.hover(); await page.mouse.up(); diff --git a/src/frontend/tests/extended/features/loop-component.spec.ts b/src/frontend/tests/extended/features/loop-component.spec.ts index 6ba9ba392d52..406682419cda 100644 --- a/src/frontend/tests/extended/features/loop-component.spec.ts +++ b/src/frontend/tests/extended/features/loop-component.spec.ts @@ -54,12 +54,12 @@ test( // Add Update Data component await page.getByTestId("sidebar-search-input").click(); await page.getByTestId("sidebar-search-input").fill("data operations"); - await page.waitForSelector('[data-testid="processingData Operations"]', { + await page.waitForSelector('[data-testid="processingJSON Operations"]', { timeout: 1000, }); await page - .getByTestId("processingData Operations") + .getByTestId("processingJSON Operations") .dragTo(page.locator('//*[@id="react-flow-id"]'), { targetPosition: { x: 500, y: 100 }, }); @@ -120,7 +120,7 @@ test( .first() .click(); await page - .getByTestId("handle-dataoperations-shownode-data-left") + .getByTestId("handle-dataoperations-shownode-json-left") .first() .click(); @@ -140,7 +140,7 @@ test( .first() .click(); await page - .getByTestId("handle-parsercomponent-shownode-data or dataframe-left") + .getByTestId("handle-parsercomponent-shownode-json or table-left") .first() .click(); @@ -173,7 +173,7 @@ test( .getByTestId("inputlist_str_urls_1") .fill("https://en.wikipedia.org/wiki/Human_intelligence"); - await page.getByTestId("title-Data Operations").click(); + await page.getByTestId("title-JSON Operations").click(); await page.waitForTimeout(1000); @@ -203,7 +203,7 @@ test( // Update Data -> Loop Item (left side) await page - .getByTestId("handle-dataoperations-shownode-data-right") + .getByTestId("handle-dataoperations-shownode-json-right") .first() .click(); await page diff --git a/src/frontend/tests/extended/integrations/duckduckgo.spec.ts b/src/frontend/tests/extended/integrations/duckduckgo.spec.ts index f99564f976c6..145f1c57f121 100644 --- a/src/frontend/tests/extended/integrations/duckduckgo.spec.ts +++ b/src/frontend/tests/extended/integrations/duckduckgo.spec.ts @@ -47,7 +47,7 @@ test( ) ?? false; await page - .getByTestId("output-inspection-dataframe-duckduckgosearchcomponent") + .getByTestId("output-inspection-table-duckduckgosearchcomponent") .first() .click(); diff --git a/src/lfx/src/lfx/_assets/component_index.json b/src/lfx/src/lfx/_assets/component_index.json index 17a89d69ceb5..61189ff9d9cc 100644 --- a/src/lfx/src/lfx/_assets/component_index.json +++ b/src/lfx/src/lfx/_assets/component_index.json @@ -5,8 +5,8 @@ { "FAISS": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -55,24 +55,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -280,7 +280,7 @@ { "AddContentToPage": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -337,14 +337,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -458,7 +458,7 @@ }, "NotionDatabaseProperties": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -506,14 +506,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -598,7 +598,7 @@ }, "NotionListPages": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -647,14 +647,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -768,7 +768,7 @@ }, "NotionPageContent": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -816,14 +816,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -908,7 +908,7 @@ }, "NotionPageCreator": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -957,14 +957,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -1078,7 +1078,7 @@ }, "NotionPageUpdate": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -1127,14 +1127,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -1248,7 +1248,7 @@ }, "NotionSearch": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -1298,14 +1298,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -1444,7 +1444,7 @@ }, "NotionUserList": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -1491,14 +1491,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -1567,7 +1567,7 @@ { "SemanticAggregator": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -1591,7 +1591,7 @@ "icon": "Agentics", "legacy": false, "metadata": { - "code_hash": "4e631c501d33", + "code_hash": "080199fa8b09", "dependencies": { "dependencies": [ { @@ -1617,14 +1617,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Output DataFrame", + "display_name": "Output Table", "group_outputs": false, "method": "aReduce", "name": "states", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -1700,7 +1700,7 @@ "show": true, "title_case": false, "type": "code", - "value": "\"\"\"SemanticAggregator component for aggregating and summarizing input data using LLM-based semantic analysis.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import ClassVar\n\nfrom pydantic import create_model\n\nfrom lfx.components.agentics.constants import (\n ERROR_AGENTICS_NOT_INSTALLED,\n ERROR_INPUT_SCHEMA_REQUIRED,\n TRANSDUCTION_AREDUCE,\n)\nfrom lfx.components.agentics.helpers import (\n build_schema_fields,\n prepare_llm_from_component,\n)\nfrom lfx.components.agentics.inputs import (\n get_generated_fields_input,\n get_model_provider_inputs,\n)\nfrom lfx.components.agentics.inputs.base_component import BaseAgenticComponent\nfrom lfx.io import (\n BoolInput,\n DataFrameInput,\n MessageTextInput,\n Output,\n)\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SemanticAggregator(BaseAgenticComponent):\n \"\"\"Aggregate or summarize entire input data using natural language instructions and a defined output schema.\n\n This component processes all rows of input data collectively to produce aggregated results,\n such as summaries, statistics, or consolidated information based on LLM analysis.\n \"\"\"\n\n code_class_base_inheritance: ClassVar[str] = \"Component\"\n display_name = \"aReduce\"\n description = (\n \"Analyze the entire input dataframe at once and generate a new dataframe \"\n \"following the instruction and the required schema\"\n )\n documentation: str = \"https://docs.langflow.org/bundles-agentics\"\n icon = \"Agentics\"\n\n inputs = [\n *get_model_provider_inputs(),\n DataFrameInput(\n name=\"source\",\n display_name=\"Input DataFrame\",\n info=\"Input DataFrame to aggregate. The schema is automatically inferred from column names and types.\",\n required=True,\n ),\n get_generated_fields_input(),\n BoolInput(\n name=\"return_multiple_instances\",\n display_name=\"As List\",\n info=\"If True, generate a list of instances of the provided schema.\",\n advanced=False,\n value=False,\n ),\n MessageTextInput(\n name=\"instructions\",\n display_name=\"Instructions\",\n info=\"Natural language instructions describing how to aggregate the input data into the output schema.\",\n advanced=False,\n value=\"\",\n required=False,\n ),\n ]\n\n outputs = [\n Output(\n name=\"states\",\n method=\"aReduce\",\n display_name=\"Output DataFrame\",\n info=\"Aggregated DataFrame generated by the LLM following the specified output schema.\",\n tool_mode=True,\n ),\n ]\n\n async def aReduce(self) -> DataFrame: # noqa: N802\n \"\"\"Aggregate input data using LLM-based semantic analysis.\n\n Returns:\n DataFrame containing the aggregated results following the output schema.\n \"\"\"\n try:\n from agentics import AG\n from agentics.core.atype import create_pydantic_model\n except ImportError as e:\n raise ImportError(ERROR_AGENTICS_NOT_INSTALLED) from e\n\n llm = prepare_llm_from_component(self)\n\n if self.source and self.schema != []:\n source = AG.from_dataframe(DataFrame(self.source))\n\n schema_fields = build_schema_fields(self.schema)\n atype = create_pydantic_model(schema_fields, name=\"Target\")\n if self.return_multiple_instances:\n final_atype = create_model(\"ListOfTarget\", items=(list[atype], ...))\n else:\n final_atype = atype\n\n target = AG(\n atype=final_atype,\n transduction_type=TRANSDUCTION_AREDUCE,\n instructions=self.instructions\n if not self.return_multiple_instances\n else \"\\nGenerate a list of instances of the target type following those instructions : .\"\n + self.instructions,\n llm=llm,\n )\n\n output = await (target << source)\n if self.return_multiple_instances:\n output = AG(atype=atype, states=output[0].items)\n\n return DataFrame(output.to_dataframe().to_dict(orient=\"records\"))\n raise ValueError(ERROR_INPUT_SCHEMA_REQUIRED)\n" + "value": "\"\"\"SemanticAggregator component for aggregating and summarizing input data using LLM-based semantic analysis.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import ClassVar\n\nfrom pydantic import create_model\n\nfrom lfx.components.agentics.constants import (\n ERROR_AGENTICS_NOT_INSTALLED,\n ERROR_INPUT_SCHEMA_REQUIRED,\n TRANSDUCTION_AREDUCE,\n)\nfrom lfx.components.agentics.helpers import (\n build_schema_fields,\n prepare_llm_from_component,\n)\nfrom lfx.components.agentics.inputs import (\n get_generated_fields_input,\n get_model_provider_inputs,\n)\nfrom lfx.components.agentics.inputs.base_component import BaseAgenticComponent\nfrom lfx.io import (\n BoolInput,\n DataFrameInput,\n MessageTextInput,\n Output,\n)\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SemanticAggregator(BaseAgenticComponent):\n \"\"\"Aggregate or summarize entire input data using natural language instructions and a defined output schema.\n\n This component processes all rows of input data collectively to produce aggregated results,\n such as summaries, statistics, or consolidated information based on LLM analysis.\n \"\"\"\n\n code_class_base_inheritance: ClassVar[str] = \"Component\"\n display_name = \"aReduce\"\n description = (\n \"Analyze the entire input dataframe at once and generate a new dataframe \"\n \"following the instruction and the required schema\"\n )\n documentation: str = \"https://docs.langflow.org/bundles-agentics\"\n icon = \"Agentics\"\n\n inputs = [\n *get_model_provider_inputs(),\n DataFrameInput(\n name=\"source\",\n display_name=\"Input Table\",\n info=\"Input DataFrame to aggregate. The schema is automatically inferred from column names and types.\",\n required=True,\n ),\n get_generated_fields_input(),\n BoolInput(\n name=\"return_multiple_instances\",\n display_name=\"As List\",\n info=\"If True, generate a list of instances of the provided schema.\",\n advanced=False,\n value=False,\n ),\n MessageTextInput(\n name=\"instructions\",\n display_name=\"Instructions\",\n info=\"Natural language instructions describing how to aggregate the input data into the output schema.\",\n advanced=False,\n value=\"\",\n required=False,\n ),\n ]\n\n outputs = [\n Output(\n name=\"states\",\n method=\"aReduce\",\n display_name=\"Output Table\",\n info=\"Aggregated DataFrame generated by the LLM following the specified output schema.\",\n tool_mode=True,\n ),\n ]\n\n async def aReduce(self) -> DataFrame: # noqa: N802\n \"\"\"Aggregate input data using LLM-based semantic analysis.\n\n Returns:\n DataFrame containing the aggregated results following the output schema.\n \"\"\"\n try:\n from agentics import AG\n from agentics.core.atype import create_pydantic_model\n except ImportError as e:\n raise ImportError(ERROR_AGENTICS_NOT_INSTALLED) from e\n\n llm = prepare_llm_from_component(self)\n\n if self.source and self.schema != []:\n source = AG.from_dataframe(DataFrame(self.source))\n\n schema_fields = build_schema_fields(self.schema)\n atype = create_pydantic_model(schema_fields, name=\"Target\")\n if self.return_multiple_instances:\n final_atype = create_model(\"ListOfTarget\", items=(list[atype], ...))\n else:\n final_atype = atype\n\n target = AG(\n atype=final_atype,\n transduction_type=TRANSDUCTION_AREDUCE,\n instructions=self.instructions\n if not self.return_multiple_instances\n else \"\\nGenerate a list of instances of the target type following those instructions : .\"\n + self.instructions,\n llm=llm,\n )\n\n output = await (target << source)\n if self.return_multiple_instances:\n output = AG(atype=atype, states=output[0].items)\n\n return DataFrame(output.to_dataframe().to_dict(orient=\"records\"))\n raise ValueError(ERROR_INPUT_SCHEMA_REQUIRED)\n" }, "instructions": { "_input_type": "MessageTextInput", @@ -1865,6 +1865,10 @@ "display_name": "Schema", "dynamic": false, "info": "Define the structure of data to generate. Specify column names, descriptions, and types.", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "schema", @@ -1926,11 +1930,12 @@ "source": { "_input_type": "DataFrameInput", "advanced": false, - "display_name": "Input DataFrame", + "display_name": "Input Table", "dynamic": false, "info": "Input DataFrame to aggregate. The schema is automatically inferred from column names and types.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -1952,7 +1957,7 @@ }, "SemanticMap": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -1977,7 +1982,7 @@ "icon": "Agentics", "legacy": false, "metadata": { - "code_hash": "9fe34c926467", + "code_hash": "ab1e08451407", "dependencies": { "dependencies": [ { @@ -2003,14 +2008,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Output DataFrame", + "display_name": "Output Table", "group_outputs": false, "method": "aMap", "name": "states", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -2106,7 +2111,7 @@ "show": true, "title_case": false, "type": "code", - "value": "\"\"\"SemanticMap component for transforming each row of input data using LLM-based semantic processing.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import ClassVar\n\nfrom pydantic import create_model\n\nfrom lfx.components.agentics.constants import (\n ERROR_AGENTICS_NOT_INSTALLED,\n ERROR_INPUT_SCHEMA_REQUIRED,\n TRANSDUCTION_AMAP,\n)\nfrom lfx.components.agentics.helpers import (\n build_schema_fields,\n prepare_llm_from_component,\n)\nfrom lfx.components.agentics.inputs import (\n get_generated_fields_input,\n get_model_provider_inputs,\n)\nfrom lfx.components.agentics.inputs.base_component import BaseAgenticComponent\nfrom lfx.io import (\n BoolInput,\n DataFrameInput,\n MessageTextInput,\n Output,\n)\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SemanticMap(BaseAgenticComponent):\n \"\"\"Transform each row of input data using natural language instructions and a defined output schema.\n\n This component processes input data row-by-row, applying LLM-based transformations to generate\n new columns or derive insights for each individual record.\n \"\"\"\n\n code_class_base_inheritance: ClassVar[str] = \"Component\"\n display_name = \"aMap\"\n description = (\n \"Augment the input dataframe adding new columns defined in the input schema. \"\n \"Rows are processed independently and in parallel using LLMs.\"\n )\n documentation: str = \"https://docs.langflow.org/bundles-agentics\"\n icon = \"Agentics\"\n\n inputs = [\n *get_model_provider_inputs(),\n DataFrameInput(\n name=\"source\",\n display_name=\"Input DataFrame\",\n info=(\"Input DataFrame to transform. The schema is automatically inferred from column names and types.\"),\n ),\n get_generated_fields_input(),\n BoolInput(\n name=\"return_multiple_instances\",\n display_name=\"As List\",\n info=(\n \"If True, generate multiple instances of the provided schema for each input row concatenating all them.\"\n ),\n advanced=False,\n value=False,\n ),\n MessageTextInput(\n name=\"instructions\",\n display_name=\"Instructions\",\n info=\"Natural language instructions describing how to transform each input row into the output schema.\",\n value=\"\",\n required=False,\n ),\n BoolInput(\n name=\"append_to_input_columns\",\n display_name=\"Keep Source Columns\",\n info=(\n \"Keep original input columns in the output. If disabled, only newly \"\n \"generated columns are returned. This is ignored if As List is set to True.\"\n ),\n value=True,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n name=\"states\",\n display_name=\"Output DataFrame\",\n info=\"Transformed DataFrame resulting from semantic mapping.\",\n method=\"aMap\",\n tool_mode=True,\n ),\n ]\n\n async def aMap(self) -> DataFrame: # noqa: N802\n \"\"\"Transform input data row-by-row using LLM-based semantic processing.\n\n Returns:\n DataFrame with transformed data following the output schema.\n \"\"\"\n try:\n from agentics import AG\n from agentics.core.atype import create_pydantic_model\n except ImportError as e:\n raise ImportError(ERROR_AGENTICS_NOT_INSTALLED) from e\n\n llm = prepare_llm_from_component(self)\n if self.source and self.schema != []:\n source = AG.from_dataframe(DataFrame(self.source))\n\n schema_fields = build_schema_fields(self.schema)\n atype = create_pydantic_model(schema_fields, name=\"Target\")\n if self.return_multiple_instances:\n final_atype = create_model(\"ListOfTarget\", items=(list[atype], ...))\n else:\n final_atype = atype\n\n target = AG(\n atype=final_atype,\n transduction_type=TRANSDUCTION_AMAP,\n llm=llm,\n )\n if \"{\" in self.instructions:\n source.prompt_template = self.instructions\n else:\n source.instructions += self.instructions\n\n output = await (target << source)\n if self.return_multiple_instances:\n appended_states = [item_state for state in output for item_state in state.items]\n output = AG(atype=atype, states=appended_states)\n\n elif self.append_to_input_columns:\n output = source.merge_states(output)\n\n return DataFrame(output.to_dataframe().to_dict(orient=\"records\"))\n raise ValueError(ERROR_INPUT_SCHEMA_REQUIRED)\n" + "value": "\"\"\"SemanticMap component for transforming each row of input data using LLM-based semantic processing.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import ClassVar\n\nfrom pydantic import create_model\n\nfrom lfx.components.agentics.constants import (\n ERROR_AGENTICS_NOT_INSTALLED,\n ERROR_INPUT_SCHEMA_REQUIRED,\n TRANSDUCTION_AMAP,\n)\nfrom lfx.components.agentics.helpers import (\n build_schema_fields,\n prepare_llm_from_component,\n)\nfrom lfx.components.agentics.inputs import (\n get_generated_fields_input,\n get_model_provider_inputs,\n)\nfrom lfx.components.agentics.inputs.base_component import BaseAgenticComponent\nfrom lfx.io import (\n BoolInput,\n DataFrameInput,\n MessageTextInput,\n Output,\n)\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SemanticMap(BaseAgenticComponent):\n \"\"\"Transform each row of input data using natural language instructions and a defined output schema.\n\n This component processes input data row-by-row, applying LLM-based transformations to generate\n new columns or derive insights for each individual record.\n \"\"\"\n\n code_class_base_inheritance: ClassVar[str] = \"Component\"\n display_name = \"aMap\"\n description = (\n \"Augment the input dataframe adding new columns defined in the input schema. \"\n \"Rows are processed independently and in parallel using LLMs.\"\n )\n documentation: str = \"https://docs.langflow.org/bundles-agentics\"\n icon = \"Agentics\"\n\n inputs = [\n *get_model_provider_inputs(),\n DataFrameInput(\n name=\"source\",\n display_name=\"Input Table\",\n info=(\"Input DataFrame to transform. The schema is automatically inferred from column names and types.\"),\n ),\n get_generated_fields_input(),\n BoolInput(\n name=\"return_multiple_instances\",\n display_name=\"As List\",\n info=(\n \"If True, generate multiple instances of the provided schema for each input row concatenating all them.\"\n ),\n advanced=False,\n value=False,\n ),\n MessageTextInput(\n name=\"instructions\",\n display_name=\"Instructions\",\n info=\"Natural language instructions describing how to transform each input row into the output schema.\",\n value=\"\",\n required=False,\n ),\n BoolInput(\n name=\"append_to_input_columns\",\n display_name=\"Keep Source Columns\",\n info=(\n \"Keep original input columns in the output. If disabled, only newly \"\n \"generated columns are returned. This is ignored if As List is set to True.\"\n ),\n value=True,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n name=\"states\",\n display_name=\"Output Table\",\n info=\"Transformed DataFrame resulting from semantic mapping.\",\n method=\"aMap\",\n tool_mode=True,\n ),\n ]\n\n async def aMap(self) -> DataFrame: # noqa: N802\n \"\"\"Transform input data row-by-row using LLM-based semantic processing.\n\n Returns:\n DataFrame with transformed data following the output schema.\n \"\"\"\n try:\n from agentics import AG\n from agentics.core.atype import create_pydantic_model\n except ImportError as e:\n raise ImportError(ERROR_AGENTICS_NOT_INSTALLED) from e\n\n llm = prepare_llm_from_component(self)\n if self.source and self.schema != []:\n source = AG.from_dataframe(DataFrame(self.source))\n\n schema_fields = build_schema_fields(self.schema)\n atype = create_pydantic_model(schema_fields, name=\"Target\")\n if self.return_multiple_instances:\n final_atype = create_model(\"ListOfTarget\", items=(list[atype], ...))\n else:\n final_atype = atype\n\n target = AG(\n atype=final_atype,\n transduction_type=TRANSDUCTION_AMAP,\n llm=llm,\n )\n if \"{\" in self.instructions:\n source.prompt_template = self.instructions\n else:\n source.instructions += self.instructions\n\n output = await (target << source)\n if self.return_multiple_instances:\n appended_states = [item_state for state in output for item_state in state.items]\n output = AG(atype=atype, states=appended_states)\n\n elif self.append_to_input_columns:\n output = source.merge_states(output)\n\n return DataFrame(output.to_dataframe().to_dict(orient=\"records\"))\n raise ValueError(ERROR_INPUT_SCHEMA_REQUIRED)\n" }, "instructions": { "_input_type": "MessageTextInput", @@ -2271,6 +2276,10 @@ "display_name": "Schema", "dynamic": false, "info": "Define the structure of data to generate. Specify column names, descriptions, and types.", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "schema", @@ -2332,11 +2341,12 @@ "source": { "_input_type": "DataFrameInput", "advanced": false, - "display_name": "Input DataFrame", + "display_name": "Input Table", "dynamic": false, "info": "Input DataFrame to transform. The schema is automatically inferred from column names and types.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -2358,7 +2368,7 @@ }, "SyntheticDataGenerator": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -2382,7 +2392,7 @@ "icon": "Agentics", "legacy": false, "metadata": { - "code_hash": "efd180878996", + "code_hash": "677579fcf15f", "dependencies": { "dependencies": [ { @@ -2404,14 +2414,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Output DataFrame", + "display_name": "Output Table", "group_outputs": false, "method": "aGenerate", "name": "states", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -2507,7 +2517,7 @@ "show": true, "title_case": false, "type": "code", - "value": "\"\"\"SyntheticDataGenerator component for creating synthetic data using LLM-based generation.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import ClassVar\n\nfrom lfx.components.agentics.constants import ERROR_AGENTICS_NOT_INSTALLED\nfrom lfx.components.agentics.helpers import (\n build_schema_fields,\n prepare_llm_from_component,\n)\nfrom lfx.components.agentics.inputs import (\n get_generated_fields_input,\n get_model_provider_inputs,\n)\nfrom lfx.components.agentics.inputs.base_component import BaseAgenticComponent\nfrom lfx.io import DataFrameInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SyntheticDataGenerator(BaseAgenticComponent):\n \"\"\"Generate synthetic data using either example data or a defined schema.\n\n This component creates realistic synthetic data by either:\n 1. Learning from an input DataFrame and generating similar rows, or\n 2. Following a user-defined schema to create data from scratch.\n\n \"\"\"\n\n code_class_base_inheritance: ClassVar[str] = \"Component\"\n display_name = \"aGenerate\"\n description = (\n \"Generate mock data for user defined schema. If a dataframe is provided, \"\n \"the component will generate similar rows.\"\n )\n documentation: str = \"https://docs.langflow.org/bundles-agentics\"\n icon = \"Agentics\"\n\n inputs = [\n *get_model_provider_inputs(),\n get_generated_fields_input(\n name=\"schema\",\n display_name=\"Schema\",\n info=(\n \"Define the structure of data to generate. Specify column names, \"\n \"descriptions, and types. Used only when input DataFrame is not provided.\"\n ),\n required=False,\n ),\n DataFrameInput(\n name=\"source\",\n display_name=\"Input DataFrame\",\n info=(\n \"Provide example DataFrame to learn from and generate similar data. \"\n \"Only the first 50 rows will be used as examples.\"\n ),\n required=False,\n advanced=False,\n value=None,\n ),\n MessageTextInput(\n name=\"instructions\",\n display_name=\"Instructions\",\n info=\"Optional natural language instructions to guide the synthetic data generation process.\",\n value=\"\",\n required=False,\n advanced=True,\n ),\n IntInput(\n name=\"batch_size\",\n display_name=\"Number of Rows to Generate\",\n value=10,\n advanced=False,\n ),\n ]\n\n outputs = [\n Output(\n name=\"states\",\n display_name=\"Output DataFrame\",\n info=\"Synthetic DataFrame generated by the LLM based on the schema or example data.\",\n method=\"aGenerate\",\n tool_mode=True,\n ),\n ]\n\n async def aGenerate(self) -> DataFrame: # noqa: N802\n \"\"\"Generate synthetic data using LLM-based generation.\n\n Returns:\n DataFrame containing the generated synthetic data.\n \"\"\"\n try:\n from agentics import AG\n from agentics.core.atype import create_pydantic_model\n from agentics.core.transducible_functions import generate_prototypical_instances\n except ImportError as e:\n raise ImportError(ERROR_AGENTICS_NOT_INSTALLED) from e\n\n llm = prepare_llm_from_component(self)\n\n if self.source:\n source = AG.from_dataframe(DataFrame(self.source))\n atype = source.atype\n instructions = str(self.instructions)\n instructions += \"\\nHere are examples to take inspiration from\" + str(source.states[:50])\n elif self.schema != []:\n schema_fields = build_schema_fields(self.schema)\n atype = create_pydantic_model(schema_fields, name=\"GeneratedData\")\n instructions = str(self.instructions)\n else:\n msg = \"Synthetic data generation requires either a sample DataFrame or schema definition (but not both).\"\n raise ValueError(msg)\n\n output_states = await generate_prototypical_instances(\n atype,\n n_instances=self.batch_size,\n llm=llm,\n instructions=instructions,\n )\n if self.source:\n output_states = source.states + output_states\n output = AG(states=output_states)\n\n return DataFrame(output.to_dataframe().to_dict(orient=\"records\"))\n" + "value": "\"\"\"SyntheticDataGenerator component for creating synthetic data using LLM-based generation.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import ClassVar\n\nfrom lfx.components.agentics.constants import ERROR_AGENTICS_NOT_INSTALLED\nfrom lfx.components.agentics.helpers import (\n build_schema_fields,\n prepare_llm_from_component,\n)\nfrom lfx.components.agentics.inputs import (\n get_generated_fields_input,\n get_model_provider_inputs,\n)\nfrom lfx.components.agentics.inputs.base_component import BaseAgenticComponent\nfrom lfx.io import DataFrameInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SyntheticDataGenerator(BaseAgenticComponent):\n \"\"\"Generate synthetic data using either example data or a defined schema.\n\n This component creates realistic synthetic data by either:\n 1. Learning from an input DataFrame and generating similar rows, or\n 2. Following a user-defined schema to create data from scratch.\n\n \"\"\"\n\n code_class_base_inheritance: ClassVar[str] = \"Component\"\n display_name = \"aGenerate\"\n description = (\n \"Generate mock data for user defined schema. If a dataframe is provided, \"\n \"the component will generate similar rows.\"\n )\n documentation: str = \"https://docs.langflow.org/bundles-agentics\"\n icon = \"Agentics\"\n\n inputs = [\n *get_model_provider_inputs(),\n get_generated_fields_input(\n name=\"schema\",\n display_name=\"Schema\",\n info=(\n \"Define the structure of data to generate. Specify column names, \"\n \"descriptions, and types. Used only when input DataFrame is not provided.\"\n ),\n required=False,\n ),\n DataFrameInput(\n name=\"source\",\n display_name=\"Input Table\",\n info=(\n \"Provide example DataFrame to learn from and generate similar data. \"\n \"Only the first 50 rows will be used as examples.\"\n ),\n required=False,\n advanced=False,\n value=None,\n ),\n MessageTextInput(\n name=\"instructions\",\n display_name=\"Instructions\",\n info=\"Optional natural language instructions to guide the synthetic data generation process.\",\n value=\"\",\n required=False,\n advanced=True,\n ),\n IntInput(\n name=\"batch_size\",\n display_name=\"Number of Rows to Generate\",\n value=10,\n advanced=False,\n ),\n ]\n\n outputs = [\n Output(\n name=\"states\",\n display_name=\"Output Table\",\n info=\"Synthetic DataFrame generated by the LLM based on the schema or example data.\",\n method=\"aGenerate\",\n tool_mode=True,\n ),\n ]\n\n async def aGenerate(self) -> DataFrame: # noqa: N802\n \"\"\"Generate synthetic data using LLM-based generation.\n\n Returns:\n DataFrame containing the generated synthetic data.\n \"\"\"\n try:\n from agentics import AG\n from agentics.core.atype import create_pydantic_model\n from agentics.core.transducible_functions import generate_prototypical_instances\n except ImportError as e:\n raise ImportError(ERROR_AGENTICS_NOT_INSTALLED) from e\n\n llm = prepare_llm_from_component(self)\n\n if self.source:\n source = AG.from_dataframe(DataFrame(self.source))\n atype = source.atype\n instructions = str(self.instructions)\n instructions += \"\\nHere are examples to take inspiration from\" + str(source.states[:50])\n elif self.schema != []:\n schema_fields = build_schema_fields(self.schema)\n atype = create_pydantic_model(schema_fields, name=\"GeneratedData\")\n instructions = str(self.instructions)\n else:\n msg = \"Synthetic data generation requires either a sample DataFrame or schema definition (but not both).\"\n raise ValueError(msg)\n\n output_states = await generate_prototypical_instances(\n atype,\n n_instances=self.batch_size,\n llm=llm,\n instructions=instructions,\n )\n if self.source:\n output_states = source.states + output_states\n output = AG(states=output_states)\n\n return DataFrame(output.to_dataframe().to_dict(orient=\"records\"))\n" }, "instructions": { "_input_type": "MessageTextInput", @@ -2652,6 +2662,10 @@ "display_name": "Schema", "dynamic": false, "info": "Define the structure of data to generate. Specify column names, descriptions, and types. Used only when input DataFrame is not provided.", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "schema", @@ -2713,11 +2727,12 @@ "source": { "_input_type": "DataFrameInput", "advanced": false, - "display_name": "Input DataFrame", + "display_name": "Input Table", "dynamic": false, "info": "Provide example DataFrame to learn from and generate similar data. Only the first 50 rows will be used as examples.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -2743,7 +2758,7 @@ { "AgentQL": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -2768,7 +2783,7 @@ "icon": "AgentQL", "legacy": false, "metadata": { - "code_hash": "37de3210aed9", + "code_hash": "3737ac221d7d", "dependencies": { "dependencies": [ { @@ -2790,14 +2805,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "build_output", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -2840,7 +2855,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass AgentQL(Component):\n display_name = \"Extract Web Data\"\n description = \"Extracts structured data from a web page using an AgentQL query or a Natural Language description.\"\n documentation: str = \"https://docs.agentql.com/rest-api/api-reference\"\n icon = \"AgentQL\"\n name = \"AgentQL\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"AgentQL API Key\",\n required=True,\n password=True,\n info=\"Your AgentQL API key from dev.agentql.com\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL of the public web page you want to extract data from.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"query\",\n display_name=\"AgentQL Query\",\n required=False,\n info=\"The AgentQL query to execute. Learn more at https://docs.agentql.com/agentql-query or use a prompt.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n required=False,\n info=\"A Natural Language description of the data to extract from the page. Alternative to AgentQL query.\",\n tool_mode=True,\n ),\n BoolInput(\n name=\"is_stealth_mode_enabled\",\n display_name=\"Enable Stealth Mode (Beta)\",\n info=\"Enable experimental anti-bot evasion strategies. May not work for all websites at all times.\",\n value=False,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Seconds to wait for a request.\",\n value=900,\n advanced=True,\n ),\n DropdownInput(\n name=\"mode\",\n display_name=\"Request Mode\",\n info=\"'standard' uses deep data analysis, while 'fast' trades some depth of analysis for speed.\",\n options=[\"fast\", \"standard\"],\n value=\"fast\",\n advanced=True,\n ),\n IntInput(\n name=\"wait_for\",\n display_name=\"Wait For\",\n info=\"Seconds to wait for the page to load before extracting data.\",\n value=0,\n range_spec=RangeSpec(min=0, max=10, step_type=\"int\"),\n advanced=True,\n ),\n BoolInput(\n name=\"is_scroll_to_bottom_enabled\",\n display_name=\"Enable scroll to bottom\",\n info=\"Scroll to bottom of the page before extracting data.\",\n value=False,\n advanced=True,\n ),\n BoolInput(\n name=\"is_screenshot_enabled\",\n display_name=\"Enable screenshot\",\n info=\"Take a screenshot before extracting data. Returned in 'metadata' as a Base64 string.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"build_output\"),\n ]\n\n def build_output(self) -> Data:\n endpoint = \"https://api.agentql.com/v1/query-data\"\n headers = {\n \"X-API-Key\": self.api_key,\n \"Content-Type\": \"application/json\",\n \"X-TF-Request-Origin\": \"langflow\",\n }\n\n payload = {\n \"url\": self.url,\n \"query\": self.query,\n \"prompt\": self.prompt,\n \"params\": {\n \"mode\": self.mode,\n \"wait_for\": self.wait_for,\n \"is_scroll_to_bottom_enabled\": self.is_scroll_to_bottom_enabled,\n \"is_screenshot_enabled\": self.is_screenshot_enabled,\n },\n \"metadata\": {\n \"experimental_stealth_mode_enabled\": self.is_stealth_mode_enabled,\n },\n }\n\n if not self.prompt and not self.query:\n self.status = \"Either Query or Prompt must be provided.\"\n raise ValueError(self.status)\n if self.prompt and self.query:\n self.status = \"Both Query and Prompt can't be provided at the same time.\"\n raise ValueError(self.status)\n\n try:\n response = httpx.post(endpoint, headers=headers, json=payload, timeout=self.timeout)\n response.raise_for_status()\n\n json = response.json()\n data = Data(result=json[\"data\"], metadata=json[\"metadata\"])\n\n except httpx.HTTPStatusError as e:\n response = e.response\n if response.status_code == httpx.codes.UNAUTHORIZED:\n self.status = \"Please, provide a valid API Key. You can create one at https://dev.agentql.com.\"\n else:\n try:\n error_json = response.json()\n logger.error(\n f\"Failure response: '{response.status_code} {response.reason_phrase}' with body: {error_json}\"\n )\n msg = error_json[\"error_info\"] if \"error_info\" in error_json else error_json[\"detail\"]\n except (ValueError, TypeError):\n msg = f\"HTTP {e}.\"\n self.status = msg\n raise ValueError(self.status) from e\n\n else:\n self.status = data\n return data\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass AgentQL(Component):\n display_name = \"Extract Web Data\"\n description = \"Extracts structured data from a web page using an AgentQL query or a Natural Language description.\"\n documentation: str = \"https://docs.agentql.com/rest-api/api-reference\"\n icon = \"AgentQL\"\n name = \"AgentQL\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"AgentQL API Key\",\n required=True,\n password=True,\n info=\"Your AgentQL API key from dev.agentql.com\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL of the public web page you want to extract data from.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"query\",\n display_name=\"AgentQL Query\",\n required=False,\n info=\"The AgentQL query to execute. Learn more at https://docs.agentql.com/agentql-query or use a prompt.\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n required=False,\n info=\"A Natural Language description of the data to extract from the page. Alternative to AgentQL query.\",\n tool_mode=True,\n ),\n BoolInput(\n name=\"is_stealth_mode_enabled\",\n display_name=\"Enable Stealth Mode (Beta)\",\n info=\"Enable experimental anti-bot evasion strategies. May not work for all websites at all times.\",\n value=False,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Seconds to wait for a request.\",\n value=900,\n advanced=True,\n ),\n DropdownInput(\n name=\"mode\",\n display_name=\"Request Mode\",\n info=\"'standard' uses deep data analysis, while 'fast' trades some depth of analysis for speed.\",\n options=[\"fast\", \"standard\"],\n value=\"fast\",\n advanced=True,\n ),\n IntInput(\n name=\"wait_for\",\n display_name=\"Wait For\",\n info=\"Seconds to wait for the page to load before extracting data.\",\n value=0,\n range_spec=RangeSpec(min=0, max=10, step_type=\"int\"),\n advanced=True,\n ),\n BoolInput(\n name=\"is_scroll_to_bottom_enabled\",\n display_name=\"Enable scroll to bottom\",\n info=\"Scroll to bottom of the page before extracting data.\",\n value=False,\n advanced=True,\n ),\n BoolInput(\n name=\"is_screenshot_enabled\",\n display_name=\"Enable screenshot\",\n info=\"Take a screenshot before extracting data. Returned in 'metadata' as a Base64 string.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"build_output\"),\n ]\n\n def build_output(self) -> Data:\n endpoint = \"https://api.agentql.com/v1/query-data\"\n headers = {\n \"X-API-Key\": self.api_key,\n \"Content-Type\": \"application/json\",\n \"X-TF-Request-Origin\": \"langflow\",\n }\n\n payload = {\n \"url\": self.url,\n \"query\": self.query,\n \"prompt\": self.prompt,\n \"params\": {\n \"mode\": self.mode,\n \"wait_for\": self.wait_for,\n \"is_scroll_to_bottom_enabled\": self.is_scroll_to_bottom_enabled,\n \"is_screenshot_enabled\": self.is_screenshot_enabled,\n },\n \"metadata\": {\n \"experimental_stealth_mode_enabled\": self.is_stealth_mode_enabled,\n },\n }\n\n if not self.prompt and not self.query:\n self.status = \"Either Query or Prompt must be provided.\"\n raise ValueError(self.status)\n if self.prompt and self.query:\n self.status = \"Both Query and Prompt can't be provided at the same time.\"\n raise ValueError(self.status)\n\n try:\n response = httpx.post(endpoint, headers=headers, json=payload, timeout=self.timeout)\n response.raise_for_status()\n\n json = response.json()\n data = Data(result=json[\"data\"], metadata=json[\"metadata\"])\n\n except httpx.HTTPStatusError as e:\n response = e.response\n if response.status_code == httpx.codes.UNAUTHORIZED:\n self.status = \"Please, provide a valid API Key. You can create one at https://dev.agentql.com.\"\n else:\n try:\n error_json = response.json()\n logger.error(\n f\"Failure response: '{response.status_code} {response.reason_phrase}' with body: {error_json}\"\n )\n msg = error_json[\"error_info\"] if \"error_info\" in error_json else error_json[\"detail\"]\n except (ValueError, TypeError):\n msg = f\"HTTP {e}.\"\n self.status = msg\n raise ValueError(self.status) from e\n\n else:\n self.status = data\n return data\n" }, "is_screenshot_enabled": { "_input_type": "BoolInput", @@ -4002,6 +4017,10 @@ "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", @@ -5445,7 +5464,7 @@ "icon": "Amazon", "legacy": false, "metadata": { - "code_hash": "6e4ba2dafc3c", + "code_hash": "119c89b6bd40", "dependencies": { "dependencies": [ { @@ -5557,7 +5576,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from pathlib import Path\nfrom typing import Any\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n BoolInput,\n DropdownInput,\n HandleInput,\n Output,\n SecretStrInput,\n StrInput,\n)\n\n\nclass S3BucketUploaderComponent(Component):\n \"\"\"S3BucketUploaderComponent is a component responsible for uploading files to an S3 bucket.\n\n It provides two strategies for file upload: \"By Data\" and \"By File Name\". The component\n requires AWS credentials and bucket details as inputs and processes files accordingly.\n\n Attributes:\n display_name (str): The display name of the component.\n description (str): A brief description of the components functionality.\n icon (str): The icon representing the component.\n name (str): The internal name of the component.\n inputs (list): A list of input configurations required by the component.\n outputs (list): A list of output configurations provided by the component.\n\n Methods:\n process_files() -> None:\n Processes files based on the selected strategy. Calls the appropriate method\n based on the strategy attribute.\n process_files_by_data() -> None:\n Processes and uploads files to an S3 bucket based on the data inputs. Iterates\n over the data inputs, logs the file path and text content, and uploads each file\n to the specified S3 bucket if both file path and text content are available.\n process_files_by_name() -> None:\n Processes and uploads files to an S3 bucket based on their names. Iterates through\n the list of data inputs, retrieves the file path from each data item, and uploads\n the file to the specified S3 bucket if the file path is available. Logs the file\n path being uploaded.\n _s3_client() -> Any:\n Creates and returns an S3 client using the provided AWS access key ID and secret\n access key.\n\n Please note that this component requires the boto3 library to be installed. It is designed\n to work with File and Director components as inputs\n \"\"\"\n\n display_name = \"S3 Bucket Uploader\"\n description = \"Uploads files to S3 bucket.\"\n icon = \"Amazon\"\n name = \"s3bucketuploader\"\n\n inputs = [\n SecretStrInput(\n name=\"aws_access_key_id\",\n display_name=\"AWS Access Key ID\",\n required=True,\n password=True,\n info=\"AWS Access key ID.\",\n ),\n SecretStrInput(\n name=\"aws_secret_access_key\",\n display_name=\"AWS Secret Key\",\n required=True,\n password=True,\n info=\"AWS Secret Key.\",\n ),\n StrInput(\n name=\"bucket_name\",\n display_name=\"Bucket Name\",\n info=\"Enter the name of the bucket.\",\n advanced=False,\n ),\n DropdownInput(\n name=\"strategy\",\n display_name=\"Strategy for file upload\",\n options=[\"Store Data\", \"Store Original File\"],\n value=\"By Data\",\n info=(\n \"Choose the strategy to upload the file. By Data means that the source file \"\n \"is parsed and stored as LangFlow data. By File Name means that the source \"\n \"file is uploaded as is.\"\n ),\n ),\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Data Inputs\",\n info=\"The data to split.\",\n input_types=[\"Data\"],\n is_list=True,\n required=True,\n ),\n StrInput(\n name=\"s3_prefix\",\n display_name=\"S3 Prefix\",\n info=\"Prefix for all files.\",\n advanced=True,\n ),\n BoolInput(\n name=\"strip_path\",\n display_name=\"Strip Path\",\n info=\"Removes path from file path.\",\n required=True,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Writes to AWS Bucket\", name=\"data\", method=\"process_files\"),\n ]\n\n def process_files(self) -> None:\n \"\"\"Process files based on the selected strategy.\n\n This method uses a strategy pattern to process files. The strategy is determined\n by the `self.strategy` attribute, which can be either \"By Data\" or \"By File Name\".\n Depending on the strategy, the corresponding method (`process_files_by_data` or\n `process_files_by_name`) is called. If an invalid strategy is provided, an error\n is logged.\n\n Returns:\n None\n \"\"\"\n strategy_methods = {\n \"Store Data\": self.process_files_by_data,\n \"Store Original File\": self.process_files_by_name,\n }\n strategy_methods.get(self.strategy, lambda: self.log(\"Invalid strategy\"))()\n\n def process_files_by_data(self) -> None:\n \"\"\"Processes and uploads files to an S3 bucket based on the data inputs.\n\n This method iterates over the data inputs, logs the file path and text content,\n and uploads each file to the specified S3 bucket if both file path and text content\n are available.\n\n Args:\n None\n\n Returns:\n None\n \"\"\"\n for data_item in self.data_inputs:\n file_path = data_item.data.get(\"file_path\")\n text_content = data_item.data.get(\"text\")\n\n if file_path and text_content:\n self._s3_client().put_object(\n Bucket=self.bucket_name, Key=self._normalize_path(file_path), Body=text_content\n )\n\n def process_files_by_name(self) -> None:\n \"\"\"Processes and uploads files to an S3 bucket based on their names.\n\n Iterates through the list of data inputs, retrieves the file path from each data item,\n and uploads the file to the specified S3 bucket if the file path is available.\n Logs the file path being uploaded.\n\n Returns:\n None\n \"\"\"\n for data_item in self.data_inputs:\n file_path = data_item.data.get(\"file_path\")\n self.log(f\"Uploading file: {file_path}\")\n if file_path:\n self._s3_client().upload_file(file_path, Bucket=self.bucket_name, Key=self._normalize_path(file_path))\n\n def _s3_client(self) -> Any:\n \"\"\"Creates and returns an S3 client using the provided AWS access key ID and secret access key.\n\n Returns:\n Any: A boto3 S3 client instance.\n \"\"\"\n try:\n import boto3\n except ImportError as e:\n msg = \"boto3 is not installed. Please install it using `uv pip install boto3`.\"\n raise ImportError(msg) from e\n\n return boto3.client(\n \"s3\",\n aws_access_key_id=self.aws_access_key_id,\n aws_secret_access_key=self.aws_secret_access_key,\n )\n\n def _normalize_path(self, file_path) -> str:\n \"\"\"Process the file path based on the s3_prefix and path_as_prefix.\n\n Args:\n file_path (str): The original file path.\n s3_prefix (str): The S3 prefix to use.\n path_as_prefix (bool): Whether to use the file path as the S3 prefix.\n\n Returns:\n str: The processed file path.\n \"\"\"\n prefix = self.s3_prefix\n strip_path = self.strip_path\n processed_path: str = file_path\n\n if strip_path:\n # Filename only\n processed_path = Path(file_path).name\n\n # Concatenate the s3_prefix if it exists\n if prefix:\n processed_path = str(Path(prefix) / processed_path)\n\n return processed_path\n" + "value": "from pathlib import Path\nfrom typing import Any\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n BoolInput,\n DropdownInput,\n HandleInput,\n Output,\n SecretStrInput,\n StrInput,\n)\n\n\nclass S3BucketUploaderComponent(Component):\n \"\"\"S3BucketUploaderComponent is a component responsible for uploading files to an S3 bucket.\n\n It provides two strategies for file upload: \"By Data\" and \"By File Name\". The component\n requires AWS credentials and bucket details as inputs and processes files accordingly.\n\n Attributes:\n display_name (str): The display name of the component.\n description (str): A brief description of the components functionality.\n icon (str): The icon representing the component.\n name (str): The internal name of the component.\n inputs (list): A list of input configurations required by the component.\n outputs (list): A list of output configurations provided by the component.\n\n Methods:\n process_files() -> None:\n Processes files based on the selected strategy. Calls the appropriate method\n based on the strategy attribute.\n process_files_by_data() -> None:\n Processes and uploads files to an S3 bucket based on the data inputs. Iterates\n over the data inputs, logs the file path and text content, and uploads each file\n to the specified S3 bucket if both file path and text content are available.\n process_files_by_name() -> None:\n Processes and uploads files to an S3 bucket based on their names. Iterates through\n the list of data inputs, retrieves the file path from each data item, and uploads\n the file to the specified S3 bucket if the file path is available. Logs the file\n path being uploaded.\n _s3_client() -> Any:\n Creates and returns an S3 client using the provided AWS access key ID and secret\n access key.\n\n Please note that this component requires the boto3 library to be installed. It is designed\n to work with File and Director components as inputs\n \"\"\"\n\n display_name = \"S3 Bucket Uploader\"\n description = \"Uploads files to S3 bucket.\"\n icon = \"Amazon\"\n name = \"s3bucketuploader\"\n\n inputs = [\n SecretStrInput(\n name=\"aws_access_key_id\",\n display_name=\"AWS Access Key ID\",\n required=True,\n password=True,\n info=\"AWS Access key ID.\",\n ),\n SecretStrInput(\n name=\"aws_secret_access_key\",\n display_name=\"AWS Secret Key\",\n required=True,\n password=True,\n info=\"AWS Secret Key.\",\n ),\n StrInput(\n name=\"bucket_name\",\n display_name=\"Bucket Name\",\n info=\"Enter the name of the bucket.\",\n advanced=False,\n ),\n DropdownInput(\n name=\"strategy\",\n display_name=\"Strategy for file upload\",\n options=[\"Store Data\", \"Store Original File\"],\n value=\"By Data\",\n info=(\n \"Choose the strategy to upload the file. By Data means that the source file \"\n \"is parsed and stored as LangFlow data. By File Name means that the source \"\n \"file is uploaded as is.\"\n ),\n ),\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Data Inputs\",\n info=\"The data to split.\",\n input_types=[\"Data\", \"JSON\"],\n is_list=True,\n required=True,\n ),\n StrInput(\n name=\"s3_prefix\",\n display_name=\"S3 Prefix\",\n info=\"Prefix for all files.\",\n advanced=True,\n ),\n BoolInput(\n name=\"strip_path\",\n display_name=\"Strip Path\",\n info=\"Removes path from file path.\",\n required=True,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Writes to AWS Bucket\", name=\"data\", method=\"process_files\"),\n ]\n\n def process_files(self) -> None:\n \"\"\"Process files based on the selected strategy.\n\n This method uses a strategy pattern to process files. The strategy is determined\n by the `self.strategy` attribute, which can be either \"By Data\" or \"By File Name\".\n Depending on the strategy, the corresponding method (`process_files_by_data` or\n `process_files_by_name`) is called. If an invalid strategy is provided, an error\n is logged.\n\n Returns:\n None\n \"\"\"\n strategy_methods = {\n \"Store Data\": self.process_files_by_data,\n \"Store Original File\": self.process_files_by_name,\n }\n strategy_methods.get(self.strategy, lambda: self.log(\"Invalid strategy\"))()\n\n def process_files_by_data(self) -> None:\n \"\"\"Processes and uploads files to an S3 bucket based on the data inputs.\n\n This method iterates over the data inputs, logs the file path and text content,\n and uploads each file to the specified S3 bucket if both file path and text content\n are available.\n\n Args:\n None\n\n Returns:\n None\n \"\"\"\n for data_item in self.data_inputs:\n file_path = data_item.data.get(\"file_path\")\n text_content = data_item.data.get(\"text\")\n\n if file_path and text_content:\n self._s3_client().put_object(\n Bucket=self.bucket_name, Key=self._normalize_path(file_path), Body=text_content\n )\n\n def process_files_by_name(self) -> None:\n \"\"\"Processes and uploads files to an S3 bucket based on their names.\n\n Iterates through the list of data inputs, retrieves the file path from each data item,\n and uploads the file to the specified S3 bucket if the file path is available.\n Logs the file path being uploaded.\n\n Returns:\n None\n \"\"\"\n for data_item in self.data_inputs:\n file_path = data_item.data.get(\"file_path\")\n self.log(f\"Uploading file: {file_path}\")\n if file_path:\n self._s3_client().upload_file(file_path, Bucket=self.bucket_name, Key=self._normalize_path(file_path))\n\n def _s3_client(self) -> Any:\n \"\"\"Creates and returns an S3 client using the provided AWS access key ID and secret access key.\n\n Returns:\n Any: A boto3 S3 client instance.\n \"\"\"\n try:\n import boto3\n except ImportError as e:\n msg = \"boto3 is not installed. Please install it using `uv pip install boto3`.\"\n raise ImportError(msg) from e\n\n return boto3.client(\n \"s3\",\n aws_access_key_id=self.aws_access_key_id,\n aws_secret_access_key=self.aws_secret_access_key,\n )\n\n def _normalize_path(self, file_path) -> str:\n \"\"\"Process the file path based on the s3_prefix and path_as_prefix.\n\n Args:\n file_path (str): The original file path.\n s3_prefix (str): The S3 prefix to use.\n path_as_prefix (bool): Whether to use the file path as the S3 prefix.\n\n Returns:\n str: The processed file path.\n \"\"\"\n prefix = self.s3_prefix\n strip_path = self.strip_path\n processed_path: str = file_path\n\n if strip_path:\n # Filename only\n processed_path = Path(file_path).name\n\n # Concatenate the s3_prefix if it exists\n if prefix:\n processed_path = str(Path(prefix) / processed_path)\n\n return processed_path\n" }, "data_inputs": { "_input_type": "HandleInput", @@ -5566,7 +5585,8 @@ "dynamic": false, "info": "The data to split.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -6005,7 +6025,7 @@ { "ApifyActors": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -6064,10 +6084,10 @@ "group_outputs": false, "method": "run_model", "name": "output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -6235,7 +6255,7 @@ { "ArXivComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -6253,7 +6273,7 @@ "icon": "arXiv", "legacy": false, "metadata": { - "code_hash": "219239ee2b48", + "code_hash": "2d892beaf98b", "dependencies": { "dependencies": [ { @@ -6275,14 +6295,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "search_papers_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -6306,7 +6326,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import urllib.request\nfrom urllib.parse import urlparse\nfrom xml.etree.ElementTree import Element\n\nfrom defusedxml.ElementTree import fromstring\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass ArXivComponent(Component):\n display_name = \"arXiv\"\n description = \"Search and retrieve papers from arXiv.org\"\n icon = \"arXiv\"\n\n inputs = [\n MessageTextInput(\n name=\"search_query\",\n display_name=\"Search Query\",\n info=\"The search query for arXiv papers (e.g., 'quantum computing')\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_type\",\n display_name=\"Search Field\",\n info=\"The field to search in\",\n options=[\"all\", \"title\", \"abstract\", \"author\", \"cat\"], # cat is for category\n value=\"all\",\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"Maximum number of results to return\",\n value=10,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"search_papers_dataframe\"),\n ]\n\n def build_query_url(self) -> str:\n \"\"\"Build the arXiv API query URL.\"\"\"\n base_url = \"http://export.arxiv.org/api/query?\"\n\n # Build the search query based on search type\n if self.search_type == \"all\":\n search_query = self.search_query # No prefix for all fields\n else:\n # Map dropdown values to ArXiv API prefixes\n prefix_map = {\"title\": \"ti\", \"abstract\": \"abs\", \"author\": \"au\", \"cat\": \"cat\"}\n prefix = prefix_map.get(self.search_type, \"\")\n search_query = f\"{prefix}:{self.search_query}\"\n\n # URL parameters\n params = {\n \"search_query\": search_query,\n \"max_results\": str(self.max_results),\n }\n\n # Convert params to URL query string\n query_string = \"&\".join([f\"{k}={urllib.parse.quote(str(v))}\" for k, v in params.items()])\n\n return base_url + query_string\n\n def parse_atom_response(self, response_text: str) -> list[dict]:\n \"\"\"Parse the Atom XML response from arXiv.\"\"\"\n # Parse XML safely using defusedxml\n root = fromstring(response_text)\n\n # Define namespace dictionary for XML parsing\n ns = {\"atom\": \"http://www.w3.org/2005/Atom\", \"arxiv\": \"http://arxiv.org/schemas/atom\"}\n\n papers = []\n # Process each entry (paper)\n for entry in root.findall(\"atom:entry\", ns):\n paper = {\n \"id\": self._get_text(entry, \"atom:id\", ns),\n \"title\": self._get_text(entry, \"atom:title\", ns),\n \"summary\": self._get_text(entry, \"atom:summary\", ns),\n \"published\": self._get_text(entry, \"atom:published\", ns),\n \"updated\": self._get_text(entry, \"atom:updated\", ns),\n \"authors\": [author.find(\"atom:name\", ns).text for author in entry.findall(\"atom:author\", ns)],\n \"arxiv_url\": self._get_link(entry, \"alternate\", ns),\n \"pdf_url\": self._get_link(entry, \"related\", ns),\n \"comment\": self._get_text(entry, \"arxiv:comment\", ns),\n \"journal_ref\": self._get_text(entry, \"arxiv:journal_ref\", ns),\n \"primary_category\": self._get_category(entry, ns),\n \"categories\": [cat.get(\"term\") for cat in entry.findall(\"atom:category\", ns)],\n }\n papers.append(paper)\n\n return papers\n\n def _get_text(self, element: Element, path: str, ns: dict) -> str | None:\n \"\"\"Safely extract text from an XML element.\"\"\"\n el = element.find(path, ns)\n return el.text.strip() if el is not None and el.text else None\n\n def _get_link(self, element: Element, rel: str, ns: dict) -> str | None:\n \"\"\"Get link URL based on relation type.\"\"\"\n for link in element.findall(\"atom:link\", ns):\n if link.get(\"rel\") == rel:\n return link.get(\"href\")\n return None\n\n def _get_category(self, element: Element, ns: dict) -> str | None:\n \"\"\"Get primary category.\"\"\"\n cat = element.find(\"arxiv:primary_category\", ns)\n return cat.get(\"term\") if cat is not None else None\n\n def run_model(self) -> DataFrame:\n return self.search_papers_dataframe()\n\n def search_papers(self) -> list[Data]:\n \"\"\"Search arXiv and return results.\"\"\"\n try:\n # Build the query URL\n url = self.build_query_url()\n\n # Validate URL scheme and host\n parsed_url = urlparse(url)\n if parsed_url.scheme not in {\"http\", \"https\"}:\n error_msg = f\"Invalid URL scheme: {parsed_url.scheme}\"\n raise ValueError(error_msg)\n if parsed_url.hostname != \"export.arxiv.org\":\n error_msg = f\"Invalid host: {parsed_url.hostname}\"\n raise ValueError(error_msg)\n\n # Create a custom opener that only allows http/https schemes\n class RestrictedHTTPHandler(urllib.request.HTTPHandler):\n def http_open(self, req):\n return super().http_open(req)\n\n class RestrictedHTTPSHandler(urllib.request.HTTPSHandler):\n def https_open(self, req):\n return super().https_open(req)\n\n # Build opener with restricted handlers\n opener = urllib.request.build_opener(RestrictedHTTPHandler, RestrictedHTTPSHandler)\n urllib.request.install_opener(opener)\n\n # Make the request with validated URL using restricted opener\n response = opener.open(url)\n response_text = response.read().decode(\"utf-8\")\n\n # Parse the response\n papers = self.parse_atom_response(response_text)\n\n # Convert to Data objects\n results = [Data(data=paper) for paper in papers]\n self.status = results\n except (urllib.error.URLError, ValueError) as e:\n error_data = Data(data={\"error\": f\"Request error: {e!s}\"})\n self.status = error_data\n return [error_data]\n else:\n return results\n\n def search_papers_dataframe(self) -> DataFrame:\n \"\"\"Convert the Arxiv search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.search_papers()\n return DataFrame(data)\n" + "value": "import urllib.request\nfrom urllib.parse import urlparse\nfrom xml.etree.ElementTree import Element\n\nfrom defusedxml.ElementTree import fromstring\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DropdownInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass ArXivComponent(Component):\n display_name = \"arXiv\"\n description = \"Search and retrieve papers from arXiv.org\"\n icon = \"arXiv\"\n\n inputs = [\n MessageTextInput(\n name=\"search_query\",\n display_name=\"Search Query\",\n info=\"The search query for arXiv papers (e.g., 'quantum computing')\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_type\",\n display_name=\"Search Field\",\n info=\"The field to search in\",\n options=[\"all\", \"title\", \"abstract\", \"author\", \"cat\"], # cat is for category\n value=\"all\",\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"Maximum number of results to return\",\n value=10,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"search_papers_dataframe\"),\n ]\n\n def build_query_url(self) -> str:\n \"\"\"Build the arXiv API query URL.\"\"\"\n base_url = \"http://export.arxiv.org/api/query?\"\n\n # Build the search query based on search type\n if self.search_type == \"all\":\n search_query = self.search_query # No prefix for all fields\n else:\n # Map dropdown values to ArXiv API prefixes\n prefix_map = {\"title\": \"ti\", \"abstract\": \"abs\", \"author\": \"au\", \"cat\": \"cat\"}\n prefix = prefix_map.get(self.search_type, \"\")\n search_query = f\"{prefix}:{self.search_query}\"\n\n # URL parameters\n params = {\n \"search_query\": search_query,\n \"max_results\": str(self.max_results),\n }\n\n # Convert params to URL query string\n query_string = \"&\".join([f\"{k}={urllib.parse.quote(str(v))}\" for k, v in params.items()])\n\n return base_url + query_string\n\n def parse_atom_response(self, response_text: str) -> list[dict]:\n \"\"\"Parse the Atom XML response from arXiv.\"\"\"\n # Parse XML safely using defusedxml\n root = fromstring(response_text)\n\n # Define namespace dictionary for XML parsing\n ns = {\"atom\": \"http://www.w3.org/2005/Atom\", \"arxiv\": \"http://arxiv.org/schemas/atom\"}\n\n papers = []\n # Process each entry (paper)\n for entry in root.findall(\"atom:entry\", ns):\n paper = {\n \"id\": self._get_text(entry, \"atom:id\", ns),\n \"title\": self._get_text(entry, \"atom:title\", ns),\n \"summary\": self._get_text(entry, \"atom:summary\", ns),\n \"published\": self._get_text(entry, \"atom:published\", ns),\n \"updated\": self._get_text(entry, \"atom:updated\", ns),\n \"authors\": [author.find(\"atom:name\", ns).text for author in entry.findall(\"atom:author\", ns)],\n \"arxiv_url\": self._get_link(entry, \"alternate\", ns),\n \"pdf_url\": self._get_link(entry, \"related\", ns),\n \"comment\": self._get_text(entry, \"arxiv:comment\", ns),\n \"journal_ref\": self._get_text(entry, \"arxiv:journal_ref\", ns),\n \"primary_category\": self._get_category(entry, ns),\n \"categories\": [cat.get(\"term\") for cat in entry.findall(\"atom:category\", ns)],\n }\n papers.append(paper)\n\n return papers\n\n def _get_text(self, element: Element, path: str, ns: dict) -> str | None:\n \"\"\"Safely extract text from an XML element.\"\"\"\n el = element.find(path, ns)\n return el.text.strip() if el is not None and el.text else None\n\n def _get_link(self, element: Element, rel: str, ns: dict) -> str | None:\n \"\"\"Get link URL based on relation type.\"\"\"\n for link in element.findall(\"atom:link\", ns):\n if link.get(\"rel\") == rel:\n return link.get(\"href\")\n return None\n\n def _get_category(self, element: Element, ns: dict) -> str | None:\n \"\"\"Get primary category.\"\"\"\n cat = element.find(\"arxiv:primary_category\", ns)\n return cat.get(\"term\") if cat is not None else None\n\n def run_model(self) -> DataFrame:\n return self.search_papers_dataframe()\n\n def search_papers(self) -> list[Data]:\n \"\"\"Search arXiv and return results.\"\"\"\n try:\n # Build the query URL\n url = self.build_query_url()\n\n # Validate URL scheme and host\n parsed_url = urlparse(url)\n if parsed_url.scheme not in {\"http\", \"https\"}:\n error_msg = f\"Invalid URL scheme: {parsed_url.scheme}\"\n raise ValueError(error_msg)\n if parsed_url.hostname != \"export.arxiv.org\":\n error_msg = f\"Invalid host: {parsed_url.hostname}\"\n raise ValueError(error_msg)\n\n # Create a custom opener that only allows http/https schemes\n class RestrictedHTTPHandler(urllib.request.HTTPHandler):\n def http_open(self, req):\n return super().http_open(req)\n\n class RestrictedHTTPSHandler(urllib.request.HTTPSHandler):\n def https_open(self, req):\n return super().https_open(req)\n\n # Build opener with restricted handlers\n opener = urllib.request.build_opener(RestrictedHTTPHandler, RestrictedHTTPSHandler)\n urllib.request.install_opener(opener)\n\n # Make the request with validated URL using restricted opener\n response = opener.open(url)\n response_text = response.read().decode(\"utf-8\")\n\n # Parse the response\n papers = self.parse_atom_response(response_text)\n\n # Convert to Data objects\n results = [Data(data=paper) for paper in papers]\n self.status = results\n except (urllib.error.URLError, ValueError) as e:\n error_data = Data(data={\"error\": f\"Request error: {e!s}\"})\n self.status = error_data\n return [error_data]\n else:\n return results\n\n def search_papers_dataframe(self) -> DataFrame:\n \"\"\"Convert the Arxiv search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.search_papers()\n return DataFrame(data)\n" }, "max_results": { "_input_type": "IntInput", @@ -6393,7 +6413,7 @@ { "AssemblyAIGetSubtitles": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -6438,10 +6458,10 @@ "group_outputs": false, "method": "get_subtitles", "name": "subtitles", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -6534,13 +6554,14 @@ "value": "srt" }, "transcription_result": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Transcription Result", "dynamic": false, "info": "The transcription result from AssemblyAI", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -6562,7 +6583,7 @@ }, "AssemblyAILeMUR": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -6612,10 +6633,10 @@ "group_outputs": false, "method": "run_lemur", "name": "lemur_response", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -6845,13 +6866,14 @@ "value": "" }, "transcription_result": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Transcription Result", "dynamic": false, "info": "The transcription result from AssemblyAI", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -6873,7 +6895,7 @@ }, "AssemblyAIListTranscripts": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -6919,10 +6941,10 @@ "group_outputs": false, "method": "list_transcripts", "name": "transcript_list", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -7067,7 +7089,7 @@ }, "AssemblyAITranscriptionJobCreator": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -7118,10 +7140,10 @@ "group_outputs": false, "method": "create_transcription_job", "name": "transcript_id", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -7416,7 +7438,7 @@ }, "AssemblyAITranscriptionJobPoller": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -7460,10 +7482,10 @@ "group_outputs": false, "method": "poll_transcription_job", "name": "transcription_result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -7535,13 +7557,14 @@ "value": 3.0 }, "transcript_id": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Transcript ID", "dynamic": false, "info": "The ID of the transcription job to poll", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -8501,7 +8524,7 @@ { "BingSearchAPI": { "base_classes": [ - "DataFrame", + "Table", "Tool" ], "beta": false, @@ -8521,7 +8544,7 @@ "icon": "Bing", "legacy": false, "metadata": { - "code_hash": "84334607b325", + "code_hash": "21008f6682b9", "dependencies": { "dependencies": [ { @@ -8543,14 +8566,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -8632,7 +8655,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import cast\n\nfrom langchain_community.tools.bing_search import BingSearchResults\nfrom langchain_community.utilities import BingSearchAPIWrapper\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import IntInput, MessageTextInput, MultilineInput, SecretStrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass BingSearchAPIComponent(LCToolComponent):\n display_name = \"Bing Search API\"\n description = \"Call the Bing Search API.\"\n name = \"BingSearchAPI\"\n icon = \"Bing\"\n\n inputs = [\n SecretStrInput(name=\"bing_subscription_key\", display_name=\"Bing Subscription Key\"),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n ),\n MessageTextInput(name=\"bing_search_url\", display_name=\"Bing Search URL\", advanced=True),\n IntInput(name=\"k\", display_name=\"Number of results\", value=4, required=True),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n Output(display_name=\"Tool\", name=\"tool\", method=\"build_tool\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n if self.bing_search_url:\n wrapper = BingSearchAPIWrapper(\n bing_search_url=self.bing_search_url, bing_subscription_key=self.bing_subscription_key\n )\n else:\n wrapper = BingSearchAPIWrapper(bing_subscription_key=self.bing_subscription_key)\n results = wrapper.results(query=self.input_value, num_results=self.k)\n data = [Data(data=result, text=result[\"snippet\"]) for result in results]\n self.status = data\n return data\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n\n def build_tool(self) -> Tool:\n if self.bing_search_url:\n wrapper = BingSearchAPIWrapper(\n bing_search_url=self.bing_search_url, bing_subscription_key=self.bing_subscription_key\n )\n else:\n wrapper = BingSearchAPIWrapper(bing_subscription_key=self.bing_subscription_key)\n return cast(\"Tool\", BingSearchResults(api_wrapper=wrapper, num_results=self.k))\n" + "value": "from typing import cast\n\nfrom langchain_community.tools.bing_search import BingSearchResults\nfrom langchain_community.utilities import BingSearchAPIWrapper\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import IntInput, MessageTextInput, MultilineInput, SecretStrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass BingSearchAPIComponent(LCToolComponent):\n display_name = \"Bing Search API\"\n description = \"Call the Bing Search API.\"\n name = \"BingSearchAPI\"\n icon = \"Bing\"\n\n inputs = [\n SecretStrInput(name=\"bing_subscription_key\", display_name=\"Bing Subscription Key\"),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n ),\n MessageTextInput(name=\"bing_search_url\", display_name=\"Bing Search URL\", advanced=True),\n IntInput(name=\"k\", display_name=\"Number of results\", value=4, required=True),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n Output(display_name=\"Tool\", name=\"tool\", method=\"build_tool\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n if self.bing_search_url:\n wrapper = BingSearchAPIWrapper(\n bing_search_url=self.bing_search_url, bing_subscription_key=self.bing_subscription_key\n )\n else:\n wrapper = BingSearchAPIWrapper(bing_subscription_key=self.bing_subscription_key)\n results = wrapper.results(query=self.input_value, num_results=self.k)\n data = [Data(data=result, text=result[\"snippet\"]) for result in results]\n self.status = data\n return data\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n\n def build_tool(self) -> Tool:\n if self.bing_search_url:\n wrapper = BingSearchAPIWrapper(\n bing_search_url=self.bing_search_url, bing_subscription_key=self.bing_subscription_key\n )\n else:\n wrapper = BingSearchAPIWrapper(bing_subscription_key=self.bing_subscription_key)\n return cast(\"Tool\", BingSearchResults(api_wrapper=wrapper, num_results=self.k))\n" }, "input_value": { "_input_type": "MultilineInput", @@ -8693,8 +8716,8 @@ { "Cassandra": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -8758,24 +8781,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -9486,8 +9509,8 @@ }, "CassandraGraph": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -9548,24 +9571,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -9967,8 +9990,8 @@ { "Chroma": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -10036,24 +10059,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -11328,8 +11351,8 @@ { "Clickhouse": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -11391,24 +11414,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -12521,7 +12544,7 @@ }, "CohereRerank": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -12567,10 +12590,10 @@ "group_outputs": false, "method": "compress_documents", "name": "reranked_documents", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -12674,13 +12697,14 @@ "value": "" }, "search_results": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Search Results", "dynamic": false, "info": "Search Results from a Vector Store.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -13287,7 +13311,7 @@ }, "ComposioAgentQLAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -13345,14 +13369,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -13940,7 +13964,7 @@ }, "ComposioAgiledAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -13998,14 +14022,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -14593,7 +14617,7 @@ }, "ComposioAirtableAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -14651,14 +14675,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -15246,7 +15270,7 @@ }, "ComposioApolloAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -15304,14 +15328,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -15899,7 +15923,7 @@ }, "ComposioAsanaAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -15957,14 +15981,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -16552,7 +16576,7 @@ }, "ComposioAttioAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -16610,14 +16634,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -17205,7 +17229,7 @@ }, "ComposioBitbucketAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -17263,14 +17287,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -17858,7 +17882,7 @@ }, "ComposioBolnaAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -17916,14 +17940,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -18511,7 +18535,7 @@ }, "ComposioBrightdataAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -18569,14 +18593,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -19164,7 +19188,7 @@ }, "ComposioCalendlyAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -19222,14 +19246,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -19817,7 +19841,7 @@ }, "ComposioCanvaAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -19875,14 +19899,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -20470,7 +20494,7 @@ }, "ComposioCanvasAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -20528,14 +20552,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -21123,7 +21147,7 @@ }, "ComposioCodaAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -21181,14 +21205,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -21776,7 +21800,7 @@ }, "ComposioContentfulAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -21834,14 +21858,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -22429,7 +22453,7 @@ }, "ComposioDigicertAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -22487,14 +22511,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -23082,7 +23106,7 @@ }, "ComposioDiscordAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -23140,14 +23164,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -23735,7 +23759,7 @@ }, "ComposioDropboxAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -23793,14 +23817,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -24388,7 +24412,7 @@ }, "ComposioElevenLabsAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -24446,14 +24470,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -25041,7 +25065,7 @@ }, "ComposioExaAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -25099,14 +25123,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -25694,7 +25718,7 @@ }, "ComposioFigmaAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -25752,14 +25776,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -26347,7 +26371,7 @@ }, "ComposioFinageAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -26405,14 +26429,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -27000,7 +27024,7 @@ }, "ComposioFirecrawlAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -27058,14 +27082,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -27653,7 +27677,7 @@ }, "ComposioFirefliesAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -27711,14 +27735,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -28306,7 +28330,7 @@ }, "ComposioFixerAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -28364,14 +28388,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -28959,7 +28983,7 @@ }, "ComposioFlexisignAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -29017,14 +29041,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -29612,7 +29636,7 @@ }, "ComposioFreshdeskAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -29670,14 +29694,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -30265,7 +30289,7 @@ }, "ComposioGitHubAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -30323,14 +30347,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -30918,7 +30942,7 @@ }, "ComposioGmailAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -30976,14 +31000,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -31571,7 +31595,7 @@ }, "ComposioGoogleBigQueryAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -31629,14 +31653,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -32224,7 +32248,7 @@ }, "ComposioGoogleCalendarAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -32282,14 +32306,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -32877,7 +32901,7 @@ }, "ComposioGoogleDocsAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -32935,14 +32959,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -33530,7 +33554,7 @@ }, "ComposioGoogleSheetsAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -33588,14 +33612,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -34183,7 +34207,7 @@ }, "ComposioGoogleTasksAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -34241,14 +34265,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -34836,7 +34860,7 @@ }, "ComposioGoogleclassroomAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -34894,14 +34918,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -35489,7 +35513,7 @@ }, "ComposioGooglemeetAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -35547,14 +35571,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -36142,7 +36166,7 @@ }, "ComposioHeygenAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -36200,14 +36224,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -36795,7 +36819,7 @@ }, "ComposioInstagramAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -36853,14 +36877,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -37448,7 +37472,7 @@ }, "ComposioJiraAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -37506,14 +37530,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -38101,7 +38125,7 @@ }, "ComposioJotformAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -38159,14 +38183,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -38754,7 +38778,7 @@ }, "ComposioKlaviyoAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -38812,14 +38836,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -39407,7 +39431,7 @@ }, "ComposioLinearAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -39465,14 +39489,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -40060,7 +40084,7 @@ }, "ComposioListennotesAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -40118,14 +40142,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -40713,7 +40737,7 @@ }, "ComposioMem0APIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -40771,14 +40795,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -41366,7 +41390,7 @@ }, "ComposioMiroAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -41424,14 +41448,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -42019,7 +42043,7 @@ }, "ComposioMissiveAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -42077,14 +42101,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -42672,7 +42696,7 @@ }, "ComposioNotionAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -42730,14 +42754,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -43325,7 +43349,7 @@ }, "ComposioOneDriveAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -43383,14 +43407,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -43978,7 +44002,7 @@ }, "ComposioOutlookAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -44036,14 +44060,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -44631,7 +44655,7 @@ }, "ComposioPandadocAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -44689,14 +44713,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -45284,7 +45308,7 @@ }, "ComposioPeopleDataLabsAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -45342,14 +45366,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -45937,7 +45961,7 @@ }, "ComposioPerplexityAIAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -45995,14 +46019,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -46590,7 +46614,7 @@ }, "ComposioRedditAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -46648,14 +46672,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -47243,7 +47267,7 @@ }, "ComposioSerpAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -47301,14 +47325,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -47896,7 +47920,7 @@ }, "ComposioSlackAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -47954,14 +47978,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -48549,7 +48573,7 @@ }, "ComposioSlackbotAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -48607,14 +48631,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -49202,7 +49226,7 @@ }, "ComposioSnowflakeAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -49260,14 +49284,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -49855,7 +49879,7 @@ }, "ComposioSupabaseAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -49913,14 +49937,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -50508,7 +50532,7 @@ }, "ComposioTavilyAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -50566,14 +50590,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -51161,7 +51185,7 @@ }, "ComposioTimelinesAIAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -51219,14 +51243,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -51814,7 +51838,7 @@ }, "ComposioTodoistAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -51872,14 +51896,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -52467,7 +52491,7 @@ }, "ComposioWrikeAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -52525,14 +52549,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -53120,7 +53144,7 @@ }, "ComposioYoutubeAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -53178,14 +53202,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataFrame", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -53778,7 +53802,7 @@ { "Confluence": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -53800,7 +53824,7 @@ "icon": "Confluence", "legacy": false, "metadata": { - "code_hash": "8a7ef34b66e4", + "code_hash": "d669f422824e", "dependencies": { "dependencies": [ { @@ -53822,14 +53846,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "load_documents", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -53892,7 +53916,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from langchain_community.document_loaders import ConfluenceLoader\nfrom langchain_community.document_loaders.confluence import ContentFormat\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, IntInput, Output, SecretStrInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass ConfluenceComponent(Component):\n display_name = \"Confluence\"\n description = \"Confluence wiki collaboration platform\"\n documentation = \"https://python.langchain.com/v0.2/docs/integrations/document_loaders/confluence/\"\n trace_type = \"tool\"\n icon = \"Confluence\"\n name = \"Confluence\"\n\n inputs = [\n StrInput(\n name=\"url\",\n display_name=\"Site URL\",\n required=True,\n info=\"The base URL of the Confluence Space. Example: https://.atlassian.net/wiki.\",\n ),\n StrInput(\n name=\"username\",\n display_name=\"Username\",\n required=True,\n info=\"Atlassian User E-mail. Example: email@example.com\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Confluence API Key\",\n required=True,\n info=\"Atlassian Key. Create at: https://id.atlassian.com/manage-profile/security/api-tokens\",\n ),\n StrInput(name=\"space_key\", display_name=\"Space Key\", required=True),\n BoolInput(name=\"cloud\", display_name=\"Use Cloud?\", required=True, value=True, advanced=True),\n DropdownInput(\n name=\"content_format\",\n display_name=\"Content Format\",\n options=[\n ContentFormat.EDITOR.value,\n ContentFormat.EXPORT_VIEW.value,\n ContentFormat.ANONYMOUS_EXPORT_VIEW.value,\n ContentFormat.STORAGE.value,\n ContentFormat.VIEW.value,\n ],\n value=ContentFormat.STORAGE.value,\n required=True,\n advanced=True,\n info=\"Specify content format, defaults to ContentFormat.STORAGE\",\n ),\n IntInput(\n name=\"max_pages\",\n display_name=\"Max Pages\",\n required=False,\n value=1000,\n advanced=True,\n info=\"Maximum number of pages to retrieve in total, defaults 1000\",\n ),\n ]\n\n outputs = [\n Output(name=\"data\", display_name=\"Data\", method=\"load_documents\"),\n ]\n\n def build_confluence(self) -> ConfluenceLoader:\n content_format = ContentFormat(self.content_format)\n return ConfluenceLoader(\n url=self.url,\n username=self.username,\n api_key=self.api_key,\n cloud=self.cloud,\n space_key=self.space_key,\n content_format=content_format,\n max_pages=self.max_pages,\n )\n\n def load_documents(self) -> list[Data]:\n confluence = self.build_confluence()\n documents = confluence.load()\n data = [Data.from_document(doc) for doc in documents] # Using the from_document method of Data\n self.status = data\n return data\n" + "value": "from langchain_community.document_loaders import ConfluenceLoader\nfrom langchain_community.document_loaders.confluence import ContentFormat\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, IntInput, Output, SecretStrInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass ConfluenceComponent(Component):\n display_name = \"Confluence\"\n description = \"Confluence wiki collaboration platform\"\n documentation = \"https://python.langchain.com/v0.2/docs/integrations/document_loaders/confluence/\"\n trace_type = \"tool\"\n icon = \"Confluence\"\n name = \"Confluence\"\n\n inputs = [\n StrInput(\n name=\"url\",\n display_name=\"Site URL\",\n required=True,\n info=\"The base URL of the Confluence Space. Example: https://.atlassian.net/wiki.\",\n ),\n StrInput(\n name=\"username\",\n display_name=\"Username\",\n required=True,\n info=\"Atlassian User E-mail. Example: email@example.com\",\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Confluence API Key\",\n required=True,\n info=\"Atlassian Key. Create at: https://id.atlassian.com/manage-profile/security/api-tokens\",\n ),\n StrInput(name=\"space_key\", display_name=\"Space Key\", required=True),\n BoolInput(name=\"cloud\", display_name=\"Use Cloud?\", required=True, value=True, advanced=True),\n DropdownInput(\n name=\"content_format\",\n display_name=\"Content Format\",\n options=[\n ContentFormat.EDITOR.value,\n ContentFormat.EXPORT_VIEW.value,\n ContentFormat.ANONYMOUS_EXPORT_VIEW.value,\n ContentFormat.STORAGE.value,\n ContentFormat.VIEW.value,\n ],\n value=ContentFormat.STORAGE.value,\n required=True,\n advanced=True,\n info=\"Specify content format, defaults to ContentFormat.STORAGE\",\n ),\n IntInput(\n name=\"max_pages\",\n display_name=\"Max Pages\",\n required=False,\n value=1000,\n advanced=True,\n info=\"Maximum number of pages to retrieve in total, defaults 1000\",\n ),\n ]\n\n outputs = [\n Output(name=\"data\", display_name=\"JSON\", method=\"load_documents\"),\n ]\n\n def build_confluence(self) -> ConfluenceLoader:\n content_format = ContentFormat(self.content_format)\n return ConfluenceLoader(\n url=self.url,\n username=self.username,\n api_key=self.api_key,\n cloud=self.cloud,\n space_key=self.space_key,\n content_format=content_format,\n max_pages=self.max_pages,\n )\n\n def load_documents(self) -> list[Data]:\n confluence = self.build_confluence()\n documents = confluence.load()\n data = [Data.from_document(doc) for doc in documents] # Using the from_document method of Data\n self.status = data\n return data\n" }, "content_format": { "_input_type": "DropdownInput", @@ -54017,8 +54041,8 @@ { "Couchbase": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -54075,24 +54099,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -56730,7 +56754,7 @@ { "CustomComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -56768,10 +56792,10 @@ "group_outputs": false, "method": "build_output", "name": "output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -56832,7 +56856,7 @@ { "APIRequest": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -56858,7 +56882,7 @@ "icon": "Globe", "legacy": false, "metadata": { - "code_hash": "f102aadfb328", + "code_hash": "2af407885294", "dependencies": { "dependencies": [ { @@ -56892,10 +56916,10 @@ "group_outputs": false, "method": "make_api_request", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -56910,7 +56934,8 @@ "dynamic": false, "info": "The body to send with the request as a dictionary (for POST, PATCH, PUT).", "input_types": [ - "Data" + "Data", + "JSON" ], "is_list": true, "list_add_label": "Add More", @@ -56959,7 +56984,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nimport re\nimport tempfile\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import parse_qsl, urlencode, urlparse, urlunparse\n\nimport aiofiles\nimport aiofiles.os as aiofiles_os\nimport httpx\nimport validators\n\nfrom lfx.base.curl.parse import parse_context\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import TabInput\nfrom lfx.io import (\n BoolInput,\n DataInput,\n DropdownInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n Output,\n TableInput,\n)\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.component_utils import set_current_fields, set_field_advanced, set_field_display\nfrom lfx.utils.ssrf_protection import SSRFProtectionError, validate_url_for_ssrf\n\n# Define fields for each mode\nMODE_FIELDS = {\n \"URL\": [\n \"url_input\",\n \"method\",\n ],\n \"cURL\": [\"curl_input\"],\n}\n\n# Fields that should always be visible\nDEFAULT_FIELDS = [\"mode\"]\n\n\nclass APIRequestComponent(Component):\n display_name = \"API Request\"\n description = \"Make HTTP requests using URL or cURL commands.\"\n documentation: str = \"https://docs.langflow.org/api-request\"\n icon = \"Globe\"\n name = \"APIRequest\"\n\n inputs = [\n MessageTextInput(\n name=\"url_input\",\n display_name=\"URL\",\n info=\"Enter the URL for the request.\",\n advanced=False,\n tool_mode=True,\n ),\n MultilineInput(\n name=\"curl_input\",\n display_name=\"cURL\",\n info=(\n \"Paste a curl command to populate the fields. \"\n \"This will fill in the dictionary fields for headers and body.\"\n ),\n real_time_refresh=True,\n tool_mode=True,\n advanced=True,\n show=False,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Method\",\n options=[\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"],\n value=\"GET\",\n info=\"The HTTP method to use.\",\n real_time_refresh=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"URL\", \"cURL\"],\n value=\"URL\",\n info=\"Enable cURL mode to populate fields from a cURL command.\",\n real_time_refresh=True,\n ),\n DataInput(\n name=\"query_params\",\n display_name=\"Query Parameters\",\n info=\"The query parameters to append to the URL.\",\n advanced=True,\n ),\n TableInput(\n name=\"body\",\n display_name=\"Body\",\n info=\"The body to send with the request as a dictionary (for POST, PATCH, PUT).\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Parameter name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"description\": \"Parameter value\",\n },\n ],\n value=[],\n input_types=[\"Data\"],\n advanced=True,\n real_time_refresh=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": \"Langflow/1.0\"}],\n advanced=True,\n input_types=[\"Data\"],\n real_time_refresh=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n value=30,\n info=\"The timeout to use for the request.\",\n advanced=True,\n ),\n BoolInput(\n name=\"follow_redirects\",\n display_name=\"Follow Redirects\",\n value=False,\n info=(\n \"Whether to follow HTTP redirects. \"\n \"WARNING: Enabling redirects may allow SSRF bypass attacks where a public URL \"\n \"redirects to internal resources. Only enable if you trust the target server. \"\n \"See OWASP SSRF Prevention Cheat Sheet for details.\"\n ),\n advanced=True,\n ),\n BoolInput(\n name=\"save_to_file\",\n display_name=\"Save to File\",\n value=False,\n info=\"Save the API response to a temporary file\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_httpx_metadata\",\n display_name=\"Include HTTPx Metadata\",\n value=False,\n info=(\n \"Include properties such as headers, status_code, response_headers, \"\n \"and redirection_history in the output.\"\n ),\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"API Response\", name=\"data\", method=\"make_api_request\"),\n ]\n\n def _parse_json_value(self, value: Any) -> Any:\n \"\"\"Parse a value that might be a JSON string.\"\"\"\n if not isinstance(value, str):\n return value\n\n try:\n parsed = json.loads(value)\n except json.JSONDecodeError:\n return value\n else:\n return parsed\n\n def _process_body(self, body: Any) -> dict:\n \"\"\"Process the body input into a valid dictionary.\"\"\"\n if body is None:\n return {}\n if hasattr(body, \"data\"):\n body = body.data\n if isinstance(body, dict):\n return self._process_dict_body(body)\n if isinstance(body, str):\n return self._process_string_body(body)\n if isinstance(body, list):\n return self._process_list_body(body)\n return {}\n\n def _process_dict_body(self, body: dict) -> dict:\n \"\"\"Process dictionary body by parsing JSON values.\"\"\"\n return {k: self._parse_json_value(v) for k, v in body.items()}\n\n def _process_string_body(self, body: str) -> dict:\n \"\"\"Process string body by attempting JSON parse.\"\"\"\n try:\n return self._process_body(json.loads(body))\n except json.JSONDecodeError:\n return {\"data\": body}\n\n def _process_list_body(self, body: list) -> dict:\n \"\"\"Process list body by converting to key-value dictionary.\"\"\"\n processed_dict = {}\n try:\n for item in body:\n # Unwrap Data objects\n current_item = item\n if hasattr(item, \"data\"):\n unwrapped_data = item.data\n # If the unwrapped data is a dict but not key-value format, use it directly\n if isinstance(unwrapped_data, dict) and not self._is_valid_key_value_item(unwrapped_data):\n return unwrapped_data\n current_item = unwrapped_data\n if not self._is_valid_key_value_item(current_item):\n continue\n key = current_item[\"key\"]\n value = self._parse_json_value(current_item[\"value\"])\n processed_dict[key] = value\n except (KeyError, TypeError, ValueError) as e:\n self.log(f\"Failed to process body list: {e}\")\n return {}\n return processed_dict\n\n def _is_valid_key_value_item(self, item: Any) -> bool:\n \"\"\"Check if an item is a valid key-value dictionary.\"\"\"\n return isinstance(item, dict) and \"key\" in item and \"value\" in item\n\n def parse_curl(self, curl: str, build_config: dotdict) -> dotdict:\n \"\"\"Parse a cURL command and update build configuration.\"\"\"\n try:\n parsed = parse_context(curl)\n\n # Update basic configuration\n url = parsed.url\n # Normalize URL before setting it\n url = self._normalize_url(url)\n\n build_config[\"url_input\"][\"value\"] = url\n build_config[\"method\"][\"value\"] = parsed.method.upper()\n\n # Process headers\n headers_list = [{\"key\": k, \"value\": v} for k, v in parsed.headers.items()]\n build_config[\"headers\"][\"value\"] = headers_list\n\n # Process body data\n if not parsed.data:\n build_config[\"body\"][\"value\"] = []\n elif parsed.data:\n try:\n json_data = json.loads(parsed.data)\n if isinstance(json_data, dict):\n body_list = [\n {\"key\": k, \"value\": json.dumps(v) if isinstance(v, dict | list) else str(v)}\n for k, v in json_data.items()\n ]\n build_config[\"body\"][\"value\"] = body_list\n else:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": json.dumps(json_data)}]\n except json.JSONDecodeError:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": parsed.data}]\n\n except Exception as exc:\n msg = f\"Error parsing curl: {exc}\"\n self.log(msg)\n raise ValueError(msg) from exc\n\n return build_config\n\n def _normalize_url(self, url: str) -> str:\n \"\"\"Normalize URL by adding https:// if no protocol is specified.\"\"\"\n if not url or not isinstance(url, str):\n msg = \"URL cannot be empty\"\n raise ValueError(msg)\n\n url = url.strip()\n if url.startswith((\"http://\", \"https://\")):\n return url\n return f\"https://{url}\"\n\n async def make_request(\n self,\n client: httpx.AsyncClient,\n method: str,\n url: str,\n headers: dict | None = None,\n body: Any = None,\n timeout: int = 5,\n *,\n follow_redirects: bool = True,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n method = method.upper()\n if method not in {\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"}:\n msg = f\"Unsupported method: {method}\"\n raise ValueError(msg)\n\n processed_body = self._process_body(body)\n redirection_history = []\n\n try:\n # Prepare request parameters\n request_params = {\n \"method\": method,\n \"url\": url,\n \"headers\": headers,\n \"timeout\": timeout,\n \"follow_redirects\": follow_redirects,\n }\n # Only include body for methods that support it (GET must not have a body per HTTP spec)\n if method in {\"POST\", \"PATCH\", \"PUT\", \"DELETE\"} and processed_body is not None:\n request_params[\"json\"] = processed_body\n response = await client.request(**request_params)\n\n redirection_history = [\n {\n \"url\": redirect.headers.get(\"Location\", str(redirect.url)),\n \"status_code\": redirect.status_code,\n }\n for redirect in response.history\n ]\n\n is_binary, file_path = await self._response_info(response, with_file_path=save_to_file)\n response_headers = self._headers_to_dict(response.headers)\n\n # Base metadata\n metadata = {\n \"source\": url,\n \"status_code\": response.status_code,\n \"response_headers\": response_headers,\n }\n\n if redirection_history:\n metadata[\"redirection_history\"] = redirection_history\n\n if save_to_file:\n mode = \"wb\" if is_binary else \"w\"\n encoding = response.encoding if mode == \"w\" else None\n if file_path:\n await aiofiles_os.makedirs(file_path.parent, exist_ok=True)\n if is_binary:\n async with aiofiles.open(file_path, \"wb\") as f:\n await f.write(response.content)\n await f.flush()\n else:\n async with aiofiles.open(file_path, \"w\", encoding=encoding) as f:\n await f.write(response.text)\n await f.flush()\n metadata[\"file_path\"] = str(file_path)\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n return Data(data=metadata)\n\n # Handle response content\n if is_binary:\n result = response.content\n else:\n try:\n result = response.json()\n except json.JSONDecodeError:\n self.log(\"Failed to decode JSON response\")\n result = response.text.encode(\"utf-8\")\n\n metadata[\"result\"] = result\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n\n return Data(data=metadata)\n except (httpx.HTTPError, httpx.RequestError, httpx.TimeoutException) as exc:\n self.log(f\"Error making request to {url}\")\n return Data(\n data={\n \"source\": url,\n \"headers\": headers,\n \"status_code\": 500,\n \"error\": str(exc),\n **({\"redirection_history\": redirection_history} if redirection_history else {}),\n },\n )\n\n def add_query_params(self, url: str, params: dict) -> str:\n \"\"\"Add query parameters to URL efficiently.\"\"\"\n if not params:\n return url\n url_parts = list(urlparse(url))\n query = dict(parse_qsl(url_parts[4]))\n query.update(params)\n url_parts[4] = urlencode(query)\n return urlunparse(url_parts)\n\n def _headers_to_dict(self, headers: httpx.Headers) -> dict[str, str]:\n \"\"\"Convert HTTP headers to a dictionary with lowercased keys.\"\"\"\n return {k.lower(): v for k, v in headers.items()}\n\n def _process_headers(self, headers: Any) -> dict:\n \"\"\"Process the headers input into a valid dictionary.\"\"\"\n if headers is None:\n return {}\n if isinstance(headers, dict):\n return headers\n if isinstance(headers, list):\n return {item[\"key\"]: item[\"value\"] for item in headers if self._is_valid_key_value_item(item)}\n return {}\n\n async def make_api_request(self) -> Data:\n \"\"\"Make HTTP request with optimized parameter handling.\"\"\"\n method = self.method\n url = self.url_input.strip() if isinstance(self.url_input, str) else \"\"\n headers = self.headers or {}\n body = self.body or {}\n timeout = self.timeout\n follow_redirects = self.follow_redirects\n save_to_file = self.save_to_file\n include_httpx_metadata = self.include_httpx_metadata\n\n # Security warning when redirects are enabled\n if follow_redirects:\n self.log(\n \"Security Warning: HTTP redirects are enabled. This may allow SSRF bypass attacks \"\n \"where a public URL redirects to internal resources (e.g., cloud metadata endpoints). \"\n \"Only enable this if you trust the target server.\"\n )\n\n # if self.mode == \"cURL\" and self.curl_input:\n # self._build_config = self.parse_curl(self.curl_input, dotdict())\n # # After parsing curl, get the normalized URL\n # url = self._build_config[\"url_input\"][\"value\"]\n\n # Normalize URL before validation\n url = self._normalize_url(url)\n\n # Validate URL\n if not validators.url(url):\n msg = f\"Invalid URL provided: {url}\"\n raise ValueError(msg)\n\n # SSRF Protection: Validate URL to prevent access to internal resources\n # TODO: In next major version (2.0), remove warn_only=True to enforce blocking\n try:\n validate_url_for_ssrf(url, warn_only=True)\n except SSRFProtectionError as e:\n # This will only raise if SSRF protection is enabled and warn_only=False\n msg = f\"SSRF Protection: {e}\"\n raise ValueError(msg) from e\n\n # Process query parameters\n if isinstance(self.query_params, str):\n query_params = dict(parse_qsl(self.query_params))\n else:\n query_params = self.query_params.data if self.query_params else {}\n\n # Process headers and body\n headers = self._process_headers(headers)\n body = self._process_body(body)\n url = self.add_query_params(url, query_params)\n\n async with httpx.AsyncClient() as client:\n result = await self.make_request(\n client,\n method,\n url,\n headers,\n body,\n timeout,\n follow_redirects=follow_redirects,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n self.status = result\n return result\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n \"\"\"Update the build config based on the selected mode.\"\"\"\n if field_name != \"mode\":\n if field_name == \"curl_input\" and self.mode == \"cURL\" and self.curl_input:\n return self.parse_curl(self.curl_input, build_config)\n return build_config\n\n if field_value == \"cURL\":\n set_field_display(build_config, \"curl_input\", value=True)\n if build_config[\"curl_input\"][\"value\"]:\n try:\n build_config = self.parse_curl(build_config[\"curl_input\"][\"value\"], build_config)\n except ValueError as e:\n self.log(f\"Failed to parse cURL input: {e}\")\n else:\n set_field_display(build_config, \"curl_input\", value=False)\n\n return set_current_fields(\n build_config=build_config,\n action_fields=MODE_FIELDS,\n selected_action=field_value,\n default_fields=DEFAULT_FIELDS,\n func=set_field_advanced,\n default_value=True,\n )\n\n async def _response_info(\n self, response: httpx.Response, *, with_file_path: bool = False\n ) -> tuple[bool, Path | None]:\n \"\"\"Determine the file path and whether the response content is binary.\n\n Args:\n response (Response): The HTTP response object.\n with_file_path (bool): Whether to save the response content to a file.\n\n Returns:\n Tuple[bool, Path | None]:\n A tuple containing a boolean indicating if the content is binary and the full file path (if applicable).\n \"\"\"\n content_type = response.headers.get(\"Content-Type\", \"\")\n is_binary = \"application/octet-stream\" in content_type or \"application/binary\" in content_type\n\n if not with_file_path:\n return is_binary, None\n\n component_temp_dir = Path(tempfile.gettempdir()) / self.__class__.__name__\n\n # Create directory asynchronously\n await aiofiles_os.makedirs(component_temp_dir, exist_ok=True)\n\n filename = None\n if \"Content-Disposition\" in response.headers:\n content_disposition = response.headers[\"Content-Disposition\"]\n filename_match = re.search(r'filename=\"(.+?)\"', content_disposition)\n if filename_match:\n extracted_filename = filename_match.group(1)\n filename = extracted_filename\n\n # Step 3: Infer file extension or use part of the request URL if no filename\n if not filename:\n # Extract the last segment of the URL path\n url_path = urlparse(str(response.request.url) if response.request else \"\").path\n base_name = Path(url_path).name # Get the last segment of the path\n if not base_name: # If the path ends with a slash or is empty\n base_name = \"response\"\n\n # Infer file extension\n content_type_to_extension = {\n \"text/plain\": \".txt\",\n \"application/json\": \".json\",\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"application/octet-stream\": \".bin\",\n }\n extension = content_type_to_extension.get(content_type, \".bin\" if is_binary else \".txt\")\n filename = f\"{base_name}{extension}\"\n\n # Step 4: Define the full file path\n file_path = component_temp_dir / filename\n\n # Step 5: Check if file exists asynchronously and handle accordingly\n try:\n # Try to create the file exclusively (x mode) to check existence\n async with aiofiles.open(file_path, \"x\") as _:\n pass # File created successfully, we can use this path\n except FileExistsError:\n # If file exists, append a timestamp to the filename\n timestamp = datetime.now(timezone.utc).strftime(\"%Y%m%d%H%M%S%f\")\n file_path = component_temp_dir / f\"{timestamp}-{filename}\"\n\n return is_binary, file_path\n" + "value": "import json\nimport re\nimport tempfile\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import parse_qsl, urlencode, urlparse, urlunparse\n\nimport aiofiles\nimport aiofiles.os as aiofiles_os\nimport httpx\nimport validators\n\nfrom lfx.base.curl.parse import parse_context\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import TabInput\nfrom lfx.io import (\n BoolInput,\n DataInput,\n DropdownInput,\n IntInput,\n MessageTextInput,\n MultilineInput,\n Output,\n TableInput,\n)\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.component_utils import set_current_fields, set_field_advanced, set_field_display\nfrom lfx.utils.ssrf_protection import SSRFProtectionError, validate_url_for_ssrf\n\n# Define fields for each mode\nMODE_FIELDS = {\n \"URL\": [\n \"url_input\",\n \"method\",\n ],\n \"cURL\": [\"curl_input\"],\n}\n\n# Fields that should always be visible\nDEFAULT_FIELDS = [\"mode\"]\n\n\nclass APIRequestComponent(Component):\n display_name = \"API Request\"\n description = \"Make HTTP requests using URL or cURL commands.\"\n documentation: str = \"https://docs.langflow.org/api-request\"\n icon = \"Globe\"\n name = \"APIRequest\"\n\n inputs = [\n MessageTextInput(\n name=\"url_input\",\n display_name=\"URL\",\n info=\"Enter the URL for the request.\",\n advanced=False,\n tool_mode=True,\n ),\n MultilineInput(\n name=\"curl_input\",\n display_name=\"cURL\",\n info=(\n \"Paste a curl command to populate the fields. \"\n \"This will fill in the dictionary fields for headers and body.\"\n ),\n real_time_refresh=True,\n tool_mode=True,\n advanced=True,\n show=False,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Method\",\n options=[\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"],\n value=\"GET\",\n info=\"The HTTP method to use.\",\n real_time_refresh=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"URL\", \"cURL\"],\n value=\"URL\",\n info=\"Enable cURL mode to populate fields from a cURL command.\",\n real_time_refresh=True,\n ),\n DataInput(\n name=\"query_params\",\n display_name=\"Query Parameters\",\n info=\"The query parameters to append to the URL.\",\n advanced=True,\n ),\n TableInput(\n name=\"body\",\n display_name=\"Body\",\n info=\"The body to send with the request as a dictionary (for POST, PATCH, PUT).\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Parameter name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"description\": \"Parameter value\",\n },\n ],\n value=[],\n input_types=[\"Data\", \"JSON\"],\n advanced=True,\n real_time_refresh=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": \"Langflow/1.0\"}],\n advanced=True,\n input_types=[\"Data\", \"JSON\"],\n real_time_refresh=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n value=30,\n info=\"The timeout to use for the request.\",\n advanced=True,\n ),\n BoolInput(\n name=\"follow_redirects\",\n display_name=\"Follow Redirects\",\n value=False,\n info=(\n \"Whether to follow HTTP redirects. \"\n \"WARNING: Enabling redirects may allow SSRF bypass attacks where a public URL \"\n \"redirects to internal resources. Only enable if you trust the target server. \"\n \"See OWASP SSRF Prevention Cheat Sheet for details.\"\n ),\n advanced=True,\n ),\n BoolInput(\n name=\"save_to_file\",\n display_name=\"Save to File\",\n value=False,\n info=\"Save the API response to a temporary file\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_httpx_metadata\",\n display_name=\"Include HTTPx Metadata\",\n value=False,\n info=(\n \"Include properties such as headers, status_code, response_headers, \"\n \"and redirection_history in the output.\"\n ),\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"API Response\", name=\"data\", method=\"make_api_request\"),\n ]\n\n def _parse_json_value(self, value: Any) -> Any:\n \"\"\"Parse a value that might be a JSON string.\"\"\"\n if not isinstance(value, str):\n return value\n\n try:\n parsed = json.loads(value)\n except json.JSONDecodeError:\n return value\n else:\n return parsed\n\n def _process_body(self, body: Any) -> dict:\n \"\"\"Process the body input into a valid dictionary.\"\"\"\n if body is None:\n return {}\n if hasattr(body, \"data\"):\n body = body.data\n if isinstance(body, dict):\n return self._process_dict_body(body)\n if isinstance(body, str):\n return self._process_string_body(body)\n if isinstance(body, list):\n return self._process_list_body(body)\n return {}\n\n def _process_dict_body(self, body: dict) -> dict:\n \"\"\"Process dictionary body by parsing JSON values.\"\"\"\n return {k: self._parse_json_value(v) for k, v in body.items()}\n\n def _process_string_body(self, body: str) -> dict:\n \"\"\"Process string body by attempting JSON parse.\"\"\"\n try:\n return self._process_body(json.loads(body))\n except json.JSONDecodeError:\n return {\"data\": body}\n\n def _process_list_body(self, body: list) -> dict:\n \"\"\"Process list body by converting to key-value dictionary.\"\"\"\n processed_dict = {}\n try:\n for item in body:\n # Unwrap Data objects\n current_item = item\n if hasattr(item, \"data\"):\n unwrapped_data = item.data\n # If the unwrapped data is a dict but not key-value format, use it directly\n if isinstance(unwrapped_data, dict) and not self._is_valid_key_value_item(unwrapped_data):\n return unwrapped_data\n current_item = unwrapped_data\n if not self._is_valid_key_value_item(current_item):\n continue\n key = current_item[\"key\"]\n value = self._parse_json_value(current_item[\"value\"])\n processed_dict[key] = value\n except (KeyError, TypeError, ValueError) as e:\n self.log(f\"Failed to process body list: {e}\")\n return {}\n return processed_dict\n\n def _is_valid_key_value_item(self, item: Any) -> bool:\n \"\"\"Check if an item is a valid key-value dictionary.\"\"\"\n return isinstance(item, dict) and \"key\" in item and \"value\" in item\n\n def parse_curl(self, curl: str, build_config: dotdict) -> dotdict:\n \"\"\"Parse a cURL command and update build configuration.\"\"\"\n try:\n parsed = parse_context(curl)\n\n # Update basic configuration\n url = parsed.url\n # Normalize URL before setting it\n url = self._normalize_url(url)\n\n build_config[\"url_input\"][\"value\"] = url\n build_config[\"method\"][\"value\"] = parsed.method.upper()\n\n # Process headers\n headers_list = [{\"key\": k, \"value\": v} for k, v in parsed.headers.items()]\n build_config[\"headers\"][\"value\"] = headers_list\n\n # Process body data\n if not parsed.data:\n build_config[\"body\"][\"value\"] = []\n elif parsed.data:\n try:\n json_data = json.loads(parsed.data)\n if isinstance(json_data, dict):\n body_list = [\n {\"key\": k, \"value\": json.dumps(v) if isinstance(v, dict | list) else str(v)}\n for k, v in json_data.items()\n ]\n build_config[\"body\"][\"value\"] = body_list\n else:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": json.dumps(json_data)}]\n except json.JSONDecodeError:\n build_config[\"body\"][\"value\"] = [{\"key\": \"data\", \"value\": parsed.data}]\n\n except Exception as exc:\n msg = f\"Error parsing curl: {exc}\"\n self.log(msg)\n raise ValueError(msg) from exc\n\n return build_config\n\n def _normalize_url(self, url: str) -> str:\n \"\"\"Normalize URL by adding https:// if no protocol is specified.\"\"\"\n if not url or not isinstance(url, str):\n msg = \"URL cannot be empty\"\n raise ValueError(msg)\n\n url = url.strip()\n if url.startswith((\"http://\", \"https://\")):\n return url\n return f\"https://{url}\"\n\n async def make_request(\n self,\n client: httpx.AsyncClient,\n method: str,\n url: str,\n headers: dict | None = None,\n body: Any = None,\n timeout: int = 5,\n *,\n follow_redirects: bool = True,\n save_to_file: bool = False,\n include_httpx_metadata: bool = False,\n ) -> Data:\n method = method.upper()\n if method not in {\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"}:\n msg = f\"Unsupported method: {method}\"\n raise ValueError(msg)\n\n processed_body = self._process_body(body)\n redirection_history = []\n\n try:\n # Prepare request parameters\n request_params = {\n \"method\": method,\n \"url\": url,\n \"headers\": headers,\n \"timeout\": timeout,\n \"follow_redirects\": follow_redirects,\n }\n # Only include body for methods that support it (GET must not have a body per HTTP spec)\n if method in {\"POST\", \"PATCH\", \"PUT\", \"DELETE\"} and processed_body is not None:\n request_params[\"json\"] = processed_body\n response = await client.request(**request_params)\n\n redirection_history = [\n {\n \"url\": redirect.headers.get(\"Location\", str(redirect.url)),\n \"status_code\": redirect.status_code,\n }\n for redirect in response.history\n ]\n\n is_binary, file_path = await self._response_info(response, with_file_path=save_to_file)\n response_headers = self._headers_to_dict(response.headers)\n\n # Base metadata\n metadata = {\n \"source\": url,\n \"status_code\": response.status_code,\n \"response_headers\": response_headers,\n }\n\n if redirection_history:\n metadata[\"redirection_history\"] = redirection_history\n\n if save_to_file:\n mode = \"wb\" if is_binary else \"w\"\n encoding = response.encoding if mode == \"w\" else None\n if file_path:\n await aiofiles_os.makedirs(file_path.parent, exist_ok=True)\n if is_binary:\n async with aiofiles.open(file_path, \"wb\") as f:\n await f.write(response.content)\n await f.flush()\n else:\n async with aiofiles.open(file_path, \"w\", encoding=encoding) as f:\n await f.write(response.text)\n await f.flush()\n metadata[\"file_path\"] = str(file_path)\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n return Data(data=metadata)\n\n # Handle response content\n if is_binary:\n result = response.content\n else:\n try:\n result = response.json()\n except json.JSONDecodeError:\n self.log(\"Failed to decode JSON response\")\n result = response.text.encode(\"utf-8\")\n\n metadata[\"result\"] = result\n\n if include_httpx_metadata:\n metadata.update({\"headers\": headers})\n\n return Data(data=metadata)\n except (httpx.HTTPError, httpx.RequestError, httpx.TimeoutException) as exc:\n self.log(f\"Error making request to {url}\")\n return Data(\n data={\n \"source\": url,\n \"headers\": headers,\n \"status_code\": 500,\n \"error\": str(exc),\n **({\"redirection_history\": redirection_history} if redirection_history else {}),\n },\n )\n\n def add_query_params(self, url: str, params: dict) -> str:\n \"\"\"Add query parameters to URL efficiently.\"\"\"\n if not params:\n return url\n url_parts = list(urlparse(url))\n query = dict(parse_qsl(url_parts[4]))\n query.update(params)\n url_parts[4] = urlencode(query)\n return urlunparse(url_parts)\n\n def _headers_to_dict(self, headers: httpx.Headers) -> dict[str, str]:\n \"\"\"Convert HTTP headers to a dictionary with lowercased keys.\"\"\"\n return {k.lower(): v for k, v in headers.items()}\n\n def _process_headers(self, headers: Any) -> dict:\n \"\"\"Process the headers input into a valid dictionary.\"\"\"\n if headers is None:\n return {}\n if isinstance(headers, dict):\n return headers\n if isinstance(headers, list):\n return {item[\"key\"]: item[\"value\"] for item in headers if self._is_valid_key_value_item(item)}\n return {}\n\n async def make_api_request(self) -> Data:\n \"\"\"Make HTTP request with optimized parameter handling.\"\"\"\n method = self.method\n url = self.url_input.strip() if isinstance(self.url_input, str) else \"\"\n headers = self.headers or {}\n body = self.body or {}\n timeout = self.timeout\n follow_redirects = self.follow_redirects\n save_to_file = self.save_to_file\n include_httpx_metadata = self.include_httpx_metadata\n\n # Security warning when redirects are enabled\n if follow_redirects:\n self.log(\n \"Security Warning: HTTP redirects are enabled. This may allow SSRF bypass attacks \"\n \"where a public URL redirects to internal resources (e.g., cloud metadata endpoints). \"\n \"Only enable this if you trust the target server.\"\n )\n\n # if self.mode == \"cURL\" and self.curl_input:\n # self._build_config = self.parse_curl(self.curl_input, dotdict())\n # # After parsing curl, get the normalized URL\n # url = self._build_config[\"url_input\"][\"value\"]\n\n # Normalize URL before validation\n url = self._normalize_url(url)\n\n # Validate URL\n if not validators.url(url):\n msg = f\"Invalid URL provided: {url}\"\n raise ValueError(msg)\n\n # SSRF Protection: Validate URL to prevent access to internal resources\n # TODO: In next major version (2.0), remove warn_only=True to enforce blocking\n try:\n validate_url_for_ssrf(url, warn_only=True)\n except SSRFProtectionError as e:\n # This will only raise if SSRF protection is enabled and warn_only=False\n msg = f\"SSRF Protection: {e}\"\n raise ValueError(msg) from e\n\n # Process query parameters\n if isinstance(self.query_params, str):\n query_params = dict(parse_qsl(self.query_params))\n else:\n query_params = self.query_params.data if self.query_params else {}\n\n # Process headers and body\n headers = self._process_headers(headers)\n body = self._process_body(body)\n url = self.add_query_params(url, query_params)\n\n async with httpx.AsyncClient() as client:\n result = await self.make_request(\n client,\n method,\n url,\n headers,\n body,\n timeout,\n follow_redirects=follow_redirects,\n save_to_file=save_to_file,\n include_httpx_metadata=include_httpx_metadata,\n )\n self.status = result\n return result\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n \"\"\"Update the build config based on the selected mode.\"\"\"\n if field_name != \"mode\":\n if field_name == \"curl_input\" and self.mode == \"cURL\" and self.curl_input:\n return self.parse_curl(self.curl_input, build_config)\n return build_config\n\n if field_value == \"cURL\":\n set_field_display(build_config, \"curl_input\", value=True)\n if build_config[\"curl_input\"][\"value\"]:\n try:\n build_config = self.parse_curl(build_config[\"curl_input\"][\"value\"], build_config)\n except ValueError as e:\n self.log(f\"Failed to parse cURL input: {e}\")\n else:\n set_field_display(build_config, \"curl_input\", value=False)\n\n return set_current_fields(\n build_config=build_config,\n action_fields=MODE_FIELDS,\n selected_action=field_value,\n default_fields=DEFAULT_FIELDS,\n func=set_field_advanced,\n default_value=True,\n )\n\n async def _response_info(\n self, response: httpx.Response, *, with_file_path: bool = False\n ) -> tuple[bool, Path | None]:\n \"\"\"Determine the file path and whether the response content is binary.\n\n Args:\n response (Response): The HTTP response object.\n with_file_path (bool): Whether to save the response content to a file.\n\n Returns:\n Tuple[bool, Path | None]:\n A tuple containing a boolean indicating if the content is binary and the full file path (if applicable).\n \"\"\"\n content_type = response.headers.get(\"Content-Type\", \"\")\n is_binary = \"application/octet-stream\" in content_type or \"application/binary\" in content_type\n\n if not with_file_path:\n return is_binary, None\n\n component_temp_dir = Path(tempfile.gettempdir()) / self.__class__.__name__\n\n # Create directory asynchronously\n await aiofiles_os.makedirs(component_temp_dir, exist_ok=True)\n\n filename = None\n if \"Content-Disposition\" in response.headers:\n content_disposition = response.headers[\"Content-Disposition\"]\n filename_match = re.search(r'filename=\"(.+?)\"', content_disposition)\n if filename_match:\n extracted_filename = filename_match.group(1)\n filename = extracted_filename\n\n # Step 3: Infer file extension or use part of the request URL if no filename\n if not filename:\n # Extract the last segment of the URL path\n url_path = urlparse(str(response.request.url) if response.request else \"\").path\n base_name = Path(url_path).name # Get the last segment of the path\n if not base_name: # If the path ends with a slash or is empty\n base_name = \"response\"\n\n # Infer file extension\n content_type_to_extension = {\n \"text/plain\": \".txt\",\n \"application/json\": \".json\",\n \"image/jpeg\": \".jpg\",\n \"image/png\": \".png\",\n \"application/octet-stream\": \".bin\",\n }\n extension = content_type_to_extension.get(content_type, \".bin\" if is_binary else \".txt\")\n filename = f\"{base_name}{extension}\"\n\n # Step 4: Define the full file path\n file_path = component_temp_dir / filename\n\n # Step 5: Check if file exists asynchronously and handle accordingly\n try:\n # Try to create the file exclusively (x mode) to check existence\n async with aiofiles.open(file_path, \"x\") as _:\n pass # File created successfully, we can use this path\n except FileExistsError:\n # If file exists, append a timestamp to the filename\n timestamp = datetime.now(timezone.utc).strftime(\"%Y%m%d%H%M%S%f\")\n file_path = component_temp_dir / f\"{timestamp}-{filename}\"\n\n return is_binary, file_path\n" }, "curl_input": { "_input_type": "MultilineInput", @@ -57018,7 +57043,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "Data" + "Data", + "JSON" ], "is_list": true, "list_add_label": "Add More", @@ -57132,13 +57158,14 @@ "value": "URL" }, "query_params": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": true, "display_name": "Query Parameters", "dynamic": false, "info": "The query parameters to append to the URL.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -57225,7 +57252,7 @@ }, "CSVtoData": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -57244,7 +57271,7 @@ "icon": "file-spreadsheet", "legacy": true, "metadata": { - "code_hash": "85c7d6df7473", + "code_hash": "049e2eeb6901", "dependencies": { "dependencies": [ { @@ -57262,14 +57289,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data List", + "display_name": "JSON List", "group_outputs": false, "method": "load_csv_to_data", "name": "data_list", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -57296,7 +57323,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import csv\nimport io\nfrom pathlib import Path\n\nfrom lfx.base.data.storage_utils import read_file_text\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import FileInput, MessageTextInput, MultilineInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.utils.async_helpers import run_until_complete\n\n\nclass CSVToDataComponent(Component):\n display_name = \"Load CSV\"\n description = \"Load a CSV file, CSV from a file path, or a valid CSV string and convert it to a list of Data\"\n icon = \"file-spreadsheet\"\n name = \"CSVtoData\"\n legacy = True\n replacement = [\"data.File\"]\n\n inputs = [\n FileInput(\n name=\"csv_file\",\n display_name=\"CSV File\",\n file_types=[\"csv\"],\n info=\"Upload a CSV file to convert to a list of Data objects\",\n ),\n MessageTextInput(\n name=\"csv_path\",\n display_name=\"CSV File Path\",\n info=\"Provide the path to the CSV file as pure text\",\n ),\n MultilineInput(\n name=\"csv_string\",\n display_name=\"CSV String\",\n info=\"Paste a CSV string directly to convert to a list of Data objects\",\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"The key to use for the text column. Defaults to 'text'.\",\n value=\"text\",\n ),\n ]\n\n outputs = [\n Output(name=\"data_list\", display_name=\"Data List\", method=\"load_csv_to_data\"),\n ]\n\n def load_csv_to_data(self) -> list[Data]:\n if sum(bool(field) for field in [self.csv_file, self.csv_path, self.csv_string]) != 1:\n msg = \"Please provide exactly one of: CSV file, file path, or CSV string.\"\n raise ValueError(msg)\n\n csv_data = None\n try:\n if self.csv_file:\n # FileInput always provides a local file path\n file_path = self.csv_file\n if not file_path.lower().endswith(\".csv\"):\n self.status = \"The provided file must be a CSV file.\"\n else:\n # Resolve to absolute path and read from local filesystem\n resolved_path = self.resolve_path(file_path)\n csv_bytes = Path(resolved_path).read_bytes()\n csv_data = csv_bytes.decode(\"utf-8\")\n\n elif self.csv_path:\n file_path = self.csv_path\n if not file_path.lower().endswith(\".csv\"):\n self.status = \"The provided path must be to a CSV file.\"\n else:\n csv_data = run_until_complete(\n read_file_text(file_path, encoding=\"utf-8\", resolve_path=self.resolve_path, newline=\"\")\n )\n\n else:\n csv_data = self.csv_string\n\n if csv_data:\n csv_reader = csv.DictReader(io.StringIO(csv_data))\n result = [Data(data=row, text_key=self.text_key) for row in csv_reader]\n\n if not result:\n self.status = \"The CSV data is empty.\"\n return []\n\n self.status = result\n return result\n\n except csv.Error as e:\n error_message = f\"CSV parsing error: {e}\"\n self.status = error_message\n raise ValueError(error_message) from e\n\n except Exception as e:\n error_message = f\"An error occurred: {e}\"\n self.status = error_message\n raise ValueError(error_message) from e\n\n # An error occurred\n raise ValueError(self.status)\n" + "value": "import csv\nimport io\nfrom pathlib import Path\n\nfrom lfx.base.data.storage_utils import read_file_text\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import FileInput, MessageTextInput, MultilineInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.utils.async_helpers import run_until_complete\n\n\nclass CSVToDataComponent(Component):\n display_name = \"Load CSV\"\n description = \"Load a CSV file, CSV from a file path, or a valid CSV string and convert it to a list of Data\"\n icon = \"file-spreadsheet\"\n name = \"CSVtoData\"\n legacy = True\n replacement = [\"data.File\"]\n\n inputs = [\n FileInput(\n name=\"csv_file\",\n display_name=\"CSV File\",\n file_types=[\"csv\"],\n info=\"Upload a CSV file to convert to a list of Data objects\",\n ),\n MessageTextInput(\n name=\"csv_path\",\n display_name=\"CSV File Path\",\n info=\"Provide the path to the CSV file as pure text\",\n ),\n MultilineInput(\n name=\"csv_string\",\n display_name=\"CSV String\",\n info=\"Paste a CSV string directly to convert to a list of Data objects\",\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"The key to use for the text column. Defaults to 'text'.\",\n value=\"text\",\n ),\n ]\n\n outputs = [\n Output(name=\"data_list\", display_name=\"JSON List\", method=\"load_csv_to_data\"),\n ]\n\n def load_csv_to_data(self) -> list[Data]:\n if sum(bool(field) for field in [self.csv_file, self.csv_path, self.csv_string]) != 1:\n msg = \"Please provide exactly one of: CSV file, file path, or CSV string.\"\n raise ValueError(msg)\n\n csv_data = None\n try:\n if self.csv_file:\n # FileInput always provides a local file path\n file_path = self.csv_file\n if not file_path.lower().endswith(\".csv\"):\n self.status = \"The provided file must be a CSV file.\"\n else:\n # Resolve to absolute path and read from local filesystem\n resolved_path = self.resolve_path(file_path)\n csv_bytes = Path(resolved_path).read_bytes()\n csv_data = csv_bytes.decode(\"utf-8\")\n\n elif self.csv_path:\n file_path = self.csv_path\n if not file_path.lower().endswith(\".csv\"):\n self.status = \"The provided path must be to a CSV file.\"\n else:\n csv_data = run_until_complete(\n read_file_text(file_path, encoding=\"utf-8\", resolve_path=self.resolve_path, newline=\"\")\n )\n\n else:\n csv_data = self.csv_string\n\n if csv_data:\n csv_reader = csv.DictReader(io.StringIO(csv_data))\n result = [Data(data=row, text_key=self.text_key) for row in csv_reader]\n\n if not result:\n self.status = \"The CSV data is empty.\"\n return []\n\n self.status = result\n return result\n\n except csv.Error as e:\n error_message = f\"CSV parsing error: {e}\"\n self.status = error_message\n raise ValueError(error_message) from e\n\n except Exception as e:\n error_message = f\"An error occurred: {e}\"\n self.status = error_message\n raise ValueError(error_message) from e\n\n # An error occurred\n raise ValueError(self.status)\n" }, "csv_file": { "_input_type": "FileInput", @@ -57407,7 +57434,7 @@ }, "JSONtoData": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -57425,7 +57452,7 @@ "icon": "braces", "legacy": true, "metadata": { - "code_hash": "0d9d78d496a2", + "code_hash": "e8d050bde0d0", "dependencies": { "dependencies": [ { @@ -57447,14 +57474,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "convert_json_to_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -57481,7 +57508,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nfrom pathlib import Path\n\nfrom json_repair import repair_json\n\nfrom lfx.base.data.storage_utils import read_file_text\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import FileInput, MessageTextInput, MultilineInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.utils.async_helpers import run_until_complete\n\n\nclass JSONToDataComponent(Component):\n display_name = \"Load JSON\"\n description = (\n \"Convert a JSON file, JSON from a file path, or a JSON string to a Data object or a list of Data objects\"\n )\n icon = \"braces\"\n name = \"JSONtoData\"\n legacy = True\n replacement = [\"data.File\"]\n\n inputs = [\n FileInput(\n name=\"json_file\",\n display_name=\"JSON File\",\n file_types=[\"json\"],\n info=\"Upload a JSON file to convert to a Data object or list of Data objects\",\n ),\n MessageTextInput(\n name=\"json_path\",\n display_name=\"JSON File Path\",\n info=\"Provide the path to the JSON file as pure text\",\n ),\n MultilineInput(\n name=\"json_string\",\n display_name=\"JSON String\",\n info=\"Enter a valid JSON string (object or array) to convert to a Data object or list of Data objects\",\n ),\n ]\n\n outputs = [\n Output(name=\"data\", display_name=\"Data\", method=\"convert_json_to_data\"),\n ]\n\n def convert_json_to_data(self) -> Data | list[Data]:\n if sum(bool(field) for field in [self.json_file, self.json_path, self.json_string]) != 1:\n msg = \"Please provide exactly one of: JSON file, file path, or JSON string.\"\n self.status = msg\n raise ValueError(msg)\n\n json_data = None\n\n try:\n if self.json_file:\n # FileInput always provides a local file path\n file_path = self.json_file\n if not file_path.lower().endswith(\".json\"):\n self.status = \"The provided file must be a JSON file.\"\n else:\n # Resolve to absolute path and read from local filesystem\n resolved_path = self.resolve_path(file_path)\n json_data = Path(resolved_path).read_text(encoding=\"utf-8\")\n\n elif self.json_path:\n # User-provided text path - could be local or S3 key\n file_path = self.json_path\n if not file_path.lower().endswith(\".json\"):\n self.status = \"The provided path must be to a JSON file.\"\n else:\n json_data = run_until_complete(\n read_file_text(file_path, encoding=\"utf-8\", resolve_path=self.resolve_path)\n )\n\n else:\n json_data = self.json_string\n\n if json_data:\n # Try to parse the JSON string\n try:\n parsed_data = json.loads(json_data)\n except json.JSONDecodeError:\n # If JSON parsing fails, try to repair the JSON string\n repaired_json_string = repair_json(json_data)\n parsed_data = json.loads(repaired_json_string)\n\n # Check if the parsed data is a list\n if isinstance(parsed_data, list):\n result = [Data(data=item) for item in parsed_data]\n else:\n result = Data(data=parsed_data)\n self.status = result\n return result\n\n except (json.JSONDecodeError, SyntaxError, ValueError) as e:\n error_message = f\"Invalid JSON or Python literal: {e}\"\n self.status = error_message\n raise ValueError(error_message) from e\n\n except Exception as e:\n error_message = f\"An error occurred: {e}\"\n self.status = error_message\n raise ValueError(error_message) from e\n\n # An error occurred\n raise ValueError(self.status)\n" + "value": "import json\nfrom pathlib import Path\n\nfrom json_repair import repair_json\n\nfrom lfx.base.data.storage_utils import read_file_text\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import FileInput, MessageTextInput, MultilineInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.utils.async_helpers import run_until_complete\n\n\nclass JSONToDataComponent(Component):\n display_name = \"Load JSON\"\n description = (\n \"Convert a JSON file, JSON from a file path, or a JSON string to a Data object or a list of Data objects\"\n )\n icon = \"braces\"\n name = \"JSONtoData\"\n legacy = True\n replacement = [\"data.File\"]\n\n inputs = [\n FileInput(\n name=\"json_file\",\n display_name=\"JSON File\",\n file_types=[\"json\"],\n info=\"Upload a JSON file to convert to a Data object or list of Data objects\",\n ),\n MessageTextInput(\n name=\"json_path\",\n display_name=\"JSON File Path\",\n info=\"Provide the path to the JSON file as pure text\",\n ),\n MultilineInput(\n name=\"json_string\",\n display_name=\"JSON String\",\n info=\"Enter a valid JSON string (object or array) to convert to a Data object or list of Data objects\",\n ),\n ]\n\n outputs = [\n Output(name=\"data\", display_name=\"JSON\", method=\"convert_json_to_data\"),\n ]\n\n def convert_json_to_data(self) -> Data | list[Data]:\n if sum(bool(field) for field in [self.json_file, self.json_path, self.json_string]) != 1:\n msg = \"Please provide exactly one of: JSON file, file path, or JSON string.\"\n self.status = msg\n raise ValueError(msg)\n\n json_data = None\n\n try:\n if self.json_file:\n # FileInput always provides a local file path\n file_path = self.json_file\n if not file_path.lower().endswith(\".json\"):\n self.status = \"The provided file must be a JSON file.\"\n else:\n # Resolve to absolute path and read from local filesystem\n resolved_path = self.resolve_path(file_path)\n json_data = Path(resolved_path).read_text(encoding=\"utf-8\")\n\n elif self.json_path:\n # User-provided text path - could be local or S3 key\n file_path = self.json_path\n if not file_path.lower().endswith(\".json\"):\n self.status = \"The provided path must be to a JSON file.\"\n else:\n json_data = run_until_complete(\n read_file_text(file_path, encoding=\"utf-8\", resolve_path=self.resolve_path)\n )\n\n else:\n json_data = self.json_string\n\n if json_data:\n # Try to parse the JSON string\n try:\n parsed_data = json.loads(json_data)\n except json.JSONDecodeError:\n # If JSON parsing fails, try to repair the JSON string\n repaired_json_string = repair_json(json_data)\n parsed_data = json.loads(repaired_json_string)\n\n # Check if the parsed data is a list\n if isinstance(parsed_data, list):\n result = [Data(data=item) for item in parsed_data]\n else:\n result = Data(data=parsed_data)\n self.status = result\n return result\n\n except (json.JSONDecodeError, SyntaxError, ValueError) as e:\n error_message = f\"Invalid JSON or Python literal: {e}\"\n self.status = error_message\n raise ValueError(error_message) from e\n\n except Exception as e:\n error_message = f\"An error occurred: {e}\"\n self.status = error_message\n raise ValueError(error_message) from e\n\n # An error occurred\n raise ValueError(self.status)\n" }, "json_file": { "_input_type": "FileInput", @@ -57567,9 +57594,9 @@ }, "MockDataGenerator": { "base_classes": [ - "Data", - "DataFrame", - "Message" + "JSON", + "Message", + "Table" ], "beta": false, "conditional_paths": [], @@ -57609,10 +57636,10 @@ "group_outputs": false, "method": "generate_dataframe_output", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -57637,10 +57664,10 @@ "group_outputs": false, "method": "generate_data_output", "name": "data_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -57671,7 +57698,7 @@ }, "NewsSearch": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -57727,10 +57754,10 @@ "group_outputs": false, "method": "search_news", "name": "articles", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -57922,7 +57949,7 @@ }, "RSSReaderSimple": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -57973,10 +58000,10 @@ "group_outputs": false, "method": "read_rss", "name": "articles", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -58053,7 +58080,7 @@ }, "SQLComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -58109,10 +58136,10 @@ "group_outputs": false, "method": "run_sql_query", "name": "run_sql_query", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -58237,8 +58264,8 @@ }, "URLComponent": { "base_classes": [ - "DataFrame", - "Message" + "Message", + "Table" ], "beta": false, "conditional_paths": [], @@ -58264,7 +58291,7 @@ "icon": "layout-template", "legacy": false, "metadata": { - "code_hash": "f773f55e3820", + "code_hash": "7c2b0b18854e", "dependencies": { "dependencies": [ { @@ -58302,10 +58329,10 @@ "group_outputs": false, "method": "fetch_content", "name": "page_results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -58383,7 +58410,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import importlib\nimport io\nimport re\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom langchain_community.document_loaders import RecursiveUrlLoader\nfrom markitdown import MarkItDown\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.data import safe_convert\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SliderInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.request_utils import get_user_agent\n\n# Constants\nDEFAULT_TIMEOUT = 30\nDEFAULT_MAX_DEPTH = 1\nDEFAULT_FORMAT = \"Text\"\n\n\nURL_REGEX = re.compile(\n r\"^(https?:\\/\\/)?\" r\"(www\\.)?\" r\"([a-zA-Z0-9.-]+)\" r\"(\\.[a-zA-Z]{2,})?\" r\"(:\\d+)?\" r\"(\\/[^\\s]*)?$\",\n re.IGNORECASE,\n)\n\nUSER_AGENT = None\n# Check if langflow is installed using importlib.util.find_spec(name))\nif importlib.util.find_spec(\"langflow\"):\n langflow_installed = True\n USER_AGENT = get_user_agent()\nelse:\n langflow_installed = False\n USER_AGENT = \"lfx\"\n\n\nclass URLComponent(Component):\n \"\"\"A component that loads and parses content from web pages recursively.\n\n This component allows fetching content from one or more URLs, with options to:\n - Control crawl depth\n - Prevent crawling outside the root domain\n - Use async loading for better performance\n - Extract either raw HTML or clean text\n - Configure request headers and timeouts\n \"\"\"\n\n display_name = \"URL\"\n description = \"Fetch content from one or more web pages, following links recursively.\"\n documentation: str = \"https://docs.langflow.org/url\"\n icon = \"layout-template\"\n name = \"URLComponent\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs to crawl recursively, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n input_types=[],\n ),\n SliderInput(\n name=\"max_depth\",\n display_name=\"Depth\",\n info=(\n \"Controls how many 'clicks' away from the initial page the crawler will go:\\n\"\n \"- depth 1: only the initial page\\n\"\n \"- depth 2: initial page + all pages linked directly from it\\n\"\n \"- depth 3: initial page + direct links + links found on those direct link pages\\n\"\n \"Note: This is about link traversal, not URL path depth.\"\n ),\n value=DEFAULT_MAX_DEPTH,\n range_spec=RangeSpec(min=1, max=5, step=1),\n required=False,\n min_label=\" \",\n max_label=\" \",\n min_label_icon=\"None\",\n max_label_icon=\"None\",\n # slider_input=True\n ),\n BoolInput(\n name=\"prevent_outside\",\n display_name=\"Prevent Outside\",\n info=(\n \"If enabled, only crawls URLs within the same domain as the root URL. \"\n \"This helps prevent the crawler from going to external websites.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"use_async\",\n display_name=\"Use Async\",\n info=(\n \"If enabled, uses asynchronous loading which can be significantly faster \"\n \"but might use more system resources.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=(\n \"Output Format. Use 'Text' to extract the text from the HTML, \"\n \"'Markdown' to parse the HTML into Markdown format, or 'HTML' \"\n \"for the raw HTML content.\"\n ),\n options=[\"Text\", \"HTML\", \"Markdown\"],\n value=DEFAULT_FORMAT,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout for the request in seconds.\",\n value=DEFAULT_TIMEOUT,\n required=False,\n advanced=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": USER_AGENT}],\n advanced=True,\n input_types=[\"DataFrame\"],\n ),\n BoolInput(\n name=\"filter_text_html\",\n display_name=\"Filter Text/HTML\",\n info=\"If enabled, filters out text/css content type from the results.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"continue_on_failure\",\n display_name=\"Continue on Failure\",\n info=\"If enabled, continues crawling even if some requests fail.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"check_response_status\",\n display_name=\"Check Response Status\",\n info=\"If enabled, checks the response status of the request.\",\n value=False,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"autoset_encoding\",\n display_name=\"Autoset Encoding\",\n info=\"If enabled, automatically sets the encoding of the request.\",\n value=True,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Extracted Pages\", name=\"page_results\", method=\"fetch_content\"),\n Output(display_name=\"Raw Content\", name=\"raw_results\", method=\"fetch_content_as_message\", tool_mode=False),\n ]\n\n @staticmethod\n def _html_extractor(x: str) -> str:\n \"\"\"Extract raw HTML content.\"\"\"\n return x\n\n @staticmethod\n def _text_extractor(x: str) -> str:\n \"\"\"Extract clean text from HTML.\"\"\"\n return BeautifulSoup(x, \"lxml\").get_text()\n\n @staticmethod\n def _markdown_extractor(x: str) -> str:\n \"\"\"Convert HTML to Markdown format.\"\"\"\n stream = io.BytesIO(x.encode(\"utf-8\"))\n result = MarkItDown(enable_plugins=False).convert_stream(stream)\n return result.markdown\n\n @staticmethod\n def validate_url(url: str) -> bool:\n \"\"\"Validates if the given string matches URL pattern.\n\n Args:\n url: The URL string to validate\n\n Returns:\n bool: True if the URL is valid, False otherwise\n \"\"\"\n return bool(URL_REGEX.match(url))\n\n def ensure_url(self, url: str) -> str:\n \"\"\"Ensures the given string is a valid URL.\n\n Args:\n url: The URL string to validate and normalize\n\n Returns:\n str: The normalized URL\n\n Raises:\n ValueError: If the URL is invalid\n \"\"\"\n url = url.strip()\n if not url.startswith((\"http://\", \"https://\")):\n url = \"https://\" + url\n\n if not self.validate_url(url):\n msg = f\"Invalid URL: {url}\"\n raise ValueError(msg)\n\n return url\n\n def _create_loader(self, url: str) -> RecursiveUrlLoader:\n \"\"\"Creates a RecursiveUrlLoader instance with the configured settings.\n\n Args:\n url: The URL to load\n\n Returns:\n RecursiveUrlLoader: Configured loader instance\n \"\"\"\n headers_dict = {header[\"key\"]: header[\"value\"] for header in self.headers if header[\"value\"] is not None}\n extractors = {\n \"HTML\": self._html_extractor,\n \"Markdown\": self._markdown_extractor,\n \"Text\": self._text_extractor,\n }\n extractor = extractors.get(self.format, self._text_extractor)\n\n return RecursiveUrlLoader(\n url=url,\n max_depth=self.max_depth,\n prevent_outside=self.prevent_outside,\n use_async=self.use_async,\n extractor=extractor,\n timeout=self.timeout,\n headers=headers_dict,\n check_response_status=self.check_response_status,\n continue_on_failure=self.continue_on_failure,\n base_url=url, # Add base_url to ensure consistent domain crawling\n autoset_encoding=self.autoset_encoding, # Enable automatic encoding detection\n exclude_dirs=[], # Allow customization of excluded directories\n link_regex=None, # Allow customization of link filtering\n )\n\n def fetch_url_contents(self) -> list[dict]:\n \"\"\"Load documents from the configured URLs.\n\n Returns:\n List[Data]: List of Data objects containing the fetched content\n\n Raises:\n ValueError: If no valid URLs are provided or if there's an error loading documents\n \"\"\"\n try:\n urls = list({self.ensure_url(url) for url in self.urls if url.strip()})\n logger.debug(f\"URLs: {urls}\")\n if not urls:\n msg = \"No valid URLs provided.\"\n raise ValueError(msg)\n\n all_docs = []\n for url in urls:\n logger.debug(f\"Loading documents from {url}\")\n\n try:\n loader = self._create_loader(url)\n docs = loader.load()\n\n if not docs:\n logger.warning(f\"No documents found for {url}\")\n continue\n\n logger.debug(f\"Found {len(docs)} documents from {url}\")\n all_docs.extend(docs)\n\n except requests.exceptions.RequestException as e:\n logger.exception(f\"Error loading documents from {url}: {e}\")\n continue\n\n if not all_docs:\n msg = \"No documents were successfully loaded from any URL\"\n raise ValueError(msg)\n\n # data = [Data(text=doc.page_content, **doc.metadata) for doc in all_docs]\n data = [\n {\n \"text\": safe_convert(doc.page_content, clean_data=True),\n \"url\": doc.metadata.get(\"source\", \"\"),\n \"title\": doc.metadata.get(\"title\", \"\"),\n \"description\": doc.metadata.get(\"description\", \"\"),\n \"content_type\": doc.metadata.get(\"content_type\", \"\"),\n \"language\": doc.metadata.get(\"language\", \"\"),\n }\n for doc in all_docs\n ]\n except Exception as e:\n error_msg = e.message if hasattr(e, \"message\") else e\n msg = f\"Error loading documents: {error_msg!s}\"\n logger.exception(msg)\n raise ValueError(msg) from e\n return data\n\n def fetch_content(self) -> DataFrame:\n \"\"\"Convert the documents to a DataFrame.\"\"\"\n return DataFrame(data=self.fetch_url_contents())\n\n def fetch_content_as_message(self) -> Message:\n \"\"\"Convert the documents to a Message.\"\"\"\n url_contents = self.fetch_url_contents()\n return Message(text=\"\\n\\n\".join([x[\"text\"] for x in url_contents]), data={\"data\": url_contents})\n" + "value": "import importlib\nimport io\nimport re\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom langchain_community.document_loaders import RecursiveUrlLoader\nfrom markitdown import MarkItDown\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.data import safe_convert\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, Output, SliderInput, TableInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.request_utils import get_user_agent\n\n# Constants\nDEFAULT_TIMEOUT = 30\nDEFAULT_MAX_DEPTH = 1\nDEFAULT_FORMAT = \"Text\"\n\n\nURL_REGEX = re.compile(\n r\"^(https?:\\/\\/)?\" r\"(www\\.)?\" r\"([a-zA-Z0-9.-]+)\" r\"(\\.[a-zA-Z]{2,})?\" r\"(:\\d+)?\" r\"(\\/[^\\s]*)?$\",\n re.IGNORECASE,\n)\n\nUSER_AGENT = None\n# Check if langflow is installed using importlib.util.find_spec(name))\nif importlib.util.find_spec(\"langflow\"):\n langflow_installed = True\n USER_AGENT = get_user_agent()\nelse:\n langflow_installed = False\n USER_AGENT = \"lfx\"\n\n\nclass URLComponent(Component):\n \"\"\"A component that loads and parses content from web pages recursively.\n\n This component allows fetching content from one or more URLs, with options to:\n - Control crawl depth\n - Prevent crawling outside the root domain\n - Use async loading for better performance\n - Extract either raw HTML or clean text\n - Configure request headers and timeouts\n \"\"\"\n\n display_name = \"URL\"\n description = \"Fetch content from one or more web pages, following links recursively.\"\n documentation: str = \"https://docs.langflow.org/url\"\n icon = \"layout-template\"\n name = \"URLComponent\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs to crawl recursively, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n input_types=[],\n ),\n SliderInput(\n name=\"max_depth\",\n display_name=\"Depth\",\n info=(\n \"Controls how many 'clicks' away from the initial page the crawler will go:\\n\"\n \"- depth 1: only the initial page\\n\"\n \"- depth 2: initial page + all pages linked directly from it\\n\"\n \"- depth 3: initial page + direct links + links found on those direct link pages\\n\"\n \"Note: This is about link traversal, not URL path depth.\"\n ),\n value=DEFAULT_MAX_DEPTH,\n range_spec=RangeSpec(min=1, max=5, step=1),\n required=False,\n min_label=\" \",\n max_label=\" \",\n min_label_icon=\"None\",\n max_label_icon=\"None\",\n # slider_input=True\n ),\n BoolInput(\n name=\"prevent_outside\",\n display_name=\"Prevent Outside\",\n info=(\n \"If enabled, only crawls URLs within the same domain as the root URL. \"\n \"This helps prevent the crawler from going to external websites.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"use_async\",\n display_name=\"Use Async\",\n info=(\n \"If enabled, uses asynchronous loading which can be significantly faster \"\n \"but might use more system resources.\"\n ),\n value=True,\n required=False,\n advanced=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=(\n \"Output Format. Use 'Text' to extract the text from the HTML, \"\n \"'Markdown' to parse the HTML into Markdown format, or 'HTML' \"\n \"for the raw HTML content.\"\n ),\n options=[\"Text\", \"HTML\", \"Markdown\"],\n value=DEFAULT_FORMAT,\n advanced=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout for the request in seconds.\",\n value=DEFAULT_TIMEOUT,\n required=False,\n advanced=True,\n ),\n TableInput(\n name=\"headers\",\n display_name=\"Headers\",\n info=\"The headers to send with the request\",\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Header\",\n \"type\": \"str\",\n \"description\": \"Header name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Header value\",\n },\n ],\n value=[{\"key\": \"User-Agent\", \"value\": USER_AGENT}],\n advanced=True,\n input_types=[\"DataFrame\", \"Table\"],\n ),\n BoolInput(\n name=\"filter_text_html\",\n display_name=\"Filter Text/HTML\",\n info=\"If enabled, filters out text/css content type from the results.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"continue_on_failure\",\n display_name=\"Continue on Failure\",\n info=\"If enabled, continues crawling even if some requests fail.\",\n value=True,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"check_response_status\",\n display_name=\"Check Response Status\",\n info=\"If enabled, checks the response status of the request.\",\n value=False,\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"autoset_encoding\",\n display_name=\"Autoset Encoding\",\n info=\"If enabled, automatically sets the encoding of the request.\",\n value=True,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Extracted Pages\", name=\"page_results\", method=\"fetch_content\"),\n Output(display_name=\"Raw Content\", name=\"raw_results\", method=\"fetch_content_as_message\", tool_mode=False),\n ]\n\n @staticmethod\n def _html_extractor(x: str) -> str:\n \"\"\"Extract raw HTML content.\"\"\"\n return x\n\n @staticmethod\n def _text_extractor(x: str) -> str:\n \"\"\"Extract clean text from HTML.\"\"\"\n return BeautifulSoup(x, \"lxml\").get_text()\n\n @staticmethod\n def _markdown_extractor(x: str) -> str:\n \"\"\"Convert HTML to Markdown format.\"\"\"\n stream = io.BytesIO(x.encode(\"utf-8\"))\n result = MarkItDown(enable_plugins=False).convert_stream(stream)\n return result.markdown\n\n @staticmethod\n def validate_url(url: str) -> bool:\n \"\"\"Validates if the given string matches URL pattern.\n\n Args:\n url: The URL string to validate\n\n Returns:\n bool: True if the URL is valid, False otherwise\n \"\"\"\n return bool(URL_REGEX.match(url))\n\n def ensure_url(self, url: str) -> str:\n \"\"\"Ensures the given string is a valid URL.\n\n Args:\n url: The URL string to validate and normalize\n\n Returns:\n str: The normalized URL\n\n Raises:\n ValueError: If the URL is invalid\n \"\"\"\n url = url.strip()\n if not url.startswith((\"http://\", \"https://\")):\n url = \"https://\" + url\n\n if not self.validate_url(url):\n msg = f\"Invalid URL: {url}\"\n raise ValueError(msg)\n\n return url\n\n def _create_loader(self, url: str) -> RecursiveUrlLoader:\n \"\"\"Creates a RecursiveUrlLoader instance with the configured settings.\n\n Args:\n url: The URL to load\n\n Returns:\n RecursiveUrlLoader: Configured loader instance\n \"\"\"\n headers_dict = {header[\"key\"]: header[\"value\"] for header in self.headers if header[\"value\"] is not None}\n extractors = {\n \"HTML\": self._html_extractor,\n \"Markdown\": self._markdown_extractor,\n \"Text\": self._text_extractor,\n }\n extractor = extractors.get(self.format, self._text_extractor)\n\n return RecursiveUrlLoader(\n url=url,\n max_depth=self.max_depth,\n prevent_outside=self.prevent_outside,\n use_async=self.use_async,\n extractor=extractor,\n timeout=self.timeout,\n headers=headers_dict,\n check_response_status=self.check_response_status,\n continue_on_failure=self.continue_on_failure,\n base_url=url, # Add base_url to ensure consistent domain crawling\n autoset_encoding=self.autoset_encoding, # Enable automatic encoding detection\n exclude_dirs=[], # Allow customization of excluded directories\n link_regex=None, # Allow customization of link filtering\n )\n\n def fetch_url_contents(self) -> list[dict]:\n \"\"\"Load documents from the configured URLs.\n\n Returns:\n List[Data]: List of Data objects containing the fetched content\n\n Raises:\n ValueError: If no valid URLs are provided or if there's an error loading documents\n \"\"\"\n try:\n urls = list({self.ensure_url(url) for url in self.urls if url.strip()})\n logger.debug(f\"URLs: {urls}\")\n if not urls:\n msg = \"No valid URLs provided.\"\n raise ValueError(msg)\n\n all_docs = []\n for url in urls:\n logger.debug(f\"Loading documents from {url}\")\n\n try:\n loader = self._create_loader(url)\n docs = loader.load()\n\n if not docs:\n logger.warning(f\"No documents found for {url}\")\n continue\n\n logger.debug(f\"Found {len(docs)} documents from {url}\")\n all_docs.extend(docs)\n\n except requests.exceptions.RequestException as e:\n logger.exception(f\"Error loading documents from {url}: {e}\")\n continue\n\n if not all_docs:\n msg = \"No documents were successfully loaded from any URL\"\n raise ValueError(msg)\n\n # data = [Data(text=doc.page_content, **doc.metadata) for doc in all_docs]\n data = [\n {\n \"text\": safe_convert(doc.page_content, clean_data=True),\n \"url\": doc.metadata.get(\"source\", \"\"),\n \"title\": doc.metadata.get(\"title\", \"\"),\n \"description\": doc.metadata.get(\"description\", \"\"),\n \"content_type\": doc.metadata.get(\"content_type\", \"\"),\n \"language\": doc.metadata.get(\"language\", \"\"),\n }\n for doc in all_docs\n ]\n except Exception as e:\n error_msg = e.message if hasattr(e, \"message\") else e\n msg = f\"Error loading documents: {error_msg!s}\"\n logger.exception(msg)\n raise ValueError(msg) from e\n return data\n\n def fetch_content(self) -> DataFrame:\n \"\"\"Convert the documents to a DataFrame.\"\"\"\n return DataFrame(data=self.fetch_url_contents())\n\n def fetch_content_as_message(self) -> Message:\n \"\"\"Convert the documents to a Message.\"\"\"\n url_contents = self.fetch_url_contents()\n return Message(text=\"\\n\\n\".join([x[\"text\"] for x in url_contents]), data={\"data\": url_contents})\n" }, "continue_on_failure": { "_input_type": "BoolInput", @@ -58460,7 +58487,8 @@ "dynamic": false, "info": "The headers to send with the request", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "is_list": true, "list_add_label": "Add More", @@ -58616,7 +58644,7 @@ }, "UnifiedWebSearch": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -58673,10 +58701,10 @@ "group_outputs": false, "method": "perform_search", "name": "results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -59959,8 +59987,8 @@ }, "AstraDB": { "base_classes": [ - "Data", - "DataFrame", + "JSON", + "Table", "VectorStore" ], "beta": false, @@ -60032,24 +60060,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -60826,7 +60854,7 @@ }, "AstraDBCQLToolComponent": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -60887,14 +60915,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -61452,6 +61480,10 @@ "display_name": "Tools Parameters", "dynamic": false, "info": "Define the structure for the tool parameters. Describe the parameters in a way the LLM can understand how to use them. Add the parameters respecting the table schema (Partition Keys, Clustering Keys and Indexed Fields).", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "tools_params", @@ -62022,8 +62054,8 @@ }, "AstraDBGraph": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -62083,24 +62115,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -62682,7 +62714,7 @@ }, "AstraDBTool": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -62745,14 +62777,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -63314,6 +63346,10 @@ "display_name": "Tools Parameters", "dynamic": false, "info": "Define the structure for the tool parameters. Describe the parameters in a way the LLM can understand how to use them.", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "tools_params_v2", @@ -63879,8 +63915,8 @@ }, "GraphRAG": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -63931,24 +63967,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -64123,8 +64159,8 @@ }, "HCD": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -64193,24 +64229,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -65159,7 +65195,7 @@ { "ChunkDoclingDocument": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -65183,7 +65219,7 @@ "icon": "Docling", "legacy": false, "metadata": { - "code_hash": "49d762d97039", + "code_hash": "7775393185fe", "dependencies": { "dependencies": [ { @@ -65209,14 +65245,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "chunk_documents", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -65291,17 +65327,19 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\n\nimport tiktoken\nfrom docling_core.transforms.chunker import BaseChunker, DocMeta\nfrom docling_core.transforms.chunker.hierarchical_chunker import HierarchicalChunker\n\nfrom lfx.base.data.docling_utils import extract_docling_documents\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, Output, StrInput\nfrom lfx.schema import Data, DataFrame\n\n\nclass ChunkDoclingDocumentComponent(Component):\n display_name: str = \"Chunk DoclingDocument\"\n description: str = \"Use the DocumentDocument chunkers to split the document into chunks.\"\n documentation = \"https://docling-project.github.io/docling/concepts/chunking/\"\n icon = \"Docling\"\n name = \"ChunkDoclingDocument\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Data or DataFrame\",\n info=\"The data with documents to split in chunks.\",\n input_types=[\"Data\", \"DataFrame\"],\n required=True,\n ),\n DropdownInput(\n name=\"chunker\",\n display_name=\"Chunker\",\n options=[\"HybridChunker\", \"HierarchicalChunker\"],\n info=(\"Which chunker to use.\"),\n value=\"HybridChunker\",\n real_time_refresh=True,\n input_types=[\"Message\"],\n ),\n DropdownInput(\n name=\"provider\",\n display_name=\"Provider\",\n options=[\"Hugging Face\", \"OpenAI\"],\n info=(\"Which tokenizer provider.\"),\n value=\"Hugging Face\",\n show=True,\n real_time_refresh=True,\n advanced=True,\n dynamic=True,\n ),\n StrInput(\n name=\"hf_model_name\",\n display_name=\"HF model name\",\n info=(\n \"Model name of the tokenizer to use with the HybridChunker when Hugging Face is chosen as a tokenizer.\"\n ),\n value=\"sentence-transformers/all-MiniLM-L6-v2\",\n show=True,\n advanced=True,\n dynamic=True,\n ),\n StrInput(\n name=\"openai_model_name\",\n display_name=\"OpenAI model name\",\n info=(\"Model name of the tokenizer to use with the HybridChunker when OpenAI is chosen as a tokenizer.\"),\n value=\"gpt-4o\",\n show=False,\n advanced=True,\n dynamic=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Maximum tokens\",\n info=(\"Maximum number of tokens for the HybridChunker.\"),\n show=True,\n required=False,\n advanced=True,\n dynamic=True,\n input_types=[\"Message\"],\n ),\n BoolInput(\n name=\"merge_peers\",\n display_name=\"Merge peers\",\n info=\"Merge undersized chunks sharing the same relevant metadata.\",\n value=True,\n show=True,\n advanced=True,\n dynamic=True,\n ),\n BoolInput(\n name=\"always_emit_headings\",\n display_name=\"Always emit headings\",\n info=\"Emit headings even for empty sections.\",\n value=False,\n show=True,\n advanced=True,\n dynamic=True,\n ),\n MessageTextInput(\n name=\"doc_key\",\n display_name=\"Doc Key\",\n info=\"The key to use for the DoclingDocument column.\",\n value=\"doc\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"chunk_documents\"),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n \"\"\"Update build_config to show/hide fields based on chunker and provider selection.\"\"\"\n if field_name == \"chunker\":\n provider_type = build_config[\"provider\"][\"value\"]\n is_hf = provider_type == \"Hugging Face\"\n is_openai = provider_type == \"OpenAI\"\n if field_value == \"HybridChunker\":\n build_config[\"provider\"][\"show\"] = True\n build_config[\"hf_model_name\"][\"show\"] = is_hf\n build_config[\"openai_model_name\"][\"show\"] = is_openai\n build_config[\"max_tokens\"][\"show\"] = True\n build_config[\"merge_peers\"][\"show\"] = True\n build_config[\"always_emit_headings\"][\"show\"] = True\n else:\n build_config[\"provider\"][\"show\"] = False\n build_config[\"hf_model_name\"][\"show\"] = False\n build_config[\"openai_model_name\"][\"show\"] = False\n build_config[\"max_tokens\"][\"show\"] = False\n build_config[\"merge_peers\"][\"show\"] = False\n build_config[\"always_emit_headings\"][\"show\"] = False\n elif field_name == \"provider\" and build_config[\"chunker\"][\"value\"] == \"HybridChunker\":\n if field_value == \"Hugging Face\":\n build_config[\"hf_model_name\"][\"show\"] = True\n build_config[\"openai_model_name\"][\"show\"] = False\n elif field_value == \"OpenAI\":\n build_config[\"hf_model_name\"][\"show\"] = False\n build_config[\"openai_model_name\"][\"show\"] = True\n\n return build_config\n\n def _docs_to_data(self, docs) -> list[Data]:\n return [Data(text=doc.page_content, data=doc.metadata) for doc in docs]\n\n def chunk_documents(self) -> DataFrame:\n documents, warning = extract_docling_documents(self.data_inputs, self.doc_key)\n if warning:\n self.status = warning\n\n chunker: BaseChunker\n if self.chunker == \"HybridChunker\":\n try:\n from docling_core.transforms.chunker.hybrid_chunker import HybridChunker\n except ImportError as e:\n msg = (\n \"HybridChunker is not installed. Please install it with `uv pip install docling-core[chunking] \"\n \"or `uv pip install transformers`\"\n )\n raise ImportError(msg) from e\n max_tokens: int | None = self.max_tokens if self.max_tokens else None\n if self.provider == \"Hugging Face\":\n try:\n from docling_core.transforms.chunker.tokenizer.huggingface import HuggingFaceTokenizer\n except ImportError as e:\n msg = (\n \"HuggingFaceTokenizer is not installed.\"\n \" Please install it with `uv pip install docling-core[chunking]`\"\n )\n raise ImportError(msg) from e\n tokenizer = HuggingFaceTokenizer.from_pretrained(\n model_name=self.hf_model_name,\n max_tokens=max_tokens,\n )\n elif self.provider == \"OpenAI\":\n try:\n from docling_core.transforms.chunker.tokenizer.openai import OpenAITokenizer\n except ImportError as e:\n msg = (\n \"OpenAITokenizer is not installed.\"\n \" Please install it with `uv pip install docling-core[chunking]`\"\n \" or `uv pip install transformers`\"\n )\n raise ImportError(msg) from e\n if max_tokens is None:\n max_tokens = 128 * 1024 # context window length required for OpenAI tokenizers\n tokenizer = OpenAITokenizer(\n tokenizer=tiktoken.encoding_for_model(self.openai_model_name), max_tokens=max_tokens\n )\n chunker = HybridChunker(\n tokenizer=tokenizer,\n merge_peers=bool(self.merge_peers),\n always_emit_headings=bool(self.always_emit_headings),\n )\n\n elif self.chunker == \"HierarchicalChunker\":\n chunker = HierarchicalChunker()\n else:\n msg = f\"Unknown chunker: {self.chunker}\"\n raise ValueError(msg)\n\n results: list[Data] = []\n try:\n for doc in documents:\n for chunk in chunker.chunk(dl_doc=doc):\n enriched_text = chunker.contextualize(chunk=chunk)\n meta = DocMeta.model_validate(chunk.meta)\n\n results.append(\n Data(\n data={\n \"text\": enriched_text,\n \"document_id\": f\"{doc.origin.binary_hash}\",\n \"doc_items\": json.dumps([item.self_ref for item in meta.doc_items]),\n }\n )\n )\n\n except Exception as e:\n msg = f\"Error splitting text: {e}\"\n raise TypeError(msg) from e\n\n return DataFrame(results)\n" + "value": "import json\n\nimport tiktoken\nfrom docling_core.transforms.chunker import BaseChunker, DocMeta\nfrom docling_core.transforms.chunker.hierarchical_chunker import HierarchicalChunker\n\nfrom lfx.base.data.docling_utils import extract_docling_documents\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, Output, StrInput\nfrom lfx.schema import Data, DataFrame\n\n\nclass ChunkDoclingDocumentComponent(Component):\n display_name: str = \"Chunk DoclingDocument\"\n description: str = \"Use the DocumentDocument chunkers to split the document into chunks.\"\n documentation = \"https://docling-project.github.io/docling/concepts/chunking/\"\n icon = \"Docling\"\n name = \"ChunkDoclingDocument\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"JSON or Table\",\n info=\"The data with documents to split in chunks.\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\"],\n required=True,\n ),\n DropdownInput(\n name=\"chunker\",\n display_name=\"Chunker\",\n options=[\"HybridChunker\", \"HierarchicalChunker\"],\n info=(\"Which chunker to use.\"),\n value=\"HybridChunker\",\n real_time_refresh=True,\n input_types=[\"Message\"],\n ),\n DropdownInput(\n name=\"provider\",\n display_name=\"Provider\",\n options=[\"Hugging Face\", \"OpenAI\"],\n info=(\"Which tokenizer provider.\"),\n value=\"Hugging Face\",\n show=True,\n real_time_refresh=True,\n advanced=True,\n dynamic=True,\n ),\n StrInput(\n name=\"hf_model_name\",\n display_name=\"HF model name\",\n info=(\n \"Model name of the tokenizer to use with the HybridChunker when Hugging Face is chosen as a tokenizer.\"\n ),\n value=\"sentence-transformers/all-MiniLM-L6-v2\",\n show=True,\n advanced=True,\n dynamic=True,\n ),\n StrInput(\n name=\"openai_model_name\",\n display_name=\"OpenAI model name\",\n info=(\"Model name of the tokenizer to use with the HybridChunker when OpenAI is chosen as a tokenizer.\"),\n value=\"gpt-4o\",\n show=False,\n advanced=True,\n dynamic=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Maximum tokens\",\n info=(\"Maximum number of tokens for the HybridChunker.\"),\n show=True,\n required=False,\n advanced=True,\n dynamic=True,\n input_types=[\"Message\"],\n ),\n BoolInput(\n name=\"merge_peers\",\n display_name=\"Merge peers\",\n info=\"Merge undersized chunks sharing the same relevant metadata.\",\n value=True,\n show=True,\n advanced=True,\n dynamic=True,\n ),\n BoolInput(\n name=\"always_emit_headings\",\n display_name=\"Always emit headings\",\n info=\"Emit headings even for empty sections.\",\n value=False,\n show=True,\n advanced=True,\n dynamic=True,\n ),\n MessageTextInput(\n name=\"doc_key\",\n display_name=\"Doc Key\",\n info=\"The key to use for the DoclingDocument column.\",\n value=\"doc\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"chunk_documents\"),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n \"\"\"Update build_config to show/hide fields based on chunker and provider selection.\"\"\"\n if field_name == \"chunker\":\n provider_type = build_config[\"provider\"][\"value\"]\n is_hf = provider_type == \"Hugging Face\"\n is_openai = provider_type == \"OpenAI\"\n if field_value == \"HybridChunker\":\n build_config[\"provider\"][\"show\"] = True\n build_config[\"hf_model_name\"][\"show\"] = is_hf\n build_config[\"openai_model_name\"][\"show\"] = is_openai\n build_config[\"max_tokens\"][\"show\"] = True\n build_config[\"merge_peers\"][\"show\"] = True\n build_config[\"always_emit_headings\"][\"show\"] = True\n else:\n build_config[\"provider\"][\"show\"] = False\n build_config[\"hf_model_name\"][\"show\"] = False\n build_config[\"openai_model_name\"][\"show\"] = False\n build_config[\"max_tokens\"][\"show\"] = False\n build_config[\"merge_peers\"][\"show\"] = False\n build_config[\"always_emit_headings\"][\"show\"] = False\n elif field_name == \"provider\" and build_config[\"chunker\"][\"value\"] == \"HybridChunker\":\n if field_value == \"Hugging Face\":\n build_config[\"hf_model_name\"][\"show\"] = True\n build_config[\"openai_model_name\"][\"show\"] = False\n elif field_value == \"OpenAI\":\n build_config[\"hf_model_name\"][\"show\"] = False\n build_config[\"openai_model_name\"][\"show\"] = True\n\n return build_config\n\n def _docs_to_data(self, docs) -> list[Data]:\n return [Data(text=doc.page_content, data=doc.metadata) for doc in docs]\n\n def chunk_documents(self) -> DataFrame:\n documents, warning = extract_docling_documents(self.data_inputs, self.doc_key)\n if warning:\n self.status = warning\n\n chunker: BaseChunker\n if self.chunker == \"HybridChunker\":\n try:\n from docling_core.transforms.chunker.hybrid_chunker import HybridChunker\n except ImportError as e:\n msg = (\n \"HybridChunker is not installed. Please install it with `uv pip install docling-core[chunking] \"\n \"or `uv pip install transformers`\"\n )\n raise ImportError(msg) from e\n max_tokens: int | None = self.max_tokens if self.max_tokens else None\n if self.provider == \"Hugging Face\":\n try:\n from docling_core.transforms.chunker.tokenizer.huggingface import HuggingFaceTokenizer\n except ImportError as e:\n msg = (\n \"HuggingFaceTokenizer is not installed.\"\n \" Please install it with `uv pip install docling-core[chunking]`\"\n )\n raise ImportError(msg) from e\n tokenizer = HuggingFaceTokenizer.from_pretrained(\n model_name=self.hf_model_name,\n max_tokens=max_tokens,\n )\n elif self.provider == \"OpenAI\":\n try:\n from docling_core.transforms.chunker.tokenizer.openai import OpenAITokenizer\n except ImportError as e:\n msg = (\n \"OpenAITokenizer is not installed.\"\n \" Please install it with `uv pip install docling-core[chunking]`\"\n \" or `uv pip install transformers`\"\n )\n raise ImportError(msg) from e\n if max_tokens is None:\n max_tokens = 128 * 1024 # context window length required for OpenAI tokenizers\n tokenizer = OpenAITokenizer(\n tokenizer=tiktoken.encoding_for_model(self.openai_model_name), max_tokens=max_tokens\n )\n chunker = HybridChunker(\n tokenizer=tokenizer,\n merge_peers=bool(self.merge_peers),\n always_emit_headings=bool(self.always_emit_headings),\n )\n\n elif self.chunker == \"HierarchicalChunker\":\n chunker = HierarchicalChunker()\n else:\n msg = f\"Unknown chunker: {self.chunker}\"\n raise ValueError(msg)\n\n results: list[Data] = []\n try:\n for doc in documents:\n for chunk in chunker.chunk(dl_doc=doc):\n enriched_text = chunker.contextualize(chunk=chunk)\n meta = DocMeta.model_validate(chunk.meta)\n\n results.append(\n Data(\n data={\n \"text\": enriched_text,\n \"document_id\": f\"{doc.origin.binary_hash}\",\n \"doc_items\": json.dumps([item.self_ref for item in meta.doc_items]),\n }\n )\n )\n\n except Exception as e:\n msg = f\"Error splitting text: {e}\"\n raise TypeError(msg) from e\n\n return DataFrame(results)\n" }, "data_inputs": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "The data with documents to split in chunks.", "input_types": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -65459,7 +65497,7 @@ }, "DoclingInline": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -65512,10 +65550,10 @@ "group_outputs": false, "method": "load_files", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -65589,6 +65627,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, @@ -65848,7 +65887,7 @@ }, "DoclingRemote": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -65909,10 +65948,10 @@ "group_outputs": false, "method": "load_files", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -66035,6 +66074,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, @@ -66241,8 +66281,8 @@ }, "ExportDoclingDocument": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -66263,7 +66303,7 @@ "icon": "Docling", "legacy": false, "metadata": { - "code_hash": "32577a7e396b", + "code_hash": "24cc033dcec6", "dependencies": { "dependencies": [ { @@ -66289,24 +66329,24 @@ "group_outputs": false, "method": "export_document", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -66330,17 +66370,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom docling_core.types.doc import ImageRefMode\n\nfrom lfx.base.data.docling_utils import extract_docling_documents\nfrom lfx.custom import Component\nfrom lfx.io import DropdownInput, HandleInput, MessageTextInput, Output, StrInput\nfrom lfx.schema import Data, DataFrame\n\n\nclass ExportDoclingDocumentComponent(Component):\n display_name: str = \"Export DoclingDocument\"\n description: str = \"Export DoclingDocument to markdown, html or other formats.\"\n documentation = \"https://docling-project.github.io/docling/\"\n icon = \"Docling\"\n name = \"ExportDoclingDocument\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Data or DataFrame\",\n info=\"The data with documents to export.\",\n input_types=[\"Data\", \"DataFrame\"],\n required=True,\n ),\n DropdownInput(\n name=\"export_format\",\n display_name=\"Export format\",\n options=[\"Markdown\", \"HTML\", \"Plaintext\", \"DocTags\"],\n info=\"Select the export format to convert the input.\",\n value=\"Markdown\",\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"image_mode\",\n display_name=\"Image export mode\",\n options=[\"placeholder\", \"embedded\"],\n info=(\n \"Specify how images are exported in the output. Placeholder will replace the images with a string, \"\n \"whereas Embedded will include them as base64 encoded images.\"\n ),\n value=\"placeholder\",\n ),\n StrInput(\n name=\"md_image_placeholder\",\n display_name=\"Image placeholder\",\n info=\"Specify the image placeholder for markdown exports.\",\n value=\"\",\n advanced=True,\n ),\n StrInput(\n name=\"md_page_break_placeholder\",\n display_name=\"Page break placeholder\",\n info=\"Add this placeholder betweek pages in the markdown output.\",\n value=\"\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"doc_key\",\n display_name=\"Doc Key\",\n info=\"The key to use for the DoclingDocument column.\",\n value=\"doc\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Exported data\", name=\"data\", method=\"export_document\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:\n if field_name == \"export_format\" and field_value == \"Markdown\":\n build_config[\"md_image_placeholder\"][\"show\"] = True\n build_config[\"md_page_break_placeholder\"][\"show\"] = True\n build_config[\"image_mode\"][\"show\"] = True\n elif field_name == \"export_format\" and field_value == \"HTML\":\n build_config[\"md_image_placeholder\"][\"show\"] = False\n build_config[\"md_page_break_placeholder\"][\"show\"] = False\n build_config[\"image_mode\"][\"show\"] = True\n elif field_name == \"export_format\" and field_value in {\"Plaintext\", \"DocTags\"}:\n build_config[\"md_image_placeholder\"][\"show\"] = False\n build_config[\"md_page_break_placeholder\"][\"show\"] = False\n build_config[\"image_mode\"][\"show\"] = False\n\n return build_config\n\n def export_document(self) -> list[Data]:\n documents, warning = extract_docling_documents(self.data_inputs, self.doc_key)\n if warning:\n self.status = warning\n\n results: list[Data] = []\n try:\n image_mode = ImageRefMode(self.image_mode)\n for doc in documents:\n content = \"\"\n if self.export_format == \"Markdown\":\n content = doc.export_to_markdown(\n image_mode=image_mode,\n image_placeholder=self.md_image_placeholder,\n page_break_placeholder=self.md_page_break_placeholder,\n )\n elif self.export_format == \"HTML\":\n content = doc.export_to_html(image_mode=image_mode)\n elif self.export_format == \"Plaintext\":\n content = doc.export_to_text()\n elif self.export_format == \"DocTags\":\n content = doc.export_to_doctags()\n\n results.append(Data(text=content))\n except Exception as e:\n msg = f\"Error splitting text: {e}\"\n raise TypeError(msg) from e\n\n return results\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.export_document())\n" + "value": "from typing import Any\n\nfrom docling_core.types.doc import ImageRefMode\n\nfrom lfx.base.data.docling_utils import extract_docling_documents\nfrom lfx.custom import Component\nfrom lfx.io import DropdownInput, HandleInput, MessageTextInput, Output, StrInput\nfrom lfx.schema import Data, DataFrame\n\n\nclass ExportDoclingDocumentComponent(Component):\n display_name: str = \"Export DoclingDocument\"\n description: str = \"Export DoclingDocument to markdown, html or other formats.\"\n documentation = \"https://docling-project.github.io/docling/\"\n icon = \"Docling\"\n name = \"ExportDoclingDocument\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"JSON or Table\",\n info=\"The data with documents to export.\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\"],\n required=True,\n ),\n DropdownInput(\n name=\"export_format\",\n display_name=\"Export format\",\n options=[\"Markdown\", \"HTML\", \"Plaintext\", \"DocTags\"],\n info=\"Select the export format to convert the input.\",\n value=\"Markdown\",\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"image_mode\",\n display_name=\"Image export mode\",\n options=[\"placeholder\", \"embedded\"],\n info=(\n \"Specify how images are exported in the output. Placeholder will replace the images with a string, \"\n \"whereas Embedded will include them as base64 encoded images.\"\n ),\n value=\"placeholder\",\n ),\n StrInput(\n name=\"md_image_placeholder\",\n display_name=\"Image placeholder\",\n info=\"Specify the image placeholder for markdown exports.\",\n value=\"\",\n advanced=True,\n ),\n StrInput(\n name=\"md_page_break_placeholder\",\n display_name=\"Page break placeholder\",\n info=\"Add this placeholder betweek pages in the markdown output.\",\n value=\"\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"doc_key\",\n display_name=\"Doc Key\",\n info=\"The key to use for the DoclingDocument column.\",\n value=\"doc\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Exported data\", name=\"data\", method=\"export_document\"),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:\n if field_name == \"export_format\" and field_value == \"Markdown\":\n build_config[\"md_image_placeholder\"][\"show\"] = True\n build_config[\"md_page_break_placeholder\"][\"show\"] = True\n build_config[\"image_mode\"][\"show\"] = True\n elif field_name == \"export_format\" and field_value == \"HTML\":\n build_config[\"md_image_placeholder\"][\"show\"] = False\n build_config[\"md_page_break_placeholder\"][\"show\"] = False\n build_config[\"image_mode\"][\"show\"] = True\n elif field_name == \"export_format\" and field_value in {\"Plaintext\", \"DocTags\"}:\n build_config[\"md_image_placeholder\"][\"show\"] = False\n build_config[\"md_page_break_placeholder\"][\"show\"] = False\n build_config[\"image_mode\"][\"show\"] = False\n\n return build_config\n\n def export_document(self) -> list[Data]:\n documents, warning = extract_docling_documents(self.data_inputs, self.doc_key)\n if warning:\n self.status = warning\n\n results: list[Data] = []\n try:\n image_mode = ImageRefMode(self.image_mode)\n for doc in documents:\n content = \"\"\n if self.export_format == \"Markdown\":\n content = doc.export_to_markdown(\n image_mode=image_mode,\n image_placeholder=self.md_image_placeholder,\n page_break_placeholder=self.md_page_break_placeholder,\n )\n elif self.export_format == \"HTML\":\n content = doc.export_to_html(image_mode=image_mode)\n elif self.export_format == \"Plaintext\":\n content = doc.export_to_text()\n elif self.export_format == \"DocTags\":\n content = doc.export_to_doctags()\n\n results.append(Data(text=content))\n except Exception as e:\n msg = f\"Error splitting text: {e}\"\n raise TypeError(msg) from e\n\n return results\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.export_document())\n" }, "data_inputs": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "The data with documents to export.", "input_types": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -66489,7 +66531,7 @@ { "DuckDuckGoSearchComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -66507,7 +66549,7 @@ "icon": "DuckDuckGo", "legacy": false, "metadata": { - "code_hash": "2e522a5a4389", + "code_hash": "2b8d1e2e8317", "dependencies": { "dependencies": [ { @@ -66529,14 +66571,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -66560,7 +66602,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from langchain_community.tools import DuckDuckGoSearchRun\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import IntInput, MessageTextInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass DuckDuckGoSearchComponent(Component):\n \"\"\"Component for performing web searches using DuckDuckGo.\"\"\"\n\n display_name = \"DuckDuckGo Search\"\n description = \"Search the web using DuckDuckGo with customizable result limits\"\n documentation = \"https://python.langchain.com/docs/integrations/tools/ddg\"\n icon = \"DuckDuckGo\"\n\n inputs = [\n MessageTextInput(\n name=\"input_value\",\n display_name=\"Search Query\",\n required=True,\n info=\"The search query to execute with DuckDuckGo\",\n tool_mode=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n value=5,\n required=False,\n advanced=True,\n info=\"Maximum number of search results to return\",\n ),\n IntInput(\n name=\"max_snippet_length\",\n display_name=\"Max Snippet Length\",\n value=100,\n required=False,\n advanced=True,\n info=\"Maximum length of each result snippet\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def _build_wrapper(self) -> DuckDuckGoSearchRun:\n \"\"\"Build the DuckDuckGo search wrapper.\"\"\"\n return DuckDuckGoSearchRun()\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n \"\"\"Execute the search and return results as Data objects.\"\"\"\n try:\n wrapper = self._build_wrapper()\n\n full_results = wrapper.run(f\"{self.input_value} (site:*)\")\n\n result_list = full_results.split(\"\\n\")[: self.max_results]\n\n data_results = []\n for result in result_list:\n if result.strip():\n snippet = result[: self.max_snippet_length]\n data_results.append(\n Data(\n text=snippet,\n data={\n \"content\": result,\n \"snippet\": snippet,\n },\n )\n )\n except (ValueError, AttributeError) as e:\n error_data = [Data(text=str(e), data={\"error\": str(e)})]\n self.status = error_data\n return error_data\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "from langchain_community.tools import DuckDuckGoSearchRun\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import IntInput, MessageTextInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass DuckDuckGoSearchComponent(Component):\n \"\"\"Component for performing web searches using DuckDuckGo.\"\"\"\n\n display_name = \"DuckDuckGo Search\"\n description = \"Search the web using DuckDuckGo with customizable result limits\"\n documentation = \"https://python.langchain.com/docs/integrations/tools/ddg\"\n icon = \"DuckDuckGo\"\n\n inputs = [\n MessageTextInput(\n name=\"input_value\",\n display_name=\"Search Query\",\n required=True,\n info=\"The search query to execute with DuckDuckGo\",\n tool_mode=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n value=5,\n required=False,\n advanced=True,\n info=\"Maximum number of search results to return\",\n ),\n IntInput(\n name=\"max_snippet_length\",\n display_name=\"Max Snippet Length\",\n value=100,\n required=False,\n advanced=True,\n info=\"Maximum length of each result snippet\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def _build_wrapper(self) -> DuckDuckGoSearchRun:\n \"\"\"Build the DuckDuckGo search wrapper.\"\"\"\n return DuckDuckGoSearchRun()\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n \"\"\"Execute the search and return results as Data objects.\"\"\"\n try:\n wrapper = self._build_wrapper()\n\n full_results = wrapper.run(f\"{self.input_value} (site:*)\")\n\n result_list = full_results.split(\"\\n\")[: self.max_results]\n\n data_results = []\n for result in result_list:\n if result.strip():\n snippet = result[: self.max_snippet_length]\n data_results.append(\n Data(\n text=snippet,\n data={\n \"content\": result,\n \"snippet\": snippet,\n },\n )\n )\n except (ValueError, AttributeError) as e:\n error_data = [Data(text=str(e), data={\"error\": str(e)})]\n self.status = error_data\n return error_data\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" }, "input_value": { "_input_type": "MessageTextInput", @@ -66637,8 +66679,8 @@ { "Elasticsearch": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -66701,24 +66743,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -67046,8 +67088,8 @@ }, "OpenSearchVectorStoreComponent": { "base_classes": [ - "Data", - "DataFrame", + "JSON", + "Table", "VectorStore" ], "beta": false, @@ -67086,7 +67128,7 @@ "icon": "OpenSearch", "legacy": false, "metadata": { - "code_hash": "4968b4d34fad", + "code_hash": "f4dfc3668475", "dependencies": { "dependencies": [ { @@ -67112,24 +67154,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -67216,7 +67258,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport uuid\nfrom typing import Any\n\nfrom opensearchpy import OpenSearch, helpers\nfrom opensearchpy.exceptions import RequestError\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.base.vectorstores.vector_store_connection_decorator import vector_store_connection\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MultilineInput, SecretStrInput, StrInput, TableInput\nfrom lfx.log import logger\nfrom lfx.schema.data import Data\n\n\n@vector_store_connection\nclass OpenSearchVectorStoreComponent(LCVectorStoreComponent):\n \"\"\"OpenSearch Vector Store Component with Hybrid Search Capabilities.\n\n This component provides vector storage and retrieval using OpenSearch, combining semantic\n similarity search (KNN) with keyword-based search for optimal results. It supports document\n ingestion, vector embeddings, and advanced filtering with authentication options.\n\n Features:\n - Vector storage with configurable engines (jvector, nmslib, faiss, lucene)\n - Hybrid search combining KNN vector similarity and keyword matching\n - Flexible authentication (Basic auth, JWT tokens)\n - Advanced filtering and aggregations\n - Metadata injection during document ingestion\n \"\"\"\n\n display_name: str = \"OpenSearch\"\n icon: str = \"OpenSearch\"\n description: str = (\n \"Store and search documents using OpenSearch with hybrid semantic and keyword search capabilities.\"\n )\n\n # Keys we consider baseline\n default_keys: list[str] = [\n \"opensearch_url\",\n \"index_name\",\n *[i.name for i in LCVectorStoreComponent.inputs], # search_query, add_documents, etc.\n \"embedding\",\n \"vector_field\",\n \"number_of_results\",\n \"auth_mode\",\n \"username\",\n \"password\",\n \"jwt_token\",\n \"jwt_header\",\n \"bearer_prefix\",\n \"use_ssl\",\n \"verify_certs\",\n \"request_timeout\",\n \"filter_expression\",\n \"engine\",\n \"space_type\",\n \"ef_construction\",\n \"m\",\n \"docs_metadata\",\n ]\n\n inputs = [\n TableInput(\n name=\"docs_metadata\",\n display_name=\"Document Metadata\",\n info=(\n \"Additional metadata key-value pairs to be added to all ingested documents. \"\n \"Useful for tagging documents with source information, categories, or other custom attributes.\"\n ),\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Key name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Value of the metadata\",\n },\n ],\n value=[],\n input_types=[\"Data\"],\n ),\n StrInput(\n name=\"opensearch_url\",\n display_name=\"OpenSearch URL\",\n value=\"http://localhost:9200\",\n info=(\n \"The connection URL for your OpenSearch cluster \"\n \"(e.g., http://localhost:9200 for local development or your cloud endpoint).\"\n ),\n ),\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n value=\"langflow\",\n info=(\n \"The OpenSearch index name where documents will be stored and searched. \"\n \"Will be created automatically if it doesn't exist.\"\n ),\n ),\n DropdownInput(\n name=\"engine\",\n display_name=\"Vector Engine\",\n options=[\"nmslib\", \"faiss\", \"lucene\", \"jvector\"],\n value=\"jvector\",\n info=(\n \"Vector search engine for similarity calculations. 'nmslib' works with standard \"\n \"OpenSearch. 'jvector' requires OpenSearch 2.9+. 'lucene' requires index.knn: true. \"\n \"Amazon OpenSearch Serverless only supports 'nmslib' or 'faiss'.\"\n ),\n advanced=True,\n ),\n DropdownInput(\n name=\"space_type\",\n display_name=\"Distance Metric\",\n options=[\"l2\", \"l1\", \"cosinesimil\", \"linf\", \"innerproduct\"],\n value=\"l2\",\n info=(\n \"Distance metric for calculating vector similarity. 'l2' (Euclidean) is most common, \"\n \"'cosinesimil' for cosine similarity, 'innerproduct' for dot product.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"ef_construction\",\n display_name=\"EF Construction\",\n value=512,\n info=(\n \"Size of the dynamic candidate list during index construction. \"\n \"Higher values improve recall but increase indexing time and memory usage.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"m\",\n display_name=\"M Parameter\",\n value=16,\n info=(\n \"Number of bidirectional connections for each vector in the HNSW graph. \"\n \"Higher values improve search quality but increase memory usage and indexing time.\"\n ),\n advanced=True,\n ),\n *LCVectorStoreComponent.inputs, # includes search_query, add_documents, etc.\n HandleInput(name=\"embedding\", display_name=\"Embedding\", input_types=[\"Embeddings\"]),\n StrInput(\n name=\"vector_field\",\n display_name=\"Vector Field Name\",\n value=\"chunk_embedding\",\n advanced=True,\n info=\"Name of the field in OpenSearch documents that stores the vector embeddings for similarity search.\",\n ),\n IntInput(\n name=\"number_of_results\",\n display_name=\"Default Result Limit\",\n value=10,\n advanced=True,\n info=(\n \"Default maximum number of search results to return when no limit is \"\n \"specified in the filter expression.\"\n ),\n ),\n MultilineInput(\n name=\"filter_expression\",\n display_name=\"Search Filters (JSON)\",\n value=\"\",\n info=(\n \"Optional JSON configuration for search filtering, result limits, and score thresholds.\\n\\n\"\n \"Format 1 - Explicit filters:\\n\"\n '{\"filter\": [{\"term\": {\"filename\":\"doc.pdf\"}}, '\n '{\"terms\":{\"owner\":[\"user1\",\"user2\"]}}], \"limit\": 10, \"score_threshold\": 1.6}\\n\\n'\n \"Format 2 - Context-style mapping:\\n\"\n '{\"data_sources\":[\"file.pdf\"], \"document_types\":[\"application/pdf\"], \"owners\":[\"user123\"]}\\n\\n'\n \"Use __IMPOSSIBLE_VALUE__ as placeholder to ignore specific filters.\"\n ),\n ),\n # ----- Auth controls (dynamic) -----\n DropdownInput(\n name=\"auth_mode\",\n display_name=\"Authentication Mode\",\n value=\"basic\",\n options=[\"basic\", \"jwt\"],\n info=(\n \"Authentication method: 'basic' for username/password authentication, \"\n \"or 'jwt' for JSON Web Token (Bearer) authentication.\"\n ),\n real_time_refresh=True,\n advanced=False,\n ),\n StrInput(\n name=\"username\",\n display_name=\"Username\",\n value=\"admin\",\n show=True,\n ),\n SecretStrInput(\n name=\"password\",\n display_name=\"OpenSearch Password\",\n value=\"admin\",\n show=True,\n ),\n SecretStrInput(\n name=\"jwt_token\",\n display_name=\"JWT Token\",\n value=\"JWT\",\n load_from_db=False,\n show=False,\n info=(\n \"Valid JSON Web Token for authentication. \"\n \"Will be sent in the Authorization header (with optional 'Bearer ' prefix).\"\n ),\n ),\n StrInput(\n name=\"jwt_header\",\n display_name=\"JWT Header Name\",\n value=\"Authorization\",\n show=False,\n advanced=True,\n ),\n BoolInput(\n name=\"bearer_prefix\",\n display_name=\"Prefix 'Bearer '\",\n value=True,\n show=False,\n advanced=True,\n ),\n # ----- TLS -----\n BoolInput(\n name=\"use_ssl\",\n display_name=\"Use SSL/TLS\",\n value=True,\n advanced=True,\n info=\"Enable SSL/TLS encryption for secure connections to OpenSearch.\",\n ),\n BoolInput(\n name=\"verify_certs\",\n display_name=\"Verify SSL Certificates\",\n value=False,\n advanced=True,\n info=(\n \"Verify SSL certificates when connecting. \"\n \"Disable for self-signed certificates in development environments.\"\n ),\n ),\n IntInput(\n name=\"request_timeout\",\n display_name=\"Request Timeout (seconds)\",\n value=60,\n advanced=True,\n info=(\n \"Time in seconds to wait for a response from OpenSearch (transport and per-request). \"\n \"Used for the default transport timeout and for bulk ingest HTTP calls. \"\n \"Increase for large bulk ingestion or slow clusters.\"\n ),\n ),\n ]\n\n # ---------- helper functions for index management ----------\n def _default_text_mapping(\n self,\n dim: int,\n engine: str = \"jvector\",\n space_type: str = \"l2\",\n ef_search: int = 512,\n ef_construction: int = 100,\n m: int = 16,\n vector_field: str = \"vector_field\",\n ) -> dict[str, Any]:\n \"\"\"Create the default OpenSearch index mapping for vector search.\n\n This method generates the index configuration with k-NN settings optimized\n for approximate nearest neighbor search using the specified vector engine.\n\n Args:\n dim: Dimensionality of the vector embeddings\n engine: Vector search engine (jvector, nmslib, faiss, lucene)\n space_type: Distance metric for similarity calculation\n ef_search: Size of dynamic list used during search\n ef_construction: Size of dynamic list used during index construction\n m: Number of bidirectional links for each vector\n vector_field: Name of the field storing vector embeddings\n\n Returns:\n Dictionary containing OpenSearch index mapping configuration\n \"\"\"\n return {\n \"settings\": {\"index\": {\"knn\": True, \"knn.algo_param.ef_search\": ef_search}},\n \"mappings\": {\n \"properties\": {\n vector_field: {\n \"type\": \"knn_vector\",\n \"dimension\": dim,\n \"method\": {\n \"name\": \"disk_ann\",\n \"space_type\": space_type,\n \"engine\": engine,\n \"parameters\": {\"ef_construction\": ef_construction, \"m\": m},\n },\n }\n }\n },\n }\n\n def _validate_aoss_with_engines(self, *, is_aoss: bool, engine: str) -> None:\n \"\"\"Validate engine compatibility with Amazon OpenSearch Serverless (AOSS).\n\n Amazon OpenSearch Serverless has restrictions on which vector engines\n can be used. This method ensures the selected engine is compatible.\n\n Args:\n is_aoss: Whether the connection is to Amazon OpenSearch Serverless\n engine: The selected vector search engine\n\n Raises:\n ValueError: If AOSS is used with an incompatible engine\n \"\"\"\n if is_aoss and engine not in {\"nmslib\", \"faiss\"}:\n msg = \"Amazon OpenSearch Service Serverless only supports `nmslib` or `faiss` engines\"\n raise ValueError(msg)\n\n def _get_request_timeout(self) -> int:\n \"\"\"Return the configured request timeout in seconds (default 60).\"\"\"\n if not hasattr(self, \"request_timeout\") or self.request_timeout is None:\n return 60\n try:\n t = int(self.request_timeout)\n except (TypeError, ValueError):\n return 60\n else:\n return t if t >= 1 else 60\n\n def _is_aoss_enabled(self, http_auth: Any) -> bool:\n \"\"\"Determine if Amazon OpenSearch Serverless (AOSS) is being used.\n\n Args:\n http_auth: The HTTP authentication object\n\n Returns:\n True if AOSS is enabled, False otherwise\n \"\"\"\n return http_auth is not None and hasattr(http_auth, \"service\") and http_auth.service == \"aoss\"\n\n def _bulk_ingest_embeddings(\n self,\n client: OpenSearch,\n index_name: str,\n embeddings: list[list[float]],\n texts: list[str],\n metadatas: list[dict] | None = None,\n ids: list[str] | None = None,\n vector_field: str = \"vector_field\",\n text_field: str = \"text\",\n mapping: dict | None = None,\n max_chunk_bytes: int | None = 1 * 1024 * 1024,\n *,\n is_aoss: bool = False,\n ) -> list[str]:\n \"\"\"Efficiently ingest multiple documents with embeddings into OpenSearch.\n\n This method uses bulk operations to insert documents with their vector\n embeddings and metadata into the specified OpenSearch index.\n\n Args:\n client: OpenSearch client instance\n index_name: Target index for document storage\n embeddings: List of vector embeddings for each document\n texts: List of document texts\n metadatas: Optional metadata dictionaries for each document\n ids: Optional document IDs (UUIDs generated if not provided)\n vector_field: Field name for storing vector embeddings\n text_field: Field name for storing document text\n mapping: Optional index mapping configuration\n max_chunk_bytes: Maximum size per bulk request chunk\n is_aoss: Whether using Amazon OpenSearch Serverless\n\n Returns:\n List of document IDs that were successfully ingested\n \"\"\"\n if not mapping:\n mapping = {}\n\n requests = []\n return_ids = []\n\n for i, text in enumerate(texts):\n metadata = metadatas[i] if metadatas else {}\n _id = ids[i] if ids else str(uuid.uuid4())\n request = {\n \"_op_type\": \"index\",\n \"_index\": index_name,\n vector_field: embeddings[i],\n text_field: text,\n **metadata,\n }\n if is_aoss:\n request[\"id\"] = _id\n else:\n request[\"_id\"] = _id\n requests.append(request)\n return_ids.append(_id)\n if metadatas:\n self.log(f\"Sample metadata: {metadatas[0] if metadatas else {}}\")\n helpers.bulk(\n client,\n requests,\n max_chunk_bytes=max_chunk_bytes,\n request_timeout=self._get_request_timeout(),\n )\n return return_ids\n\n # ---------- auth / client ----------\n def _build_auth_kwargs(self) -> dict[str, Any]:\n \"\"\"Build authentication configuration for OpenSearch client.\n\n Constructs the appropriate authentication parameters based on the\n selected auth mode (basic username/password or JWT token).\n\n Returns:\n Dictionary containing authentication configuration\n\n Raises:\n ValueError: If required authentication parameters are missing\n \"\"\"\n mode = (self.auth_mode or \"basic\").strip().lower()\n if mode == \"jwt\":\n token = (self.jwt_token or \"\").strip()\n if not token:\n msg = \"Auth Mode is 'jwt' but no jwt_token was provided.\"\n raise ValueError(msg)\n header_name = (self.jwt_header or \"Authorization\").strip()\n header_value = f\"Bearer {token}\" if self.bearer_prefix else token\n return {\"headers\": {header_name: header_value}}\n user = (self.username or \"\").strip()\n pwd = (self.password or \"\").strip()\n if not user or not pwd:\n msg = \"Auth Mode is 'basic' but username/password are missing.\"\n raise ValueError(msg)\n return {\"http_auth\": (user, pwd)}\n\n def build_client(self) -> OpenSearch:\n \"\"\"Create and configure an OpenSearch client instance.\n\n Returns:\n Configured OpenSearch client ready for operations\n \"\"\"\n auth_kwargs = self._build_auth_kwargs()\n return OpenSearch(\n hosts=[self.opensearch_url],\n use_ssl=self.use_ssl,\n verify_certs=self.verify_certs,\n ssl_assert_hostname=False,\n ssl_show_warn=False,\n timeout=self._get_request_timeout(),\n **auth_kwargs,\n )\n\n @check_cached_vector_store\n def build_vector_store(self) -> OpenSearch:\n # Return raw OpenSearch client as our “vector store.”\n self.log(self.ingest_data)\n client = self.build_client()\n self._add_documents_to_vector_store(client=client)\n return client\n\n # ---------- ingest ----------\n def _add_documents_to_vector_store(self, client: OpenSearch) -> None:\n \"\"\"Process and ingest documents into the OpenSearch vector store.\n\n This method handles the complete document ingestion pipeline:\n - Prepares document data and metadata\n - Generates vector embeddings\n - Creates appropriate index mappings\n - Bulk inserts documents with vectors\n\n Args:\n client: OpenSearch client for performing operations\n \"\"\"\n # Convert DataFrame to Data if needed using parent's method\n self.ingest_data = self._prepare_ingest_data()\n\n docs = self.ingest_data or []\n if not docs:\n self.log(\"No documents to ingest.\")\n return\n\n # Extract texts and metadata from documents\n texts = []\n metadatas = []\n # Process docs_metadata table input into a dict\n additional_metadata = {}\n if hasattr(self, \"docs_metadata\") and self.docs_metadata:\n logger.debug(f\"[LF] Docs metadata {self.docs_metadata}\")\n if isinstance(self.docs_metadata[-1], Data):\n logger.debug(f\"[LF] Docs metadata is a Data object {self.docs_metadata}\")\n self.docs_metadata = self.docs_metadata[-1].data\n logger.debug(f\"[LF] Docs metadata is a Data object {self.docs_metadata}\")\n additional_metadata.update(self.docs_metadata)\n else:\n for item in self.docs_metadata:\n if isinstance(item, dict) and \"key\" in item and \"value\" in item:\n additional_metadata[item[\"key\"]] = item[\"value\"]\n # Replace string \"None\" values with actual None\n for key, value in additional_metadata.items():\n if value == \"None\":\n additional_metadata[key] = None\n logger.debug(f\"[LF] Additional metadata {additional_metadata}\")\n for doc_obj in docs:\n data_copy = json.loads(doc_obj.model_dump_json())\n text = data_copy.pop(doc_obj.text_key, doc_obj.default_value)\n texts.append(text)\n\n # Merge additional metadata from table input\n data_copy.update(additional_metadata)\n\n metadatas.append(data_copy)\n self.log(metadatas)\n if not self.embedding:\n msg = \"Embedding handle is required to embed documents.\"\n raise ValueError(msg)\n\n # Generate embeddings\n vectors = self.embedding.embed_documents(texts)\n\n if not vectors:\n self.log(\"No vectors generated from documents.\")\n return\n\n # Get vector dimension for mapping\n dim = len(vectors[0]) if vectors else 768 # default fallback\n\n # Check for AOSS\n auth_kwargs = self._build_auth_kwargs()\n is_aoss = self._is_aoss_enabled(auth_kwargs.get(\"http_auth\"))\n\n # Validate engine with AOSS\n engine = getattr(self, \"engine\", \"jvector\")\n self._validate_aoss_with_engines(is_aoss=is_aoss, engine=engine)\n\n # Create mapping with proper KNN settings\n space_type = getattr(self, \"space_type\", \"l2\")\n ef_construction = getattr(self, \"ef_construction\", 512)\n m = getattr(self, \"m\", 16)\n\n mapping = self._default_text_mapping(\n dim=dim,\n engine=engine,\n space_type=space_type,\n ef_construction=ef_construction,\n m=m,\n vector_field=self.vector_field,\n )\n\n # Ensure index exists with proper KNN mapping (index.knn: true is required for vector search)\n try:\n if not client.indices.exists(index=self.index_name):\n self.log(f\"Creating index '{self.index_name}' with KNN mapping (index.knn: true)\")\n client.indices.create(index=self.index_name, body=mapping)\n except RequestError as creation_error:\n error_msg = str(creation_error).lower()\n if \"invalid engine\" in error_msg or \"illegal_argument\" in error_msg:\n if \"jvector\" in error_msg:\n msg = (\n \"The 'jvector' engine is not available in your OpenSearch installation. \"\n \"Use 'nmslib' or 'faiss' for standard OpenSearch, or upgrade to OpenSearch 2.9+.\"\n )\n raise ValueError(msg) from creation_error\n if \"index.knn\" in error_msg:\n msg = (\n \"The index has index.knn: false. Delete the existing index and let the \"\n \"component recreate it, or create a new index with a different name.\"\n )\n raise ValueError(msg) from creation_error\n raise\n\n self.log(f\"Indexing {len(texts)} documents into '{self.index_name}' with proper KNN mapping...\")\n\n # Use the LangChain-style bulk ingestion\n return_ids = self._bulk_ingest_embeddings(\n client=client,\n index_name=self.index_name,\n embeddings=vectors,\n texts=texts,\n metadatas=metadatas,\n vector_field=self.vector_field,\n text_field=\"text\",\n mapping=mapping,\n is_aoss=is_aoss,\n )\n self.log(metadatas)\n\n self.log(f\"Successfully indexed {len(return_ids)} documents.\")\n\n # ---------- helpers for filters ----------\n def _is_placeholder_term(self, term_obj: dict) -> bool:\n # term_obj like {\"filename\": \"__IMPOSSIBLE_VALUE__\"}\n return any(v == \"__IMPOSSIBLE_VALUE__\" for v in term_obj.values())\n\n def _coerce_filter_clauses(self, filter_obj: dict | None) -> list[dict]:\n \"\"\"Convert filter expressions into OpenSearch-compatible filter clauses.\n\n This method accepts two filter formats and converts them to standardized\n OpenSearch query clauses:\n\n Format A - Explicit filters:\n {\"filter\": [{\"term\": {\"field\": \"value\"}}, {\"terms\": {\"field\": [\"val1\", \"val2\"]}}],\n \"limit\": 10, \"score_threshold\": 1.5}\n\n Format B - Context-style mapping:\n {\"data_sources\": [\"file1.pdf\"], \"document_types\": [\"pdf\"], \"owners\": [\"user1\"]}\n\n Args:\n filter_obj: Filter configuration dictionary or None\n\n Returns:\n List of OpenSearch filter clauses (term/terms objects)\n Placeholder values with \"__IMPOSSIBLE_VALUE__\" are ignored\n \"\"\"\n if not filter_obj:\n return []\n\n # If it is a string, try to parse it once\n if isinstance(filter_obj, str):\n try:\n filter_obj = json.loads(filter_obj)\n except json.JSONDecodeError:\n # Not valid JSON - treat as no filters\n return []\n\n # Case A: already an explicit list/dict under \"filter\"\n if \"filter\" in filter_obj:\n raw = filter_obj[\"filter\"]\n if isinstance(raw, dict):\n raw = [raw]\n explicit_clauses: list[dict] = []\n for f in raw or []:\n if \"term\" in f and isinstance(f[\"term\"], dict) and not self._is_placeholder_term(f[\"term\"]):\n explicit_clauses.append(f)\n elif \"terms\" in f and isinstance(f[\"terms\"], dict):\n field, vals = next(iter(f[\"terms\"].items()))\n if isinstance(vals, list) and len(vals) > 0:\n explicit_clauses.append(f)\n return explicit_clauses\n\n # Case B: convert context-style maps into clauses\n field_mapping = {\n \"data_sources\": \"filename\",\n \"document_types\": \"mimetype\",\n \"owners\": \"owner\",\n }\n context_clauses: list[dict] = []\n for k, values in filter_obj.items():\n if not isinstance(values, list):\n continue\n field = field_mapping.get(k, k)\n if len(values) == 0:\n # Match-nothing placeholder (kept to mirror your tool semantics)\n context_clauses.append({\"term\": {field: \"__IMPOSSIBLE_VALUE__\"}})\n elif len(values) == 1:\n if values[0] != \"__IMPOSSIBLE_VALUE__\":\n context_clauses.append({\"term\": {field: values[0]}})\n else:\n context_clauses.append({\"terms\": {field: values}})\n return context_clauses\n\n # ---------- search (single hybrid path matching your tool) ----------\n def search(self, query: str | None = None) -> list[dict[str, Any]]:\n \"\"\"Perform hybrid search combining vector similarity and keyword matching.\n\n This method executes a sophisticated search that combines:\n - K-nearest neighbor (KNN) vector similarity search (70% weight)\n - Multi-field keyword search with fuzzy matching (30% weight)\n - Optional filtering and score thresholds\n - Aggregations for faceted search results\n\n Args:\n query: Search query string (used for both vector embedding and keyword search)\n\n Returns:\n List of search results with page_content, metadata, and relevance scores\n\n Raises:\n ValueError: If embedding component is not provided or filter JSON is invalid\n \"\"\"\n logger.info(self.ingest_data)\n client = self.build_client()\n q = (query or \"\").strip()\n\n # Parse optional filter expression (can be either A or B shape; see _coerce_filter_clauses)\n filter_obj = None\n if getattr(self, \"filter_expression\", \"\") and self.filter_expression.strip():\n try:\n filter_obj = json.loads(self.filter_expression)\n except json.JSONDecodeError as e:\n msg = f\"Invalid filter_expression JSON: {e}\"\n raise ValueError(msg) from e\n\n if not self.embedding:\n msg = \"Embedding is required to run hybrid search (KNN + keyword).\"\n raise ValueError(msg)\n\n # Embed the query\n vec = self.embedding.embed_query(q)\n\n # Build filter clauses (accept both shapes)\n filter_clauses = self._coerce_filter_clauses(filter_obj)\n\n # Respect the tool's limit/threshold defaults\n limit = (filter_obj or {}).get(\"limit\", self.number_of_results)\n score_threshold = (filter_obj or {}).get(\"score_threshold\", 0)\n\n # Build the same hybrid body as your SearchService\n body = {\n \"query\": {\n \"bool\": {\n \"should\": [\n {\n \"knn\": {\n self.vector_field: {\n \"vector\": vec,\n \"k\": 10, # fixed to match the tool\n \"boost\": 0.7,\n }\n }\n },\n {\n \"multi_match\": {\n \"query\": q,\n \"fields\": [\"text^2\", \"filename^1.5\"],\n \"type\": \"best_fields\",\n \"fuzziness\": \"AUTO\",\n \"boost\": 0.3,\n }\n },\n ],\n \"minimum_should_match\": 1,\n }\n },\n \"aggs\": {\n \"data_sources\": {\"terms\": {\"field\": \"filename\", \"size\": 20}},\n \"document_types\": {\"terms\": {\"field\": \"mimetype\", \"size\": 10}},\n \"owners\": {\"terms\": {\"field\": \"owner\", \"size\": 10}},\n },\n \"_source\": [\n \"filename\",\n \"mimetype\",\n \"page\",\n \"text\",\n \"source_url\",\n \"owner\",\n \"allowed_users\",\n \"allowed_groups\",\n ],\n \"size\": limit,\n }\n if filter_clauses:\n body[\"query\"][\"bool\"][\"filter\"] = filter_clauses\n\n if isinstance(score_threshold, (int, float)) and score_threshold > 0:\n # top-level min_score (matches your tool)\n body[\"min_score\"] = score_threshold\n\n resp = client.search(index=self.index_name, body=body)\n hits = resp.get(\"hits\", {}).get(\"hits\", [])\n return [\n {\n \"page_content\": hit[\"_source\"].get(\"text\", \"\"),\n \"metadata\": {k: v for k, v in hit[\"_source\"].items() if k != \"text\"},\n \"score\": hit.get(\"_score\"),\n }\n for hit in hits\n ]\n\n def search_documents(self) -> list[Data]:\n \"\"\"Search documents and return results as Data objects.\n\n This is the main interface method that performs the search using the\n configured search_query and returns results in Langflow's Data format.\n\n Returns:\n List of Data objects containing search results with text and metadata\n\n Raises:\n Exception: If search operation fails\n \"\"\"\n try:\n raw = self.search(self.search_query or \"\")\n return [Data(text=hit[\"page_content\"], **hit[\"metadata\"]) for hit in raw]\n self.log(self.ingest_data)\n except Exception as e:\n self.log(f\"search_documents error: {e}\")\n raise\n\n # -------- dynamic UI handling (auth switch) --------\n async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n \"\"\"Dynamically update component configuration based on field changes.\n\n This method handles real-time UI updates, particularly for authentication\n mode changes that show/hide relevant input fields.\n\n Args:\n build_config: Current component configuration\n field_value: New value for the changed field\n field_name: Name of the field that changed\n\n Returns:\n Updated build configuration with appropriate field visibility\n \"\"\"\n try:\n if field_name == \"auth_mode\":\n mode = (field_value or \"basic\").strip().lower()\n is_basic = mode == \"basic\"\n is_jwt = mode == \"jwt\"\n\n build_config[\"username\"][\"show\"] = is_basic\n build_config[\"password\"][\"show\"] = is_basic\n\n build_config[\"jwt_token\"][\"show\"] = is_jwt\n build_config[\"jwt_header\"][\"show\"] = is_jwt\n build_config[\"bearer_prefix\"][\"show\"] = is_jwt\n\n build_config[\"username\"][\"required\"] = is_basic\n build_config[\"password\"][\"required\"] = is_basic\n\n build_config[\"jwt_token\"][\"required\"] = is_jwt\n build_config[\"jwt_header\"][\"required\"] = is_jwt\n build_config[\"bearer_prefix\"][\"required\"] = False\n\n if is_basic:\n build_config[\"jwt_token\"][\"value\"] = \"\"\n\n return build_config\n\n except (KeyError, ValueError) as e:\n self.log(f\"update_build_config error: {e}\")\n\n return build_config\n" + "value": "from __future__ import annotations\n\nimport json\nimport uuid\nfrom typing import Any\n\nfrom opensearchpy import OpenSearch, helpers\nfrom opensearchpy.exceptions import RequestError\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.base.vectorstores.vector_store_connection_decorator import vector_store_connection\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MultilineInput, SecretStrInput, StrInput, TableInput\nfrom lfx.log import logger\nfrom lfx.schema.data import Data\n\n\n@vector_store_connection\nclass OpenSearchVectorStoreComponent(LCVectorStoreComponent):\n \"\"\"OpenSearch Vector Store Component with Hybrid Search Capabilities.\n\n This component provides vector storage and retrieval using OpenSearch, combining semantic\n similarity search (KNN) with keyword-based search for optimal results. It supports document\n ingestion, vector embeddings, and advanced filtering with authentication options.\n\n Features:\n - Vector storage with configurable engines (jvector, nmslib, faiss, lucene)\n - Hybrid search combining KNN vector similarity and keyword matching\n - Flexible authentication (Basic auth, JWT tokens)\n - Advanced filtering and aggregations\n - Metadata injection during document ingestion\n \"\"\"\n\n display_name: str = \"OpenSearch\"\n icon: str = \"OpenSearch\"\n description: str = (\n \"Store and search documents using OpenSearch with hybrid semantic and keyword search capabilities.\"\n )\n\n # Keys we consider baseline\n default_keys: list[str] = [\n \"opensearch_url\",\n \"index_name\",\n *[i.name for i in LCVectorStoreComponent.inputs], # search_query, add_documents, etc.\n \"embedding\",\n \"vector_field\",\n \"number_of_results\",\n \"auth_mode\",\n \"username\",\n \"password\",\n \"jwt_token\",\n \"jwt_header\",\n \"bearer_prefix\",\n \"use_ssl\",\n \"verify_certs\",\n \"request_timeout\",\n \"filter_expression\",\n \"engine\",\n \"space_type\",\n \"ef_construction\",\n \"m\",\n \"docs_metadata\",\n ]\n\n inputs = [\n TableInput(\n name=\"docs_metadata\",\n display_name=\"Document Metadata\",\n info=(\n \"Additional metadata key-value pairs to be added to all ingested documents. \"\n \"Useful for tagging documents with source information, categories, or other custom attributes.\"\n ),\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Key name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Value of the metadata\",\n },\n ],\n value=[],\n input_types=[\"Data\", \"JSON\"],\n ),\n StrInput(\n name=\"opensearch_url\",\n display_name=\"OpenSearch URL\",\n value=\"http://localhost:9200\",\n info=(\n \"The connection URL for your OpenSearch cluster \"\n \"(e.g., http://localhost:9200 for local development or your cloud endpoint).\"\n ),\n ),\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n value=\"langflow\",\n info=(\n \"The OpenSearch index name where documents will be stored and searched. \"\n \"Will be created automatically if it doesn't exist.\"\n ),\n ),\n DropdownInput(\n name=\"engine\",\n display_name=\"Vector Engine\",\n options=[\"nmslib\", \"faiss\", \"lucene\", \"jvector\"],\n value=\"jvector\",\n info=(\n \"Vector search engine for similarity calculations. 'nmslib' works with standard \"\n \"OpenSearch. 'jvector' requires OpenSearch 2.9+. 'lucene' requires index.knn: true. \"\n \"Amazon OpenSearch Serverless only supports 'nmslib' or 'faiss'.\"\n ),\n advanced=True,\n ),\n DropdownInput(\n name=\"space_type\",\n display_name=\"Distance Metric\",\n options=[\"l2\", \"l1\", \"cosinesimil\", \"linf\", \"innerproduct\"],\n value=\"l2\",\n info=(\n \"Distance metric for calculating vector similarity. 'l2' (Euclidean) is most common, \"\n \"'cosinesimil' for cosine similarity, 'innerproduct' for dot product.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"ef_construction\",\n display_name=\"EF Construction\",\n value=512,\n info=(\n \"Size of the dynamic candidate list during index construction. \"\n \"Higher values improve recall but increase indexing time and memory usage.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"m\",\n display_name=\"M Parameter\",\n value=16,\n info=(\n \"Number of bidirectional connections for each vector in the HNSW graph. \"\n \"Higher values improve search quality but increase memory usage and indexing time.\"\n ),\n advanced=True,\n ),\n *LCVectorStoreComponent.inputs, # includes search_query, add_documents, etc.\n HandleInput(name=\"embedding\", display_name=\"Embedding\", input_types=[\"Embeddings\"]),\n StrInput(\n name=\"vector_field\",\n display_name=\"Vector Field Name\",\n value=\"chunk_embedding\",\n advanced=True,\n info=\"Name of the field in OpenSearch documents that stores the vector embeddings for similarity search.\",\n ),\n IntInput(\n name=\"number_of_results\",\n display_name=\"Default Result Limit\",\n value=10,\n advanced=True,\n info=(\n \"Default maximum number of search results to return when no limit is \"\n \"specified in the filter expression.\"\n ),\n ),\n MultilineInput(\n name=\"filter_expression\",\n display_name=\"Search Filters (JSON)\",\n value=\"\",\n info=(\n \"Optional JSON configuration for search filtering, result limits, and score thresholds.\\n\\n\"\n \"Format 1 - Explicit filters:\\n\"\n '{\"filter\": [{\"term\": {\"filename\":\"doc.pdf\"}}, '\n '{\"terms\":{\"owner\":[\"user1\",\"user2\"]}}], \"limit\": 10, \"score_threshold\": 1.6}\\n\\n'\n \"Format 2 - Context-style mapping:\\n\"\n '{\"data_sources\":[\"file.pdf\"], \"document_types\":[\"application/pdf\"], \"owners\":[\"user123\"]}\\n\\n'\n \"Use __IMPOSSIBLE_VALUE__ as placeholder to ignore specific filters.\"\n ),\n ),\n # ----- Auth controls (dynamic) -----\n DropdownInput(\n name=\"auth_mode\",\n display_name=\"Authentication Mode\",\n value=\"basic\",\n options=[\"basic\", \"jwt\"],\n info=(\n \"Authentication method: 'basic' for username/password authentication, \"\n \"or 'jwt' for JSON Web Token (Bearer) authentication.\"\n ),\n real_time_refresh=True,\n advanced=False,\n ),\n StrInput(\n name=\"username\",\n display_name=\"Username\",\n value=\"admin\",\n show=True,\n ),\n SecretStrInput(\n name=\"password\",\n display_name=\"OpenSearch Password\",\n value=\"admin\",\n show=True,\n ),\n SecretStrInput(\n name=\"jwt_token\",\n display_name=\"JWT Token\",\n value=\"JWT\",\n load_from_db=False,\n show=False,\n info=(\n \"Valid JSON Web Token for authentication. \"\n \"Will be sent in the Authorization header (with optional 'Bearer ' prefix).\"\n ),\n ),\n StrInput(\n name=\"jwt_header\",\n display_name=\"JWT Header Name\",\n value=\"Authorization\",\n show=False,\n advanced=True,\n ),\n BoolInput(\n name=\"bearer_prefix\",\n display_name=\"Prefix 'Bearer '\",\n value=True,\n show=False,\n advanced=True,\n ),\n # ----- TLS -----\n BoolInput(\n name=\"use_ssl\",\n display_name=\"Use SSL/TLS\",\n value=True,\n advanced=True,\n info=\"Enable SSL/TLS encryption for secure connections to OpenSearch.\",\n ),\n BoolInput(\n name=\"verify_certs\",\n display_name=\"Verify SSL Certificates\",\n value=False,\n advanced=True,\n info=(\n \"Verify SSL certificates when connecting. \"\n \"Disable for self-signed certificates in development environments.\"\n ),\n ),\n IntInput(\n name=\"request_timeout\",\n display_name=\"Request Timeout (seconds)\",\n value=60,\n advanced=True,\n info=(\n \"Time in seconds to wait for a response from OpenSearch (transport and per-request). \"\n \"Used for the default transport timeout and for bulk ingest HTTP calls. \"\n \"Increase for large bulk ingestion or slow clusters.\"\n ),\n ),\n ]\n\n # ---------- helper functions for index management ----------\n def _default_text_mapping(\n self,\n dim: int,\n engine: str = \"jvector\",\n space_type: str = \"l2\",\n ef_search: int = 512,\n ef_construction: int = 100,\n m: int = 16,\n vector_field: str = \"vector_field\",\n ) -> dict[str, Any]:\n \"\"\"Create the default OpenSearch index mapping for vector search.\n\n This method generates the index configuration with k-NN settings optimized\n for approximate nearest neighbor search using the specified vector engine.\n\n Args:\n dim: Dimensionality of the vector embeddings\n engine: Vector search engine (jvector, nmslib, faiss, lucene)\n space_type: Distance metric for similarity calculation\n ef_search: Size of dynamic list used during search\n ef_construction: Size of dynamic list used during index construction\n m: Number of bidirectional links for each vector\n vector_field: Name of the field storing vector embeddings\n\n Returns:\n Dictionary containing OpenSearch index mapping configuration\n \"\"\"\n return {\n \"settings\": {\"index\": {\"knn\": True, \"knn.algo_param.ef_search\": ef_search}},\n \"mappings\": {\n \"properties\": {\n vector_field: {\n \"type\": \"knn_vector\",\n \"dimension\": dim,\n \"method\": {\n \"name\": \"disk_ann\",\n \"space_type\": space_type,\n \"engine\": engine,\n \"parameters\": {\"ef_construction\": ef_construction, \"m\": m},\n },\n }\n }\n },\n }\n\n def _validate_aoss_with_engines(self, *, is_aoss: bool, engine: str) -> None:\n \"\"\"Validate engine compatibility with Amazon OpenSearch Serverless (AOSS).\n\n Amazon OpenSearch Serverless has restrictions on which vector engines\n can be used. This method ensures the selected engine is compatible.\n\n Args:\n is_aoss: Whether the connection is to Amazon OpenSearch Serverless\n engine: The selected vector search engine\n\n Raises:\n ValueError: If AOSS is used with an incompatible engine\n \"\"\"\n if is_aoss and engine not in {\"nmslib\", \"faiss\"}:\n msg = \"Amazon OpenSearch Service Serverless only supports `nmslib` or `faiss` engines\"\n raise ValueError(msg)\n\n def _get_request_timeout(self) -> int:\n \"\"\"Return the configured request timeout in seconds (default 60).\"\"\"\n if not hasattr(self, \"request_timeout\") or self.request_timeout is None:\n return 60\n try:\n t = int(self.request_timeout)\n except (TypeError, ValueError):\n return 60\n else:\n return t if t >= 1 else 60\n\n def _is_aoss_enabled(self, http_auth: Any) -> bool:\n \"\"\"Determine if Amazon OpenSearch Serverless (AOSS) is being used.\n\n Args:\n http_auth: The HTTP authentication object\n\n Returns:\n True if AOSS is enabled, False otherwise\n \"\"\"\n return http_auth is not None and hasattr(http_auth, \"service\") and http_auth.service == \"aoss\"\n\n def _bulk_ingest_embeddings(\n self,\n client: OpenSearch,\n index_name: str,\n embeddings: list[list[float]],\n texts: list[str],\n metadatas: list[dict] | None = None,\n ids: list[str] | None = None,\n vector_field: str = \"vector_field\",\n text_field: str = \"text\",\n mapping: dict | None = None,\n max_chunk_bytes: int | None = 1 * 1024 * 1024,\n *,\n is_aoss: bool = False,\n ) -> list[str]:\n \"\"\"Efficiently ingest multiple documents with embeddings into OpenSearch.\n\n This method uses bulk operations to insert documents with their vector\n embeddings and metadata into the specified OpenSearch index.\n\n Args:\n client: OpenSearch client instance\n index_name: Target index for document storage\n embeddings: List of vector embeddings for each document\n texts: List of document texts\n metadatas: Optional metadata dictionaries for each document\n ids: Optional document IDs (UUIDs generated if not provided)\n vector_field: Field name for storing vector embeddings\n text_field: Field name for storing document text\n mapping: Optional index mapping configuration\n max_chunk_bytes: Maximum size per bulk request chunk\n is_aoss: Whether using Amazon OpenSearch Serverless\n\n Returns:\n List of document IDs that were successfully ingested\n \"\"\"\n if not mapping:\n mapping = {}\n\n requests = []\n return_ids = []\n\n for i, text in enumerate(texts):\n metadata = metadatas[i] if metadatas else {}\n _id = ids[i] if ids else str(uuid.uuid4())\n request = {\n \"_op_type\": \"index\",\n \"_index\": index_name,\n vector_field: embeddings[i],\n text_field: text,\n **metadata,\n }\n if is_aoss:\n request[\"id\"] = _id\n else:\n request[\"_id\"] = _id\n requests.append(request)\n return_ids.append(_id)\n if metadatas:\n self.log(f\"Sample metadata: {metadatas[0] if metadatas else {}}\")\n helpers.bulk(\n client,\n requests,\n max_chunk_bytes=max_chunk_bytes,\n request_timeout=self._get_request_timeout(),\n )\n return return_ids\n\n # ---------- auth / client ----------\n def _build_auth_kwargs(self) -> dict[str, Any]:\n \"\"\"Build authentication configuration for OpenSearch client.\n\n Constructs the appropriate authentication parameters based on the\n selected auth mode (basic username/password or JWT token).\n\n Returns:\n Dictionary containing authentication configuration\n\n Raises:\n ValueError: If required authentication parameters are missing\n \"\"\"\n mode = (self.auth_mode or \"basic\").strip().lower()\n if mode == \"jwt\":\n token = (self.jwt_token or \"\").strip()\n if not token:\n msg = \"Auth Mode is 'jwt' but no jwt_token was provided.\"\n raise ValueError(msg)\n header_name = (self.jwt_header or \"Authorization\").strip()\n header_value = f\"Bearer {token}\" if self.bearer_prefix else token\n return {\"headers\": {header_name: header_value}}\n user = (self.username or \"\").strip()\n pwd = (self.password or \"\").strip()\n if not user or not pwd:\n msg = \"Auth Mode is 'basic' but username/password are missing.\"\n raise ValueError(msg)\n return {\"http_auth\": (user, pwd)}\n\n def build_client(self) -> OpenSearch:\n \"\"\"Create and configure an OpenSearch client instance.\n\n Returns:\n Configured OpenSearch client ready for operations\n \"\"\"\n auth_kwargs = self._build_auth_kwargs()\n return OpenSearch(\n hosts=[self.opensearch_url],\n use_ssl=self.use_ssl,\n verify_certs=self.verify_certs,\n ssl_assert_hostname=False,\n ssl_show_warn=False,\n timeout=self._get_request_timeout(),\n **auth_kwargs,\n )\n\n @check_cached_vector_store\n def build_vector_store(self) -> OpenSearch:\n # Return raw OpenSearch client as our “vector store.”\n self.log(self.ingest_data)\n client = self.build_client()\n self._add_documents_to_vector_store(client=client)\n return client\n\n # ---------- ingest ----------\n def _add_documents_to_vector_store(self, client: OpenSearch) -> None:\n \"\"\"Process and ingest documents into the OpenSearch vector store.\n\n This method handles the complete document ingestion pipeline:\n - Prepares document data and metadata\n - Generates vector embeddings\n - Creates appropriate index mappings\n - Bulk inserts documents with vectors\n\n Args:\n client: OpenSearch client for performing operations\n \"\"\"\n # Convert DataFrame to Data if needed using parent's method\n self.ingest_data = self._prepare_ingest_data()\n\n docs = self.ingest_data or []\n if not docs:\n self.log(\"No documents to ingest.\")\n return\n\n # Extract texts and metadata from documents\n texts = []\n metadatas = []\n # Process docs_metadata table input into a dict\n additional_metadata = {}\n if hasattr(self, \"docs_metadata\") and self.docs_metadata:\n logger.debug(f\"[LF] Docs metadata {self.docs_metadata}\")\n if isinstance(self.docs_metadata[-1], Data):\n logger.debug(f\"[LF] Docs metadata is a Data object {self.docs_metadata}\")\n self.docs_metadata = self.docs_metadata[-1].data\n logger.debug(f\"[LF] Docs metadata is a Data object {self.docs_metadata}\")\n additional_metadata.update(self.docs_metadata)\n else:\n for item in self.docs_metadata:\n if isinstance(item, dict) and \"key\" in item and \"value\" in item:\n additional_metadata[item[\"key\"]] = item[\"value\"]\n # Replace string \"None\" values with actual None\n for key, value in additional_metadata.items():\n if value == \"None\":\n additional_metadata[key] = None\n logger.debug(f\"[LF] Additional metadata {additional_metadata}\")\n for doc_obj in docs:\n data_copy = json.loads(doc_obj.model_dump_json())\n text = data_copy.pop(doc_obj.text_key, doc_obj.default_value)\n texts.append(text)\n\n # Merge additional metadata from table input\n data_copy.update(additional_metadata)\n\n metadatas.append(data_copy)\n self.log(metadatas)\n if not self.embedding:\n msg = \"Embedding handle is required to embed documents.\"\n raise ValueError(msg)\n\n # Generate embeddings\n vectors = self.embedding.embed_documents(texts)\n\n if not vectors:\n self.log(\"No vectors generated from documents.\")\n return\n\n # Get vector dimension for mapping\n dim = len(vectors[0]) if vectors else 768 # default fallback\n\n # Check for AOSS\n auth_kwargs = self._build_auth_kwargs()\n is_aoss = self._is_aoss_enabled(auth_kwargs.get(\"http_auth\"))\n\n # Validate engine with AOSS\n engine = getattr(self, \"engine\", \"jvector\")\n self._validate_aoss_with_engines(is_aoss=is_aoss, engine=engine)\n\n # Create mapping with proper KNN settings\n space_type = getattr(self, \"space_type\", \"l2\")\n ef_construction = getattr(self, \"ef_construction\", 512)\n m = getattr(self, \"m\", 16)\n\n mapping = self._default_text_mapping(\n dim=dim,\n engine=engine,\n space_type=space_type,\n ef_construction=ef_construction,\n m=m,\n vector_field=self.vector_field,\n )\n\n # Ensure index exists with proper KNN mapping (index.knn: true is required for vector search)\n try:\n if not client.indices.exists(index=self.index_name):\n self.log(f\"Creating index '{self.index_name}' with KNN mapping (index.knn: true)\")\n client.indices.create(index=self.index_name, body=mapping)\n except RequestError as creation_error:\n error_msg = str(creation_error).lower()\n if \"invalid engine\" in error_msg or \"illegal_argument\" in error_msg:\n if \"jvector\" in error_msg:\n msg = (\n \"The 'jvector' engine is not available in your OpenSearch installation. \"\n \"Use 'nmslib' or 'faiss' for standard OpenSearch, or upgrade to OpenSearch 2.9+.\"\n )\n raise ValueError(msg) from creation_error\n if \"index.knn\" in error_msg:\n msg = (\n \"The index has index.knn: false. Delete the existing index and let the \"\n \"component recreate it, or create a new index with a different name.\"\n )\n raise ValueError(msg) from creation_error\n raise\n\n self.log(f\"Indexing {len(texts)} documents into '{self.index_name}' with proper KNN mapping...\")\n\n # Use the LangChain-style bulk ingestion\n return_ids = self._bulk_ingest_embeddings(\n client=client,\n index_name=self.index_name,\n embeddings=vectors,\n texts=texts,\n metadatas=metadatas,\n vector_field=self.vector_field,\n text_field=\"text\",\n mapping=mapping,\n is_aoss=is_aoss,\n )\n self.log(metadatas)\n\n self.log(f\"Successfully indexed {len(return_ids)} documents.\")\n\n # ---------- helpers for filters ----------\n def _is_placeholder_term(self, term_obj: dict) -> bool:\n # term_obj like {\"filename\": \"__IMPOSSIBLE_VALUE__\"}\n return any(v == \"__IMPOSSIBLE_VALUE__\" for v in term_obj.values())\n\n def _coerce_filter_clauses(self, filter_obj: dict | None) -> list[dict]:\n \"\"\"Convert filter expressions into OpenSearch-compatible filter clauses.\n\n This method accepts two filter formats and converts them to standardized\n OpenSearch query clauses:\n\n Format A - Explicit filters:\n {\"filter\": [{\"term\": {\"field\": \"value\"}}, {\"terms\": {\"field\": [\"val1\", \"val2\"]}}],\n \"limit\": 10, \"score_threshold\": 1.5}\n\n Format B - Context-style mapping:\n {\"data_sources\": [\"file1.pdf\"], \"document_types\": [\"pdf\"], \"owners\": [\"user1\"]}\n\n Args:\n filter_obj: Filter configuration dictionary or None\n\n Returns:\n List of OpenSearch filter clauses (term/terms objects)\n Placeholder values with \"__IMPOSSIBLE_VALUE__\" are ignored\n \"\"\"\n if not filter_obj:\n return []\n\n # If it is a string, try to parse it once\n if isinstance(filter_obj, str):\n try:\n filter_obj = json.loads(filter_obj)\n except json.JSONDecodeError:\n # Not valid JSON - treat as no filters\n return []\n\n # Case A: already an explicit list/dict under \"filter\"\n if \"filter\" in filter_obj:\n raw = filter_obj[\"filter\"]\n if isinstance(raw, dict):\n raw = [raw]\n explicit_clauses: list[dict] = []\n for f in raw or []:\n if \"term\" in f and isinstance(f[\"term\"], dict) and not self._is_placeholder_term(f[\"term\"]):\n explicit_clauses.append(f)\n elif \"terms\" in f and isinstance(f[\"terms\"], dict):\n field, vals = next(iter(f[\"terms\"].items()))\n if isinstance(vals, list) and len(vals) > 0:\n explicit_clauses.append(f)\n return explicit_clauses\n\n # Case B: convert context-style maps into clauses\n field_mapping = {\n \"data_sources\": \"filename\",\n \"document_types\": \"mimetype\",\n \"owners\": \"owner\",\n }\n context_clauses: list[dict] = []\n for k, values in filter_obj.items():\n if not isinstance(values, list):\n continue\n field = field_mapping.get(k, k)\n if len(values) == 0:\n # Match-nothing placeholder (kept to mirror your tool semantics)\n context_clauses.append({\"term\": {field: \"__IMPOSSIBLE_VALUE__\"}})\n elif len(values) == 1:\n if values[0] != \"__IMPOSSIBLE_VALUE__\":\n context_clauses.append({\"term\": {field: values[0]}})\n else:\n context_clauses.append({\"terms\": {field: values}})\n return context_clauses\n\n # ---------- search (single hybrid path matching your tool) ----------\n def search(self, query: str | None = None) -> list[dict[str, Any]]:\n \"\"\"Perform hybrid search combining vector similarity and keyword matching.\n\n This method executes a sophisticated search that combines:\n - K-nearest neighbor (KNN) vector similarity search (70% weight)\n - Multi-field keyword search with fuzzy matching (30% weight)\n - Optional filtering and score thresholds\n - Aggregations for faceted search results\n\n Args:\n query: Search query string (used for both vector embedding and keyword search)\n\n Returns:\n List of search results with page_content, metadata, and relevance scores\n\n Raises:\n ValueError: If embedding component is not provided or filter JSON is invalid\n \"\"\"\n logger.info(self.ingest_data)\n client = self.build_client()\n q = (query or \"\").strip()\n\n # Parse optional filter expression (can be either A or B shape; see _coerce_filter_clauses)\n filter_obj = None\n if getattr(self, \"filter_expression\", \"\") and self.filter_expression.strip():\n try:\n filter_obj = json.loads(self.filter_expression)\n except json.JSONDecodeError as e:\n msg = f\"Invalid filter_expression JSON: {e}\"\n raise ValueError(msg) from e\n\n if not self.embedding:\n msg = \"Embedding is required to run hybrid search (KNN + keyword).\"\n raise ValueError(msg)\n\n # Embed the query\n vec = self.embedding.embed_query(q)\n\n # Build filter clauses (accept both shapes)\n filter_clauses = self._coerce_filter_clauses(filter_obj)\n\n # Respect the tool's limit/threshold defaults\n limit = (filter_obj or {}).get(\"limit\", self.number_of_results)\n score_threshold = (filter_obj or {}).get(\"score_threshold\", 0)\n\n # Build the same hybrid body as your SearchService\n body = {\n \"query\": {\n \"bool\": {\n \"should\": [\n {\n \"knn\": {\n self.vector_field: {\n \"vector\": vec,\n \"k\": 10, # fixed to match the tool\n \"boost\": 0.7,\n }\n }\n },\n {\n \"multi_match\": {\n \"query\": q,\n \"fields\": [\"text^2\", \"filename^1.5\"],\n \"type\": \"best_fields\",\n \"fuzziness\": \"AUTO\",\n \"boost\": 0.3,\n }\n },\n ],\n \"minimum_should_match\": 1,\n }\n },\n \"aggs\": {\n \"data_sources\": {\"terms\": {\"field\": \"filename\", \"size\": 20}},\n \"document_types\": {\"terms\": {\"field\": \"mimetype\", \"size\": 10}},\n \"owners\": {\"terms\": {\"field\": \"owner\", \"size\": 10}},\n },\n \"_source\": [\n \"filename\",\n \"mimetype\",\n \"page\",\n \"text\",\n \"source_url\",\n \"owner\",\n \"allowed_users\",\n \"allowed_groups\",\n ],\n \"size\": limit,\n }\n if filter_clauses:\n body[\"query\"][\"bool\"][\"filter\"] = filter_clauses\n\n if isinstance(score_threshold, (int, float)) and score_threshold > 0:\n # top-level min_score (matches your tool)\n body[\"min_score\"] = score_threshold\n\n resp = client.search(index=self.index_name, body=body)\n hits = resp.get(\"hits\", {}).get(\"hits\", [])\n return [\n {\n \"page_content\": hit[\"_source\"].get(\"text\", \"\"),\n \"metadata\": {k: v for k, v in hit[\"_source\"].items() if k != \"text\"},\n \"score\": hit.get(\"_score\"),\n }\n for hit in hits\n ]\n\n def search_documents(self) -> list[Data]:\n \"\"\"Search documents and return results as Data objects.\n\n This is the main interface method that performs the search using the\n configured search_query and returns results in Langflow's Data format.\n\n Returns:\n List of Data objects containing search results with text and metadata\n\n Raises:\n Exception: If search operation fails\n \"\"\"\n try:\n raw = self.search(self.search_query or \"\")\n return [Data(text=hit[\"page_content\"], **hit[\"metadata\"]) for hit in raw]\n self.log(self.ingest_data)\n except Exception as e:\n self.log(f\"search_documents error: {e}\")\n raise\n\n # -------- dynamic UI handling (auth switch) --------\n async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n \"\"\"Dynamically update component configuration based on field changes.\n\n This method handles real-time UI updates, particularly for authentication\n mode changes that show/hide relevant input fields.\n\n Args:\n build_config: Current component configuration\n field_value: New value for the changed field\n field_name: Name of the field that changed\n\n Returns:\n Updated build configuration with appropriate field visibility\n \"\"\"\n try:\n if field_name == \"auth_mode\":\n mode = (field_value or \"basic\").strip().lower()\n is_basic = mode == \"basic\"\n is_jwt = mode == \"jwt\"\n\n build_config[\"username\"][\"show\"] = is_basic\n build_config[\"password\"][\"show\"] = is_basic\n\n build_config[\"jwt_token\"][\"show\"] = is_jwt\n build_config[\"jwt_header\"][\"show\"] = is_jwt\n build_config[\"bearer_prefix\"][\"show\"] = is_jwt\n\n build_config[\"username\"][\"required\"] = is_basic\n build_config[\"password\"][\"required\"] = is_basic\n\n build_config[\"jwt_token\"][\"required\"] = is_jwt\n build_config[\"jwt_header\"][\"required\"] = is_jwt\n build_config[\"bearer_prefix\"][\"required\"] = False\n\n if is_basic:\n build_config[\"jwt_token\"][\"value\"] = \"\"\n\n return build_config\n\n except (KeyError, ValueError) as e:\n self.log(f\"update_build_config error: {e}\")\n\n return build_config\n" }, "docs_metadata": { "_input_type": "TableInput", @@ -67225,7 +67267,8 @@ "dynamic": false, "info": "Additional metadata key-value pairs to be added to all ingested documents. Useful for tagging documents with source information, categories, or other custom attributes.", "input_types": [ - "Data" + "Data", + "JSON" ], "is_list": true, "list_add_label": "Add More", @@ -67704,7 +67747,7 @@ }, "OpenSearchVectorStoreComponentMultimodalMultiEmbedding": { "base_classes": [ - "Data", + "JSON", "VectorStore" ], "beta": false, @@ -67746,7 +67789,7 @@ "icon": "OpenSearch", "legacy": false, "metadata": { - "code_hash": "6a3df45b55c5", + "code_hash": "24abb9020048", "dependencies": { "dependencies": [ { @@ -67776,10 +67819,10 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -67790,10 +67833,10 @@ "group_outputs": false, "method": "raw_search", "name": "raw_search", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -67880,7 +67923,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport copy\nimport json\nimport time\nimport uuid\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom typing import Any\n\nfrom opensearchpy import OpenSearch, helpers\nfrom opensearchpy.exceptions import OpenSearchException, RequestError\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.base.vectorstores.vector_store_connection_decorator import vector_store_connection\nfrom lfx.io import (\n BoolInput,\n DropdownInput,\n HandleInput,\n IntInput,\n MultilineInput,\n Output,\n SecretStrInput,\n StrInput,\n TableInput,\n)\nfrom lfx.log import logger\nfrom lfx.schema.data import Data\n\nREQUEST_TIMEOUT = 60\nMAX_RETRIES = 5\n\n\ndef normalize_model_name(model_name: str) -> str:\n \"\"\"Normalize embedding model name for use as field suffix.\n\n Converts model names to valid OpenSearch field names by replacing\n special characters and ensuring alphanumeric format.\n\n Args:\n model_name: Original embedding model name (e.g., \"text-embedding-3-small\")\n\n Returns:\n Normalized field suffix (e.g., \"text_embedding_3_small\")\n \"\"\"\n normalized = model_name.lower()\n # Replace common separators with underscores\n normalized = normalized.replace(\"-\", \"_\").replace(\":\", \"_\").replace(\"/\", \"_\").replace(\".\", \"_\")\n # Remove any non-alphanumeric characters except underscores\n normalized = \"\".join(c if c.isalnum() or c == \"_\" else \"_\" for c in normalized)\n # Remove duplicate underscores\n while \"__\" in normalized:\n normalized = normalized.replace(\"__\", \"_\")\n return normalized.strip(\"_\")\n\n\ndef get_embedding_field_name(model_name: str) -> str:\n \"\"\"Get the dynamic embedding field name for a model.\n\n Args:\n model_name: Embedding model name\n\n Returns:\n Field name in format: chunk_embedding_{normalized_model_name}\n \"\"\"\n logger.info(f\"chunk_embedding_{normalize_model_name(model_name)}\")\n return f\"chunk_embedding_{normalize_model_name(model_name)}\"\n\n\n@vector_store_connection\nclass OpenSearchVectorStoreComponentMultimodalMultiEmbedding(LCVectorStoreComponent):\n \"\"\"OpenSearch Vector Store Component with Multi-Model Hybrid Search Capabilities.\n\n This component provides vector storage and retrieval using OpenSearch, combining semantic\n similarity search (KNN) with keyword-based search for optimal results. It supports:\n - Multiple embedding models per index with dynamic field names\n - Automatic detection and querying of all available embedding models\n - Parallel embedding generation for multi-model search\n - Document ingestion with model tracking\n - Advanced filtering and aggregations\n - Flexible authentication options\n\n Features:\n - Multi-model vector storage with dynamic fields (chunk_embedding_{model_name})\n - Hybrid search combining multiple KNN queries (dis_max) + keyword matching\n - Auto-detection of available models in the index\n - Parallel query embedding generation for all detected models\n - Vector storage with configurable engines (jvector, nmslib, faiss, lucene)\n - Flexible authentication (Basic auth, JWT tokens)\n\n Model Name Resolution:\n - Priority: deployment > model > model_name attributes\n - This ensures correct matching between embedding objects and index fields\n - When multiple embeddings are provided, specify embedding_model_name to select which one to use\n - During search, each detected model in the index is matched to its corresponding embedding object\n \"\"\"\n\n display_name: str = \"OpenSearch (Multi-Model Multi-Embedding)\"\n icon: str = \"OpenSearch\"\n description: str = (\n \"Store and search documents using OpenSearch with multi-model hybrid semantic and keyword search. \"\n \"To search use the tools search_documents and raw_search. \"\n \"Search documents takes a query for vector search, for example\\n\"\n ' {search_query: \"components in openrag\"}'\n )\n\n # Keys we consider baseline\n default_keys: list[str] = [\n \"opensearch_url\",\n \"index_name\",\n *[i.name for i in LCVectorStoreComponent.inputs], # search_query, add_documents, etc.\n \"embedding\",\n \"embedding_model_name\",\n \"vector_field\",\n \"number_of_results\",\n \"auth_mode\",\n \"username\",\n \"password\",\n \"jwt_token\",\n \"jwt_header\",\n \"bearer_prefix\",\n \"use_ssl\",\n \"verify_certs\",\n \"filter_expression\",\n \"engine\",\n \"space_type\",\n \"ef_construction\",\n \"m\",\n \"num_candidates\",\n \"docs_metadata\",\n \"request_timeout\",\n \"max_retries\",\n ]\n\n inputs = [\n TableInput(\n name=\"docs_metadata\",\n display_name=\"Document Metadata\",\n info=(\n \"Additional metadata key-value pairs to be added to all ingested documents. \"\n \"Useful for tagging documents with source information, categories, or other custom attributes.\"\n ),\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Key name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Value of the metadata\",\n },\n ],\n value=[],\n input_types=[\"Data\"],\n ),\n StrInput(\n name=\"opensearch_url\",\n display_name=\"OpenSearch URL\",\n value=\"http://localhost:9200\",\n info=(\n \"The connection URL for your OpenSearch cluster \"\n \"(e.g., http://localhost:9200 for local development or your cloud endpoint).\"\n ),\n ),\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n value=\"langflow\",\n info=(\n \"The OpenSearch index name where documents will be stored and searched. \"\n \"Will be created automatically if it doesn't exist.\"\n ),\n ),\n DropdownInput(\n name=\"engine\",\n display_name=\"Vector Engine\",\n options=[\"nmslib\", \"faiss\", \"lucene\", \"jvector\"],\n value=\"jvector\",\n info=(\n \"Vector search engine for similarity calculations. 'nmslib' works with standard \"\n \"OpenSearch. 'jvector' requires OpenSearch 2.9+. 'lucene' requires index.knn: true. \"\n \"Amazon OpenSearch Serverless only supports 'nmslib' or 'faiss'.\"\n ),\n advanced=True,\n ),\n DropdownInput(\n name=\"space_type\",\n display_name=\"Distance Metric\",\n options=[\"l2\", \"l1\", \"cosinesimil\", \"linf\", \"innerproduct\"],\n value=\"l2\",\n info=(\n \"Distance metric for calculating vector similarity. 'l2' (Euclidean) is most common, \"\n \"'cosinesimil' for cosine similarity, 'innerproduct' for dot product.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"ef_construction\",\n display_name=\"EF Construction\",\n value=512,\n info=(\n \"Size of the dynamic candidate list during index construction. \"\n \"Higher values improve recall but increase indexing time and memory usage.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"m\",\n display_name=\"M Parameter\",\n value=16,\n info=(\n \"Number of bidirectional connections for each vector in the HNSW graph. \"\n \"Higher values improve search quality but increase memory usage and indexing time.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"num_candidates\",\n display_name=\"Candidate Pool Size\",\n value=1000,\n info=(\n \"Number of approximate neighbors to consider for each KNN query. \"\n \"Some OpenSearch deployments do not support this parameter; set to 0 to disable.\"\n ),\n advanced=True,\n ),\n *LCVectorStoreComponent.inputs, # includes search_query, add_documents, etc.\n HandleInput(name=\"embedding\", display_name=\"Embedding\", input_types=[\"Embeddings\"], is_list=True),\n StrInput(\n name=\"embedding_model_name\",\n display_name=\"Embedding Model Name\",\n value=\"\",\n info=(\n \"Name of the embedding model to use for ingestion. This selects which embedding from the list \"\n \"will be used to embed documents. Matches on deployment, model, model_id, or model_name. \"\n \"For duplicate deployments, use combined format: 'deployment:model' \"\n \"(e.g., 'text-embedding-ada-002:text-embedding-3-large'). \"\n \"Leave empty to use the first embedding. Error message will show all available identifiers.\"\n ),\n advanced=False,\n ),\n StrInput(\n name=\"vector_field\",\n display_name=\"Legacy Vector Field Name\",\n value=\"chunk_embedding\",\n advanced=True,\n info=(\n \"Legacy field name for backward compatibility. New documents use dynamic fields \"\n \"(chunk_embedding_{model_name}) based on the embedding_model_name.\"\n ),\n ),\n IntInput(\n name=\"number_of_results\",\n display_name=\"Default Result Limit\",\n value=10,\n advanced=True,\n info=(\n \"Default maximum number of search results to return when no limit is \"\n \"specified in the filter expression.\"\n ),\n ),\n MultilineInput(\n name=\"filter_expression\",\n display_name=\"Search Filters (JSON)\",\n value=\"\",\n info=(\n \"Optional JSON configuration for search filtering, result limits, and score thresholds.\\n\\n\"\n \"Format 1 - Explicit filters:\\n\"\n '{\"filter\": [{\"term\": {\"filename\":\"doc.pdf\"}}, '\n '{\"terms\":{\"owner\":[\"user1\",\"user2\"]}}], \"limit\": 10, \"score_threshold\": 1.6}\\n\\n'\n \"Format 2 - Context-style mapping:\\n\"\n '{\"data_sources\":[\"file.pdf\"], \"document_types\":[\"application/pdf\"], \"owners\":[\"user123\"]}\\n\\n'\n \"Use __IMPOSSIBLE_VALUE__ as placeholder to ignore specific filters.\"\n ),\n ),\n # ----- Auth controls (dynamic) -----\n DropdownInput(\n name=\"auth_mode\",\n display_name=\"Authentication Mode\",\n value=\"basic\",\n options=[\"basic\", \"jwt\"],\n info=(\n \"Authentication method: 'basic' for username/password authentication, \"\n \"or 'jwt' for JSON Web Token (Bearer) authentication.\"\n ),\n real_time_refresh=True,\n advanced=False,\n ),\n StrInput(\n name=\"username\",\n display_name=\"Username\",\n value=\"admin\",\n show=True,\n ),\n SecretStrInput(\n name=\"password\",\n display_name=\"OpenSearch Password\",\n value=\"admin\",\n show=True,\n ),\n SecretStrInput(\n name=\"jwt_token\",\n display_name=\"JWT Token\",\n value=\"JWT\",\n load_from_db=False,\n show=False,\n info=(\n \"Valid JSON Web Token for authentication. \"\n \"Will be sent in the Authorization header (with optional 'Bearer ' prefix).\"\n ),\n ),\n StrInput(\n name=\"jwt_header\",\n display_name=\"JWT Header Name\",\n value=\"Authorization\",\n show=False,\n advanced=True,\n ),\n BoolInput(\n name=\"bearer_prefix\",\n display_name=\"Prefix 'Bearer '\",\n value=True,\n show=False,\n advanced=True,\n ),\n # ----- TLS -----\n BoolInput(\n name=\"use_ssl\",\n display_name=\"Use SSL/TLS\",\n value=True,\n advanced=True,\n info=\"Enable SSL/TLS encryption for secure connections to OpenSearch.\",\n ),\n BoolInput(\n name=\"verify_certs\",\n display_name=\"Verify SSL Certificates\",\n value=False,\n advanced=True,\n info=(\n \"Verify SSL certificates when connecting. \"\n \"Disable for self-signed certificates in development environments.\"\n ),\n ),\n # ----- Timeout / Retry -----\n StrInput(\n name=\"request_timeout\",\n display_name=\"Request Timeout (seconds)\",\n value=\"60\",\n advanced=True,\n info=(\n \"Time in seconds to wait for a response from OpenSearch. \"\n \"Increase for large bulk ingestion or complex hybrid queries.\"\n ),\n ),\n StrInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n value=\"3\",\n advanced=True,\n info=\"Number of retries for failed connections before raising an error.\",\n ),\n ]\n outputs = [\n Output(\n display_name=\"Search Results\",\n name=\"search_results\",\n method=\"search_documents\",\n ),\n Output(display_name=\"Raw Search\", name=\"raw_search\", method=\"raw_search\"),\n ]\n\n def raw_search(self, query: str | dict | None = None) -> Data:\n \"\"\"Execute a raw OpenSearch query against the target index.\n\n Args:\n query (dict[str, Any]): The OpenSearch query DSL dictionary.\n\n Returns:\n Data: Search results as a Data object.\n\n Raises:\n ValueError: If 'query' is not a valid OpenSearch query (must be a non-empty dict).\n \"\"\"\n raw_query = query if query is not None else self.search_query\n\n if raw_query is None or (isinstance(raw_query, str) and not raw_query.strip()):\n self.log(\"No query provided for raw search - returning empty results\")\n return Data(data={})\n\n if isinstance(raw_query, dict):\n query_body = raw_query\n elif isinstance(raw_query, str):\n s = raw_query.strip()\n\n # First, optimistically try to parse as JSON DSL\n try:\n query_body = json.loads(s)\n except json.JSONDecodeError:\n # Fallback: treat as a basic text query over common fields\n query_body = {\n \"query\": {\n \"multi_match\": {\n \"query\": s,\n \"fields\": [\"text^2\", \"filename^1.5\"],\n \"type\": \"best_fields\",\n \"fuzziness\": \"AUTO\",\n }\n }\n }\n else:\n msg = f\"Unsupported raw_search query type: {type(raw_query)!r}\"\n raise TypeError(msg)\n\n client = self.build_client()\n logger.info(f\"query: {query_body}\")\n resp = client.search(\n index=self.index_name,\n body=query_body,\n params={\"terminate_after\": 0},\n )\n # Remove any _source keys whose value is a list of floats (embedding vectors)\n # Minimum length threshold to identify embedding vectors\n min_vector_length = 100\n\n def is_vector(val):\n # Accepts if it's a list of numbers (float or int) and has reasonable vector length\n return (\n isinstance(val, list) and len(val) > min_vector_length and all(isinstance(x, (float, int)) for x in val)\n )\n\n if \"hits\" in resp and \"hits\" in resp[\"hits\"]:\n for hit in resp[\"hits\"][\"hits\"]:\n source = hit.get(\"_source\")\n if isinstance(source, dict):\n keys_to_remove = [k for k, v in source.items() if is_vector(v)]\n for k in keys_to_remove:\n source.pop(k)\n logger.info(f\"Raw search response (all embedding vectors removed): {resp}\")\n return Data(**resp)\n\n def _get_embedding_model_name(self, embedding_obj=None) -> str:\n \"\"\"Get the embedding model name from component config or embedding object.\n\n Priority: deployment > model > model_id > model_name\n This ensures we use the actual model being deployed, not just the configured model.\n Supports multiple embedding providers (OpenAI, Watsonx, Cohere, etc.)\n\n Args:\n embedding_obj: Specific embedding object to get name from (optional)\n\n Returns:\n Embedding model name\n\n Raises:\n ValueError: If embedding model name cannot be determined\n \"\"\"\n # First try explicit embedding_model_name input\n if hasattr(self, \"embedding_model_name\") and self.embedding_model_name:\n return self.embedding_model_name.strip()\n\n # Try to get from provided embedding object\n if embedding_obj:\n # Priority: deployment > model > model_id > model_name\n if hasattr(embedding_obj, \"deployment\") and embedding_obj.deployment:\n return str(embedding_obj.deployment)\n if hasattr(embedding_obj, \"model\") and embedding_obj.model:\n return str(embedding_obj.model)\n if hasattr(embedding_obj, \"model_id\") and embedding_obj.model_id:\n return str(embedding_obj.model_id)\n if hasattr(embedding_obj, \"model_name\") and embedding_obj.model_name:\n return str(embedding_obj.model_name)\n\n # Try to get from embedding component (legacy single embedding)\n if hasattr(self, \"embedding\") and self.embedding:\n # Handle list of embeddings\n if isinstance(self.embedding, list) and len(self.embedding) > 0:\n first_emb = self.embedding[0]\n if hasattr(first_emb, \"deployment\") and first_emb.deployment:\n return str(first_emb.deployment)\n if hasattr(first_emb, \"model\") and first_emb.model:\n return str(first_emb.model)\n if hasattr(first_emb, \"model_id\") and first_emb.model_id:\n return str(first_emb.model_id)\n if hasattr(first_emb, \"model_name\") and first_emb.model_name:\n return str(first_emb.model_name)\n # Handle single embedding\n elif not isinstance(self.embedding, list):\n if hasattr(self.embedding, \"deployment\") and self.embedding.deployment:\n return str(self.embedding.deployment)\n if hasattr(self.embedding, \"model\") and self.embedding.model:\n return str(self.embedding.model)\n if hasattr(self.embedding, \"model_id\") and self.embedding.model_id:\n return str(self.embedding.model_id)\n if hasattr(self.embedding, \"model_name\") and self.embedding.model_name:\n return str(self.embedding.model_name)\n\n msg = (\n \"Could not determine embedding model name. \"\n \"Please set the 'embedding_model_name' field or ensure the embedding component \"\n \"has a 'deployment', 'model', 'model_id', or 'model_name' attribute.\"\n )\n raise ValueError(msg)\n\n # ---------- helper functions for index management ----------\n def _default_text_mapping(\n self,\n dim: int,\n engine: str = \"jvector\",\n space_type: str = \"l2\",\n ef_search: int = 512,\n ef_construction: int = 100,\n m: int = 16,\n vector_field: str = \"vector_field\",\n ) -> dict[str, Any]:\n \"\"\"Create the default OpenSearch index mapping for vector search.\n\n This method generates the index configuration with k-NN settings optimized\n for approximate nearest neighbor search using the specified vector engine.\n Includes the embedding_model keyword field for tracking which model was used.\n\n Args:\n dim: Dimensionality of the vector embeddings\n engine: Vector search engine (jvector, nmslib, faiss, lucene)\n space_type: Distance metric for similarity calculation\n ef_search: Size of dynamic list used during search\n ef_construction: Size of dynamic list used during index construction\n m: Number of bidirectional links for each vector\n vector_field: Name of the field storing vector embeddings\n\n Returns:\n Dictionary containing OpenSearch index mapping configuration\n \"\"\"\n return {\n \"settings\": {\"index\": {\"knn\": True, \"knn.algo_param.ef_search\": ef_search}},\n \"mappings\": {\n \"properties\": {\n vector_field: {\n \"type\": \"knn_vector\",\n \"dimension\": dim,\n \"method\": {\n \"name\": \"disk_ann\",\n \"space_type\": space_type,\n \"engine\": engine,\n \"parameters\": {\"ef_construction\": ef_construction, \"m\": m},\n },\n },\n \"embedding_model\": {\"type\": \"keyword\"}, # Track which model was used\n \"embedding_dimensions\": {\"type\": \"integer\"},\n }\n },\n }\n\n def _ensure_embedding_field_mapping(\n self,\n client: OpenSearch,\n index_name: str,\n field_name: str,\n dim: int,\n engine: str,\n space_type: str,\n ef_construction: int,\n m: int,\n ) -> None:\n \"\"\"Lazily add a dynamic embedding field to the index if it doesn't exist.\n\n This allows adding new embedding models without recreating the entire index.\n Also ensures the embedding_model tracking field exists.\n\n Note: Some OpenSearch versions/configurations have issues with dynamically adding\n knn_vector mappings (NullPointerException). This method checks if the field\n already exists before attempting to add it, and gracefully skips if the field\n is already properly configured.\n\n Args:\n client: OpenSearch client instance\n index_name: Target index name\n field_name: Dynamic field name for this embedding model\n dim: Vector dimensionality\n engine: Vector search engine\n space_type: Distance metric\n ef_construction: Construction parameter\n m: HNSW parameter\n \"\"\"\n # First, check if the field already exists and is properly mapped\n properties = self._get_index_properties(client)\n if self._is_knn_vector_field(properties, field_name):\n # Field already exists as knn_vector - verify dimensions match\n existing_dim = self._get_field_dimension(properties, field_name)\n if existing_dim is not None and existing_dim != dim:\n logger.warning(\n f\"Field '{field_name}' exists with dimension {existing_dim}, \"\n f\"but current embedding has dimension {dim}. Using existing mapping.\"\n )\n else:\n logger.info(\n f\"[OpenSearchMultimodel] Field '{field_name}' already exists\"\n f\"as knn_vector with matching dimensions - skipping mapping update\"\n )\n return\n\n # Field doesn't exist, try to add the mapping\n try:\n mapping = {\n \"properties\": {\n field_name: {\n \"type\": \"knn_vector\",\n \"dimension\": dim,\n \"method\": {\n \"name\": \"disk_ann\",\n \"space_type\": space_type,\n \"engine\": engine,\n \"parameters\": {\"ef_construction\": ef_construction, \"m\": m},\n },\n },\n # Also ensure the embedding_model tracking field exists as keyword\n \"embedding_model\": {\"type\": \"keyword\"},\n \"embedding_dimensions\": {\"type\": \"integer\"},\n }\n }\n client.indices.put_mapping(index=index_name, body=mapping)\n logger.info(f\"Added/updated embedding field mapping: {field_name}\")\n except RequestError as e:\n error_str = str(e).lower()\n if \"invalid engine\" in error_str and \"jvector\" in error_str:\n msg = (\n \"The 'jvector' engine is not available in your OpenSearch installation. \"\n \"Use 'nmslib' or 'faiss' for standard OpenSearch, or upgrade to OpenSearch 2.9+.\"\n )\n raise ValueError(msg) from e\n if \"index.knn\" in error_str:\n msg = (\n \"The index has index.knn: false. Delete the existing index and let the \"\n \"component recreate it, or create a new index with a different name.\"\n )\n raise ValueError(msg) from e\n raise\n except Exception as e:\n # Check if this is the known OpenSearch k-NN NullPointerException issue\n error_str = str(e).lower()\n if \"null\" in error_str or \"nullpointerexception\" in error_str:\n logger.warning(\n f\"[OpenSearchMultimodel] Could not add embedding field mapping for {field_name}\"\n f\"due to OpenSearch k-NN plugin issue: {e}. \"\n f\"This is a known issue with some OpenSearch versions. \"\n f\"[OpenSearchMultimodel] Skipping mapping update. \"\n f\"Please ensure the index has the correct mapping for KNN search to work.\"\n )\n # Skip and continue - ingestion will proceed, but KNN search may fail if mapping doesn't exist\n return\n logger.warning(f\"[OpenSearchMultimodel] Could not add embedding field mapping for {field_name}: {e}\")\n raise\n\n # Verify the field was added correctly\n properties = self._get_index_properties(client)\n if not self._is_knn_vector_field(properties, field_name):\n msg = f\"Field '{field_name}' is not mapped as knn_vector. Current mapping: {properties.get(field_name)}\"\n logger.error(msg)\n raise ValueError(msg)\n\n def _validate_aoss_with_engines(self, *, is_aoss: bool, engine: str) -> None:\n \"\"\"Validate engine compatibility with Amazon OpenSearch Serverless (AOSS).\n\n Amazon OpenSearch Serverless has restrictions on which vector engines\n can be used. This method ensures the selected engine is compatible.\n\n Args:\n is_aoss: Whether the connection is to Amazon OpenSearch Serverless\n engine: The selected vector search engine\n\n Raises:\n ValueError: If AOSS is used with an incompatible engine\n \"\"\"\n if is_aoss and engine not in {\"nmslib\", \"faiss\"}:\n msg = \"Amazon OpenSearch Service Serverless only supports `nmslib` or `faiss` engines\"\n raise ValueError(msg)\n\n def _is_aoss_enabled(self, http_auth: Any) -> bool:\n \"\"\"Determine if Amazon OpenSearch Serverless (AOSS) is being used.\n\n Args:\n http_auth: The HTTP authentication object\n\n Returns:\n True if AOSS is enabled, False otherwise\n \"\"\"\n return http_auth is not None and hasattr(http_auth, \"service\") and http_auth.service == \"aoss\"\n\n def _bulk_ingest_embeddings(\n self,\n client: OpenSearch,\n index_name: str,\n embeddings: list[list[float]],\n texts: list[str],\n metadatas: list[dict] | None = None,\n ids: list[str] | None = None,\n vector_field: str = \"vector_field\",\n text_field: str = \"text\",\n embedding_model: str = \"unknown\",\n mapping: dict | None = None,\n max_chunk_bytes: int | None = 1 * 1024 * 1024,\n *,\n is_aoss: bool = False,\n ) -> list[str]:\n \"\"\"Efficiently ingest multiple documents with embeddings into OpenSearch.\n\n This method uses bulk operations to insert documents with their vector\n embeddings and metadata into the specified OpenSearch index. Each document\n is tagged with the embedding_model name for tracking.\n\n Args:\n client: OpenSearch client instance\n index_name: Target index for document storage\n embeddings: List of vector embeddings for each document\n texts: List of document texts\n metadatas: Optional metadata dictionaries for each document\n ids: Optional document IDs (UUIDs generated if not provided)\n vector_field: Field name for storing vector embeddings\n text_field: Field name for storing document text\n embedding_model: Name of the embedding model used\n mapping: Optional index mapping configuration\n max_chunk_bytes: Maximum size per bulk request chunk\n is_aoss: Whether using Amazon OpenSearch Serverless\n\n Returns:\n List of document IDs that were successfully ingested\n \"\"\"\n logger.debug(f\"[OpenSearchMultimodel] Bulk ingesting embeddings for {index_name}\")\n if not mapping:\n mapping = {}\n\n requests = []\n return_ids = []\n vector_dimensions = len(embeddings[0]) if embeddings else None\n\n for i, text in enumerate(texts):\n metadata = metadatas[i] if metadatas else {}\n if vector_dimensions is not None and \"embedding_dimensions\" not in metadata:\n metadata = {**metadata, \"embedding_dimensions\": vector_dimensions}\n\n # Normalize ACL fields that may arrive as JSON strings from flows\n for key in (\"allowed_users\", \"allowed_groups\"):\n value = metadata.get(key)\n if isinstance(value, str):\n try:\n parsed = json.loads(value)\n if isinstance(parsed, list):\n metadata[key] = parsed\n except (json.JSONDecodeError, TypeError):\n # Leave value as-is if it isn't valid JSON\n pass\n\n _id = ids[i] if ids else str(uuid.uuid4())\n request = {\n \"_op_type\": \"index\",\n \"_index\": index_name,\n vector_field: embeddings[i],\n text_field: text,\n \"embedding_model\": embedding_model, # Track which model was used\n **metadata,\n }\n if is_aoss:\n request[\"id\"] = _id\n else:\n request[\"_id\"] = _id\n requests.append(request)\n return_ids.append(_id)\n if metadatas:\n self.log(f\"Sample metadata: {metadatas[0] if metadatas else {}}\")\n helpers.bulk(client, requests, max_chunk_bytes=max_chunk_bytes)\n return return_ids\n\n # ---------- param helpers ----------\n def _parse_int_param(self, attr_name: str, default: int) -> int:\n \"\"\"Parse a string attribute to int, returning *default* on failure.\"\"\"\n raw = getattr(self, attr_name, None)\n if raw is None or str(raw).strip() == \"\":\n return default\n try:\n value = int(str(raw).strip())\n except ValueError:\n logger.warning(f\"Invalid integer value '{raw}' for {attr_name}, using default {default}\")\n return default\n\n if value < 0:\n logger.warning(f\"Negative value '{raw}' for {attr_name}, using default {default}\")\n return default\n\n return value\n\n # ---------- auth / client ----------\n def _build_auth_kwargs(self) -> dict[str, Any]:\n \"\"\"Build authentication configuration for OpenSearch client.\n\n Constructs the appropriate authentication parameters based on the\n selected auth mode (basic username/password or JWT token).\n\n Returns:\n Dictionary containing authentication configuration\n\n Raises:\n ValueError: If required authentication parameters are missing\n \"\"\"\n mode = (self.auth_mode or \"basic\").strip().lower()\n if mode == \"jwt\":\n token = (self.jwt_token or \"\").strip()\n if not token:\n msg = \"Auth Mode is 'jwt' but no jwt_token was provided.\"\n raise ValueError(msg)\n header_name = (self.jwt_header or \"Authorization\").strip()\n header_value = f\"Bearer {token}\" if self.bearer_prefix else token\n return {\"headers\": {header_name: header_value}}\n user = (self.username or \"\").strip()\n pwd = (self.password or \"\").strip()\n if not user or not pwd:\n msg = \"Auth Mode is 'basic' but username/password are missing.\"\n raise ValueError(msg)\n return {\"http_auth\": (user, pwd)}\n\n def build_client(self) -> OpenSearch:\n \"\"\"Create and configure an OpenSearch client instance.\n\n Returns:\n Configured OpenSearch client ready for operations\n \"\"\"\n logger.debug(\"[OpenSearchMultimodel] Building OpenSearch client\")\n auth_kwargs = self._build_auth_kwargs()\n return OpenSearch(\n hosts=[self.opensearch_url],\n use_ssl=self.use_ssl,\n verify_certs=self.verify_certs,\n ssl_assert_hostname=False,\n ssl_show_warn=False,\n timeout=self._parse_int_param(\"request_timeout\", REQUEST_TIMEOUT),\n max_retries=self._parse_int_param(\"max_retries\", MAX_RETRIES),\n retry_on_timeout=True,\n **auth_kwargs,\n )\n\n @check_cached_vector_store\n def build_vector_store(self) -> OpenSearch:\n # Return raw OpenSearch client as our \"vector store.\"\n client = self.build_client()\n\n # Check if we're in ingestion-only mode (no search query)\n has_search_query = bool((self.search_query or \"\").strip())\n if not has_search_query:\n logger.debug(\"[OpenSearchMultimodel] Ingestion-only mode activated: search operations will be skipped\")\n logger.debug(\"[OpenSearchMultimodel] Starting ingestion mode...\")\n\n logger.debug(f\"[OpenSearchMultimodel] Embedding: {self.embedding}\")\n self._add_documents_to_vector_store(client=client)\n return client\n\n # ---------- ingest ----------\n def _add_documents_to_vector_store(self, client: OpenSearch) -> None:\n \"\"\"Process and ingest documents into the OpenSearch vector store.\n\n This method handles the complete document ingestion pipeline:\n - Prepares document data and metadata\n - Generates vector embeddings using the selected model\n - Creates appropriate index mappings with dynamic field names\n - Bulk inserts documents with vectors and model tracking\n\n Args:\n client: OpenSearch client for performing operations\n \"\"\"\n logger.debug(\"[OpenSearchMultimodel][INGESTION] _add_documents_to_vector_store called\")\n # Convert DataFrame to Data if needed using parent's method\n self.ingest_data = self._prepare_ingest_data()\n\n logger.debug(\n f\"[OpenSearchMultimodel][INGESTION] ingest_data type: \"\n f\"{type(self.ingest_data)}, length: {len(self.ingest_data) if self.ingest_data else 0}\"\n )\n logger.debug(\n f\"[OpenSearchMultimodel][INGESTION] ingest_data content: \"\n f\"{self.ingest_data[:2] if self.ingest_data and len(self.ingest_data) > 0 else 'empty'}\"\n )\n\n docs = self.ingest_data or []\n if not docs:\n logger.debug(\"Ingestion complete: No documents provided\")\n return\n\n if not self.embedding:\n msg = \"Embedding handle is required to embed documents.\"\n raise ValueError(msg)\n\n # Normalize embedding to list first\n embeddings_list = self.embedding if isinstance(self.embedding, list) else [self.embedding]\n\n # Filter out None values (fail-safe mode) - do this BEFORE checking if empty\n embeddings_list = [e for e in embeddings_list if e is not None]\n\n # NOW check if we have any valid embeddings left after filtering\n if not embeddings_list:\n logger.warning(\"All embeddings returned None (fail-safe mode enabled). Skipping document ingestion.\")\n self.log(\"Embedding returned None (fail-safe mode enabled). Skipping document ingestion.\")\n return\n\n logger.debug(f\"[OpenSearchMultimodel][INGESTION] Valid embeddings after filtering: {len(embeddings_list)}\")\n self.log(f\"[OpenSearchMultimodel][INGESTION] Available embedding models: {len(embeddings_list)}\")\n\n # Select the embedding to use for ingestion\n selected_embedding = None\n embedding_model = None\n\n # If embedding_model_name is specified, find matching embedding\n if hasattr(self, \"embedding_model_name\") and self.embedding_model_name and self.embedding_model_name.strip():\n target_model_name = self.embedding_model_name.strip()\n self.log(f\"Looking for embedding model: {target_model_name}\")\n\n for emb_obj in embeddings_list:\n # Check all possible model identifiers (deployment, model, model_id, model_name)\n # Also check available_models list from EmbeddingsWithModels\n possible_names = []\n deployment = getattr(emb_obj, \"deployment\", None)\n model = getattr(emb_obj, \"model\", None)\n model_id = getattr(emb_obj, \"model_id\", None)\n model_name = getattr(emb_obj, \"model_name\", None)\n available_models_attr = getattr(emb_obj, \"available_models\", None)\n\n if deployment:\n possible_names.append(str(deployment))\n if model:\n possible_names.append(str(model))\n if model_id:\n possible_names.append(str(model_id))\n if model_name:\n possible_names.append(str(model_name))\n\n # Also add combined identifier\n if deployment and model and deployment != model:\n possible_names.append(f\"{deployment}:{model}\")\n\n # Add all models from available_models dict\n if available_models_attr and isinstance(available_models_attr, dict):\n possible_names.extend(\n str(model_key).strip()\n for model_key in available_models_attr\n if model_key and str(model_key).strip()\n )\n\n # Match if target matches any of the possible names\n if target_model_name in possible_names:\n # Check if target is in available_models dict - use dedicated instance\n if (\n available_models_attr\n and isinstance(available_models_attr, dict)\n and target_model_name in available_models_attr\n ):\n # Use the dedicated embedding instance from the dict\n selected_embedding = available_models_attr[target_model_name]\n embedding_model = target_model_name\n self.log(f\"Found dedicated embedding instance for '{embedding_model}' in available_models dict\")\n else:\n # Traditional identifier match\n selected_embedding = emb_obj\n embedding_model = self._get_embedding_model_name(emb_obj)\n self.log(f\"Found matching embedding model: {embedding_model} (matched on: {target_model_name})\")\n break\n\n if not selected_embedding:\n # Build detailed list of available embeddings with all their identifiers\n available_info = []\n for idx, emb in enumerate(embeddings_list):\n emb_type = type(emb).__name__\n identifiers = []\n deployment = getattr(emb, \"deployment\", None)\n model = getattr(emb, \"model\", None)\n model_id = getattr(emb, \"model_id\", None)\n model_name = getattr(emb, \"model_name\", None)\n available_models_attr = getattr(emb, \"available_models\", None)\n\n if deployment:\n identifiers.append(f\"deployment='{deployment}'\")\n if model:\n identifiers.append(f\"model='{model}'\")\n if model_id:\n identifiers.append(f\"model_id='{model_id}'\")\n if model_name:\n identifiers.append(f\"model_name='{model_name}'\")\n\n # Add combined identifier as an option\n if deployment and model and deployment != model:\n identifiers.append(f\"combined='{deployment}:{model}'\")\n\n # Add available_models dict if present\n if available_models_attr and isinstance(available_models_attr, dict):\n identifiers.append(f\"available_models={list(available_models_attr.keys())}\")\n\n available_info.append(\n f\" [{idx}] {emb_type}: {', '.join(identifiers) if identifiers else 'No identifiers'}\"\n )\n\n msg = (\n f\"Embedding model '{target_model_name}' not found in available embeddings.\\n\\n\"\n f\"Available embeddings:\\n\" + \"\\n\".join(available_info) + \"\\n\\n\"\n \"Please set 'embedding_model_name' to one of the identifier values shown above \"\n \"(use the value after the '=' sign, without quotes).\\n\"\n \"For duplicate deployments, use the 'combined' format.\\n\"\n \"Or leave it empty to use the first embedding.\"\n )\n raise ValueError(msg)\n else:\n # Use first embedding if no model name specified\n selected_embedding = embeddings_list[0]\n embedding_model = self._get_embedding_model_name(selected_embedding)\n self.log(f\"No embedding_model_name specified, using first embedding: {embedding_model}\")\n\n dynamic_field_name = get_embedding_field_name(embedding_model)\n\n logger.info(f\"Selected embedding model for ingestion: '{embedding_model}'\")\n self.log(f\"Using embedding model for ingestion: {embedding_model}\")\n self.log(f\"Dynamic vector field: {dynamic_field_name}\")\n\n # Log embedding details for debugging\n if hasattr(selected_embedding, \"deployment\"):\n logger.info(f\"Embedding deployment: {selected_embedding.deployment}\")\n if hasattr(selected_embedding, \"model\"):\n logger.info(f\"Embedding model: {selected_embedding.model}\")\n if hasattr(selected_embedding, \"model_id\"):\n logger.info(f\"Embedding model_id: {selected_embedding.model_id}\")\n if hasattr(selected_embedding, \"dimensions\"):\n logger.info(f\"Embedding dimensions: {selected_embedding.dimensions}\")\n if hasattr(selected_embedding, \"available_models\"):\n logger.info(f\"Embedding available_models: {selected_embedding.available_models}\")\n\n # No model switching needed - each model in available_models has its own dedicated instance\n # The selected_embedding is already configured correctly for the target model\n logger.info(f\"Using embedding instance for '{embedding_model}' - pre-configured and ready to use\")\n\n # Extract texts and metadata from documents\n texts = []\n metadatas = []\n # Process docs_metadata table input into a dict\n additional_metadata = {}\n logger.debug(f\"[LF] Docs metadata {self.docs_metadata}\")\n if hasattr(self, \"docs_metadata\") and self.docs_metadata:\n logger.info(f\"[LF] Docs metadata {self.docs_metadata}\")\n if isinstance(self.docs_metadata[-1], Data):\n logger.info(f\"[LF] Docs metadata is a Data object {self.docs_metadata}\")\n self.docs_metadata = self.docs_metadata[-1].data\n logger.info(f\"[LF] Docs metadata is a Data object {self.docs_metadata}\")\n additional_metadata.update(self.docs_metadata)\n else:\n for item in self.docs_metadata:\n if isinstance(item, dict) and \"key\" in item and \"value\" in item:\n additional_metadata[item[\"key\"]] = item[\"value\"]\n # Replace string \"None\" values with actual None\n for key, value in additional_metadata.items():\n if value == \"None\":\n additional_metadata[key] = None\n logger.info(f\"[LF] Additional metadata {additional_metadata}\")\n for doc_obj in docs:\n data_copy = json.loads(doc_obj.model_dump_json())\n text = data_copy.pop(doc_obj.text_key, doc_obj.default_value)\n texts.append(text)\n\n # Merge additional metadata from table input\n data_copy.update(additional_metadata)\n\n metadatas.append(data_copy)\n self.log(metadatas)\n\n # Generate embeddings with rate-limit-aware retry logic using tenacity\n from tenacity import (\n retry,\n retry_if_exception,\n stop_after_attempt,\n wait_exponential,\n )\n\n def is_rate_limit_error(exception: Exception) -> bool:\n \"\"\"Check if exception is a rate limit error (429).\"\"\"\n error_str = str(exception).lower()\n return \"429\" in error_str or \"rate_limit\" in error_str or \"rate limit\" in error_str\n\n def is_other_retryable_error(exception: Exception) -> bool:\n \"\"\"Check if exception is retryable but not a rate limit error.\"\"\"\n # Retry on most exceptions except for specific non-retryable ones\n # Add other non-retryable exceptions here if needed\n return not is_rate_limit_error(exception)\n\n # Create retry decorator for rate limit errors (longer backoff)\n retry_on_rate_limit = retry(\n retry=retry_if_exception(is_rate_limit_error),\n stop=stop_after_attempt(5),\n wait=wait_exponential(multiplier=2, min=2, max=30),\n reraise=True,\n before_sleep=lambda retry_state: logger.warning(\n f\"Rate limit hit for chunk (attempt {retry_state.attempt_number}/5), \"\n f\"backing off for {retry_state.next_action.sleep:.1f}s\"\n ),\n )\n\n # Create retry decorator for other errors (shorter backoff)\n retry_on_other_errors = retry(\n retry=retry_if_exception(is_other_retryable_error),\n stop=stop_after_attempt(3),\n wait=wait_exponential(multiplier=1, min=1, max=8),\n reraise=True,\n before_sleep=lambda retry_state: logger.warning(\n f\"Error embedding chunk (attempt {retry_state.attempt_number}/3), \"\n f\"retrying in {retry_state.next_action.sleep:.1f}s: {retry_state.outcome.exception()}\"\n ),\n )\n\n def embed_chunk_with_retry(chunk_text: str, chunk_idx: int) -> list[float]:\n \"\"\"Embed a single chunk with rate-limit-aware retry logic.\"\"\"\n\n @retry_on_rate_limit\n @retry_on_other_errors\n def _embed(text: str) -> list[float]:\n return selected_embedding.embed_documents([text])[0]\n\n try:\n return _embed(chunk_text)\n except Exception as e:\n logger.error(\n f\"Failed to embed chunk {chunk_idx} after all retries: {e}\",\n error=str(e),\n )\n raise\n\n # Restrict concurrency for IBM/Watsonx models to avoid rate limits\n is_ibm = (embedding_model and \"ibm\" in str(embedding_model).lower()) or (\n selected_embedding and \"watsonx\" in type(selected_embedding).__name__.lower()\n )\n logger.debug(f\"Is IBM: {is_ibm}\")\n\n # For IBM models, use sequential processing with rate limiting\n # For other models, use parallel processing\n vectors: list[list[float]] = [None] * len(texts)\n\n if is_ibm:\n # Sequential processing with inter-request delay for IBM models\n inter_request_delay = 0.6 # ~1.67 req/s, safely under 2 req/s limit\n logger.info(f\"Using sequential processing for IBM model with {inter_request_delay}s delay between requests\")\n\n for idx, chunk in enumerate(texts):\n if idx > 0:\n # Add delay between requests (but not before the first one)\n time.sleep(inter_request_delay)\n vectors[idx] = embed_chunk_with_retry(chunk, idx)\n else:\n # Parallel processing for non-IBM models\n max_workers = min(max(len(texts), 1), 8)\n logger.debug(f\"Using parallel processing with {max_workers} workers\")\n\n with ThreadPoolExecutor(max_workers=max_workers) as executor:\n futures = {executor.submit(embed_chunk_with_retry, chunk, idx): idx for idx, chunk in enumerate(texts)}\n for future in as_completed(futures):\n idx = futures[future]\n vectors[idx] = future.result()\n\n if not vectors:\n self.log(f\"No vectors generated from documents for model {embedding_model}.\")\n return\n\n # Get vector dimension for mapping\n dim = len(vectors[0]) if vectors else 768 # default fallback\n\n # Check for AOSS\n auth_kwargs = self._build_auth_kwargs()\n is_aoss = self._is_aoss_enabled(auth_kwargs.get(\"http_auth\"))\n\n # Validate engine with AOSS\n engine = getattr(self, \"engine\", \"jvector\")\n self._validate_aoss_with_engines(is_aoss=is_aoss, engine=engine)\n\n # Create mapping with proper KNN settings\n space_type = getattr(self, \"space_type\", \"l2\")\n ef_construction = getattr(self, \"ef_construction\", 512)\n m = getattr(self, \"m\", 16)\n\n mapping = self._default_text_mapping(\n dim=dim,\n engine=engine,\n space_type=space_type,\n ef_construction=ef_construction,\n m=m,\n vector_field=dynamic_field_name, # Use dynamic field name\n )\n\n # Ensure index exists with baseline mapping (index.knn: true is required for vector search)\n try:\n if not client.indices.exists(index=self.index_name):\n self.log(f\"Creating index '{self.index_name}' with base mapping\")\n client.indices.create(index=self.index_name, body=mapping)\n except RequestError as creation_error:\n if creation_error.error == \"resource_already_exists_exception\":\n pass # Index was created concurrently\n else:\n error_msg = str(creation_error).lower()\n if \"invalid engine\" in error_msg or \"illegal_argument\" in error_msg:\n if \"jvector\" in error_msg:\n msg = (\n \"The 'jvector' engine is not available in your OpenSearch installation. \"\n \"Use 'nmslib' or 'faiss' for standard OpenSearch, or upgrade to 2.9+.\"\n )\n raise ValueError(msg) from creation_error\n if \"index.knn\" in error_msg:\n msg = (\n \"The index has index.knn: false. Delete the existing index and let the \"\n \"component recreate it, or create a new index with a different name.\"\n )\n raise ValueError(msg) from creation_error\n logger.warning(f\"Failed to create index '{self.index_name}': {creation_error}\")\n raise\n\n # Ensure the dynamic field exists in the index\n self._ensure_embedding_field_mapping(\n client=client,\n index_name=self.index_name,\n field_name=dynamic_field_name,\n dim=dim,\n engine=engine,\n space_type=space_type,\n ef_construction=ef_construction,\n m=m,\n )\n\n self.log(f\"Indexing {len(texts)} documents into '{self.index_name}' with model '{embedding_model}'...\")\n logger.info(f\"Will store embeddings in field: {dynamic_field_name}\")\n logger.info(f\"Will tag documents with embedding_model: {embedding_model}\")\n\n # Use the bulk ingestion with model tracking\n return_ids = self._bulk_ingest_embeddings(\n client=client,\n index_name=self.index_name,\n embeddings=vectors,\n texts=texts,\n metadatas=metadatas,\n vector_field=dynamic_field_name, # Use dynamic field name\n text_field=\"text\",\n embedding_model=embedding_model, # Track the model\n mapping=mapping,\n is_aoss=is_aoss,\n )\n self.log(metadatas)\n\n logger.info(\n f\"Ingestion complete: Successfully indexed {len(return_ids)} documents with model '{embedding_model}'\"\n )\n self.log(f\"Successfully indexed {len(return_ids)} documents with model {embedding_model}.\")\n\n # ---------- helpers for filters ----------\n def _is_placeholder_term(self, term_obj: dict) -> bool:\n # term_obj like {\"filename\": \"__IMPOSSIBLE_VALUE__\"}\n return any(v == \"__IMPOSSIBLE_VALUE__\" for v in term_obj.values())\n\n def _coerce_filter_clauses(self, filter_obj: dict | None) -> list[dict]:\n \"\"\"Convert filter expressions into OpenSearch-compatible filter clauses.\n\n This method accepts two filter formats and converts them to standardized\n OpenSearch query clauses:\n\n Format A - Explicit filters:\n {\"filter\": [{\"term\": {\"field\": \"value\"}}, {\"terms\": {\"field\": [\"val1\", \"val2\"]}}],\n \"limit\": 10, \"score_threshold\": 1.5}\n\n Format B - Context-style mapping:\n {\"data_sources\": [\"file1.pdf\"], \"document_types\": [\"pdf\"], \"owners\": [\"user1\"]}\n\n Args:\n filter_obj: Filter configuration dictionary or None\n\n Returns:\n List of OpenSearch filter clauses (term/terms objects)\n Placeholder values with \"__IMPOSSIBLE_VALUE__\" are ignored\n \"\"\"\n if not filter_obj:\n return []\n\n # If it is a string, try to parse it once\n if isinstance(filter_obj, str):\n try:\n filter_obj = json.loads(filter_obj)\n except json.JSONDecodeError:\n # Not valid JSON - treat as no filters\n return []\n\n # Case A: already an explicit list/dict under \"filter\"\n if \"filter\" in filter_obj:\n raw = filter_obj[\"filter\"]\n if isinstance(raw, dict):\n raw = [raw]\n explicit_clauses: list[dict] = []\n for f in raw or []:\n if \"term\" in f and isinstance(f[\"term\"], dict) and not self._is_placeholder_term(f[\"term\"]):\n explicit_clauses.append(f)\n elif \"terms\" in f and isinstance(f[\"terms\"], dict):\n field, vals = next(iter(f[\"terms\"].items()))\n if isinstance(vals, list) and len(vals) > 0:\n explicit_clauses.append(f)\n return explicit_clauses\n\n # Case B: convert context-style maps into clauses\n field_mapping = {\n \"data_sources\": \"filename\",\n \"document_types\": \"mimetype\",\n \"owners\": \"owner\",\n }\n context_clauses: list[dict] = []\n for k, values in filter_obj.items():\n if not isinstance(values, list):\n continue\n field = field_mapping.get(k, k)\n if len(values) == 0:\n # Match-nothing placeholder (kept to mirror your tool semantics)\n context_clauses.append({\"term\": {field: \"__IMPOSSIBLE_VALUE__\"}})\n elif len(values) == 1:\n if values[0] != \"__IMPOSSIBLE_VALUE__\":\n context_clauses.append({\"term\": {field: values[0]}})\n else:\n context_clauses.append({\"terms\": {field: values}})\n return context_clauses\n\n def _detect_available_models(self, client: OpenSearch, filter_clauses: list[dict] | None = None) -> list[str]:\n \"\"\"Detect which embedding models have documents in the index.\n\n Uses aggregation to find all unique embedding_model values, optionally\n filtered to only documents matching the user's filter criteria.\n\n Args:\n client: OpenSearch client instance\n filter_clauses: Optional filter clauses to scope model detection\n\n Returns:\n List of embedding model names found in the index\n \"\"\"\n try:\n agg_query = {\"size\": 0, \"aggs\": {\"embedding_models\": {\"terms\": {\"field\": \"embedding_model\", \"size\": 10}}}}\n\n # Apply filters to model detection if any exist\n if filter_clauses:\n agg_query[\"query\"] = {\"bool\": {\"filter\": filter_clauses}}\n\n logger.debug(f\"Model detection query: {agg_query}\")\n result = client.search(\n index=self.index_name,\n body=agg_query,\n params={\"terminate_after\": 0},\n )\n buckets = result.get(\"aggregations\", {}).get(\"embedding_models\", {}).get(\"buckets\", [])\n models = [b[\"key\"] for b in buckets if b[\"key\"]]\n\n # Log detailed bucket info for debugging\n logger.info(\n f\"Detected embedding models in corpus: {models}\"\n + (f\" (with {len(filter_clauses)} filters)\" if filter_clauses else \"\")\n )\n if not models:\n total_hits = result.get(\"hits\", {}).get(\"total\", {})\n total_count = total_hits.get(\"value\", 0) if isinstance(total_hits, dict) else total_hits\n logger.warning(\n f\"No embedding_model values found in index '{self.index_name}'. \"\n f\"Total docs in index: {total_count}. \"\n f\"This may indicate documents were indexed without the embedding_model field.\"\n )\n except (OpenSearchException, KeyError, ValueError) as e:\n logger.warning(f\"Failed to detect embedding models: {e}\")\n # Fallback to current model\n fallback_model = self._get_embedding_model_name()\n logger.info(f\"Using fallback model: {fallback_model}\")\n return [fallback_model]\n else:\n return models\n\n def _get_index_properties(self, client: OpenSearch) -> dict[str, Any] | None:\n \"\"\"Retrieve flattened mapping properties for the current index.\"\"\"\n try:\n mapping = client.indices.get_mapping(index=self.index_name)\n except OpenSearchException as e:\n logger.warning(\n f\"Failed to fetch mapping for index '{self.index_name}': {e}. Proceeding without mapping metadata.\"\n )\n return None\n\n properties: dict[str, Any] = {}\n for index_data in mapping.values():\n props = index_data.get(\"mappings\", {}).get(\"properties\", {})\n if isinstance(props, dict):\n properties.update(props)\n return properties\n\n def _is_knn_vector_field(self, properties: dict[str, Any] | None, field_name: str) -> bool:\n \"\"\"Check whether the field is mapped as a knn_vector.\"\"\"\n if not field_name:\n return False\n if properties is None:\n logger.warning(f\"Mapping metadata unavailable; assuming field '{field_name}' is usable.\")\n return True\n field_def = properties.get(field_name)\n if not isinstance(field_def, dict):\n return False\n if field_def.get(\"type\") == \"knn_vector\":\n return True\n\n nested_props = field_def.get(\"properties\")\n return bool(isinstance(nested_props, dict) and nested_props.get(\"type\") == \"knn_vector\")\n\n def _get_field_dimension(self, properties: dict[str, Any] | None, field_name: str) -> int | None:\n \"\"\"Get the dimension of a knn_vector field from the index mapping.\n\n Args:\n properties: Index properties from mapping\n field_name: Name of the vector field\n\n Returns:\n Dimension of the field, or None if not found\n \"\"\"\n if not field_name or properties is None:\n return None\n\n field_def = properties.get(field_name)\n if not isinstance(field_def, dict):\n return None\n\n # Check direct knn_vector field\n if field_def.get(\"type\") == \"knn_vector\":\n return field_def.get(\"dimension\")\n\n # Check nested properties\n nested_props = field_def.get(\"properties\")\n if isinstance(nested_props, dict) and nested_props.get(\"type\") == \"knn_vector\":\n return nested_props.get(\"dimension\")\n\n return None\n\n def _get_filename_agg_field(self, index_properties: dict[str, Any] | None) -> str:\n \"\"\"Choose the appropriate field for filename aggregations.\"\"\"\n if not index_properties:\n return \"filename.keyword\"\n\n filename_def = index_properties.get(\"filename\")\n if not isinstance(filename_def, dict):\n return \"filename.keyword\"\n\n field_type = filename_def.get(\"type\")\n fields_def = filename_def.get(\"fields\", {})\n\n # Top-level keyword with no subfields\n if field_type == \"keyword\" and not isinstance(fields_def, dict):\n return \"filename\"\n\n # Text field with keyword subfield\n if isinstance(fields_def, dict) and \"keyword\" in fields_def:\n return \"filename.keyword\"\n\n # Fallback: aggregate on filename directly\n return \"filename\"\n\n # ---------- search (multi-model hybrid) ----------\n def search(self, query: str | None = None) -> list[dict[str, Any]]:\n \"\"\"Perform multi-model hybrid search combining multiple vector similarities and keyword matching.\n\n This method executes a sophisticated search that:\n 1. Auto-detects all embedding models present in the index\n 2. Generates query embeddings for ALL detected models in parallel\n 3. Combines multiple KNN queries using dis_max (picks best match)\n 4. Adds keyword search with fuzzy matching (30% weight)\n 5. Applies optional filtering and score thresholds\n 6. Returns aggregations for faceted search\n\n Search weights:\n - Semantic search (dis_max across all models): 70%\n - Keyword search: 30%\n\n Args:\n query: Search query string (used for both vector embedding and keyword search)\n\n Returns:\n List of search results with page_content, metadata, and relevance scores\n\n Raises:\n ValueError: If embedding component is not provided or filter JSON is invalid\n \"\"\"\n logger.info(self.ingest_data)\n client = self.build_client()\n q = (query or \"\").strip()\n\n # Parse optional filter expression\n filter_obj = None\n if getattr(self, \"filter_expression\", \"\") and self.filter_expression.strip():\n try:\n filter_obj = json.loads(self.filter_expression)\n except json.JSONDecodeError as e:\n msg = f\"Invalid filter_expression JSON: {e}\"\n raise ValueError(msg) from e\n\n if not self.embedding:\n msg = \"Embedding is required to run hybrid search (KNN + keyword).\"\n raise ValueError(msg)\n\n # Check if embedding is None (fail-safe mode)\n if self.embedding is None or (isinstance(self.embedding, list) and all(e is None for e in self.embedding)):\n logger.error(\"Embedding returned None (fail-safe mode enabled). Cannot perform search.\")\n return []\n\n # Build filter clauses first so we can use them in model detection\n filter_clauses = self._coerce_filter_clauses(filter_obj)\n\n # Detect available embedding models in the index (scoped by filters)\n available_models = self._detect_available_models(client, filter_clauses)\n\n if not available_models:\n logger.warning(\"No embedding models found in index, using current model\")\n available_models = [self._get_embedding_model_name()]\n\n # Generate embeddings for ALL detected models\n query_embeddings = {}\n\n # Normalize embedding to list\n embeddings_list = self.embedding if isinstance(self.embedding, list) else [self.embedding]\n # Filter out None values (fail-safe mode)\n embeddings_list = [e for e in embeddings_list if e is not None]\n\n if not embeddings_list:\n logger.error(\n \"No valid embeddings available after filtering None values (fail-safe mode). Cannot perform search.\"\n )\n return []\n\n # Create a comprehensive map of model names to embedding objects\n # Check all possible identifiers (deployment, model, model_id, model_name)\n # Also leverage available_models list from EmbeddingsWithModels\n # Handle duplicate identifiers by creating combined keys\n embedding_by_model = {}\n identifier_conflicts = {} # Track which identifiers have conflicts\n\n for idx, emb_obj in enumerate(embeddings_list):\n # Get all possible identifiers for this embedding\n identifiers = []\n deployment = getattr(emb_obj, \"deployment\", None)\n model = getattr(emb_obj, \"model\", None)\n model_id = getattr(emb_obj, \"model_id\", None)\n model_name = getattr(emb_obj, \"model_name\", None)\n dimensions = getattr(emb_obj, \"dimensions\", None)\n available_models_attr = getattr(emb_obj, \"available_models\", None)\n\n logger.info(\n f\"Embedding object {idx}: deployment={deployment}, model={model}, \"\n f\"model_id={model_id}, model_name={model_name}, dimensions={dimensions}, \"\n f\"available_models={available_models_attr}\"\n )\n\n # If this embedding has available_models dict, map all models to their dedicated instances\n if available_models_attr and isinstance(available_models_attr, dict):\n logger.info(\n f\"Embedding object {idx} provides {len(available_models_attr)} models via available_models dict\"\n )\n for model_name_key, dedicated_embedding in available_models_attr.items():\n if model_name_key and str(model_name_key).strip():\n model_str = str(model_name_key).strip()\n if model_str not in embedding_by_model:\n # Use the dedicated embedding instance from the dict\n embedding_by_model[model_str] = dedicated_embedding\n logger.info(f\"Mapped available model '{model_str}' to dedicated embedding instance\")\n else:\n # Conflict detected - track it\n if model_str not in identifier_conflicts:\n identifier_conflicts[model_str] = [embedding_by_model[model_str]]\n identifier_conflicts[model_str].append(dedicated_embedding)\n logger.warning(f\"Available model '{model_str}' has conflict - used by multiple embeddings\")\n\n # Also map traditional identifiers (for backward compatibility)\n if deployment:\n identifiers.append(str(deployment))\n if model:\n identifiers.append(str(model))\n if model_id:\n identifiers.append(str(model_id))\n if model_name:\n identifiers.append(str(model_name))\n\n # Map all identifiers to this embedding object\n for identifier in identifiers:\n if identifier not in embedding_by_model:\n embedding_by_model[identifier] = emb_obj\n logger.info(f\"Mapped identifier '{identifier}' to embedding object {idx}\")\n else:\n # Conflict detected - track it\n if identifier not in identifier_conflicts:\n identifier_conflicts[identifier] = [embedding_by_model[identifier]]\n identifier_conflicts[identifier].append(emb_obj)\n logger.warning(f\"Identifier '{identifier}' has conflict - used by multiple embeddings\")\n\n # For embeddings with model+deployment, create combined identifier\n # This helps when deployment is the same but model differs\n if deployment and model and deployment != model:\n combined_id = f\"{deployment}:{model}\"\n if combined_id not in embedding_by_model:\n embedding_by_model[combined_id] = emb_obj\n logger.info(f\"Created combined identifier '{combined_id}' for embedding object {idx}\")\n\n # Log conflicts\n if identifier_conflicts:\n logger.warning(\n f\"Found {len(identifier_conflicts)} conflicting identifiers. \"\n f\"Consider using combined format 'deployment:model' or specifying unique model names.\"\n )\n for conflict_id, emb_list in identifier_conflicts.items():\n logger.warning(f\" Conflict on '{conflict_id}': {len(emb_list)} embeddings use this identifier\")\n\n logger.info(f\"Generating embeddings for {len(available_models)} models in index\")\n logger.info(f\"Available embedding identifiers: {list(embedding_by_model.keys())}\")\n self.log(f\"[SEARCH] Models detected in index: {available_models}\")\n self.log(f\"[SEARCH] Available embedding identifiers: {list(embedding_by_model.keys())}\")\n\n # Track matching status for debugging\n matched_models = []\n unmatched_models = []\n\n for model_name in available_models:\n try:\n # Check if we have an embedding object for this model\n if model_name in embedding_by_model:\n # Use the matching embedding object directly\n emb_obj = embedding_by_model[model_name]\n emb_deployment = getattr(emb_obj, \"deployment\", None)\n emb_model = getattr(emb_obj, \"model\", None)\n emb_model_id = getattr(emb_obj, \"model_id\", None)\n emb_dimensions = getattr(emb_obj, \"dimensions\", None)\n emb_available_models = getattr(emb_obj, \"available_models\", None)\n\n logger.info(\n f\"Using embedding object for model '{model_name}': \"\n f\"deployment={emb_deployment}, model={emb_model}, model_id={emb_model_id}, \"\n f\"dimensions={emb_dimensions}\"\n )\n\n # Check if this is a dedicated instance from available_models dict\n if emb_available_models and isinstance(emb_available_models, dict):\n logger.info(\n f\"Model '{model_name}' using dedicated instance from available_models dict \"\n f\"(pre-configured with correct model and dimensions)\"\n )\n\n # Use the embedding instance directly - no model switching needed!\n vec = emb_obj.embed_query(q)\n query_embeddings[model_name] = vec\n matched_models.append(model_name)\n logger.info(f\"Generated embedding for model: {model_name} (actual dimensions: {len(vec)})\")\n self.log(f\"[MATCH] Model '{model_name}' - generated {len(vec)}-dim embedding\")\n else:\n # No matching embedding found for this model\n unmatched_models.append(model_name)\n logger.warning(\n f\"No matching embedding found for model '{model_name}'. \"\n f\"This model will be skipped. Available identifiers: {list(embedding_by_model.keys())}\"\n )\n self.log(f\"[NO MATCH] Model '{model_name}' - available: {list(embedding_by_model.keys())}\")\n except (RuntimeError, ValueError, ConnectionError, TimeoutError, AttributeError, KeyError) as e:\n logger.warning(f\"Failed to generate embedding for {model_name}: {e}\")\n self.log(f\"[ERROR] Embedding generation failed for '{model_name}': {e}\")\n\n # Log summary of model matching\n logger.info(f\"Model matching summary: {len(matched_models)} matched, {len(unmatched_models)} unmatched\")\n self.log(f\"[SUMMARY] Model matching: {len(matched_models)} matched, {len(unmatched_models)} unmatched\")\n if unmatched_models:\n self.log(f\"[WARN] Unmatched models in index: {unmatched_models}\")\n\n if not query_embeddings:\n msg = (\n f\"Failed to generate embeddings for any model. \"\n f\"Index has models: {available_models}, but no matching embedding objects found. \"\n f\"Available embedding identifiers: {list(embedding_by_model.keys())}\"\n )\n self.log(f\"[FAIL] Search failed: {msg}\")\n raise ValueError(msg)\n\n index_properties = self._get_index_properties(client)\n legacy_vector_field = getattr(self, \"vector_field\", \"chunk_embedding\")\n\n # Build KNN queries for each model\n embedding_fields: list[str] = []\n knn_queries_with_candidates = []\n knn_queries_without_candidates = []\n\n raw_num_candidates = getattr(self, \"num_candidates\", 1000)\n try:\n num_candidates = int(raw_num_candidates) if raw_num_candidates is not None else 0\n except (TypeError, ValueError):\n num_candidates = 0\n use_num_candidates = num_candidates > 0\n\n for model_name, embedding_vector in query_embeddings.items():\n field_name = get_embedding_field_name(model_name)\n selected_field = field_name\n vector_dim = len(embedding_vector)\n\n # Only use the expected dynamic field - no legacy fallback\n # This prevents dimension mismatches between models\n if not self._is_knn_vector_field(index_properties, selected_field):\n logger.warning(\n f\"Skipping model {model_name}: field '{field_name}' is not mapped as knn_vector. \"\n f\"Documents must be indexed with this embedding model before querying.\"\n )\n self.log(f\"[SKIP] Field '{selected_field}' not a knn_vector - skipping model '{model_name}'\")\n continue\n\n # Validate vector dimensions match the field dimensions\n field_dim = self._get_field_dimension(index_properties, selected_field)\n if field_dim is not None and field_dim != vector_dim:\n logger.error(\n f\"Dimension mismatch for model '{model_name}': \"\n f\"Query vector has {vector_dim} dimensions but field '{selected_field}' expects {field_dim}. \"\n f\"Skipping this model to prevent search errors.\"\n )\n self.log(f\"[DIM MISMATCH] Model '{model_name}': query={vector_dim} vs field={field_dim} - skipping\")\n continue\n\n logger.info(\n f\"Adding KNN query for model '{model_name}': field='{selected_field}', \"\n f\"query_dims={vector_dim}, field_dims={field_dim or 'unknown'}\"\n )\n embedding_fields.append(selected_field)\n\n base_query = {\n \"knn\": {\n selected_field: {\n \"vector\": embedding_vector,\n \"k\": 50,\n }\n }\n }\n\n if use_num_candidates:\n query_with_candidates = copy.deepcopy(base_query)\n query_with_candidates[\"knn\"][selected_field][\"num_candidates\"] = num_candidates\n else:\n query_with_candidates = base_query\n\n knn_queries_with_candidates.append(query_with_candidates)\n knn_queries_without_candidates.append(base_query)\n\n if not knn_queries_with_candidates:\n # No valid fields found - this can happen when:\n # 1. Index is empty (no documents yet)\n # 2. Embedding model has changed and field doesn't exist yet\n # Return empty results instead of failing\n logger.warning(\n \"No valid knn_vector fields found for embedding models. \"\n \"This may indicate an empty index or missing field mappings. \"\n \"Returning empty search results.\"\n )\n self.log(\n f\"[WARN] No valid KNN queries could be built. \"\n f\"Query embeddings generated: {list(query_embeddings.keys())}, \"\n f\"but no matching knn_vector fields found in index.\"\n )\n return []\n\n # Build exists filter - document must have at least one embedding field\n exists_any_embedding = {\n \"bool\": {\"should\": [{\"exists\": {\"field\": f}} for f in set(embedding_fields)], \"minimum_should_match\": 1}\n }\n\n # Combine user filters with exists filter\n all_filters = [*filter_clauses, exists_any_embedding]\n\n # Get limit and score threshold\n limit = (filter_obj or {}).get(\"limit\", self.number_of_results)\n score_threshold = (filter_obj or {}).get(\"score_threshold\", 0)\n\n # Determine the best aggregation field for filename based on index mapping\n filename_agg_field = self._get_filename_agg_field(index_properties)\n\n # Build multi-model hybrid query\n body = {\n \"query\": {\n \"bool\": {\n \"should\": [\n {\n \"dis_max\": {\n \"tie_breaker\": 0.0, # Take only the best match, no blending\n \"boost\": 0.7, # 70% weight for semantic search\n \"queries\": knn_queries_with_candidates,\n }\n },\n {\n \"multi_match\": {\n \"query\": q,\n \"fields\": [\"text^2\", \"filename^1.5\"],\n \"type\": \"best_fields\",\n \"fuzziness\": \"AUTO\",\n \"boost\": 0.3, # 30% weight for keyword search\n }\n },\n ],\n \"minimum_should_match\": 1,\n \"filter\": all_filters,\n }\n },\n \"aggs\": {\n \"data_sources\": {\"terms\": {\"field\": filename_agg_field, \"size\": 20}},\n \"document_types\": {\"terms\": {\"field\": \"mimetype\", \"size\": 10}},\n \"owners\": {\"terms\": {\"field\": \"owner\", \"size\": 10}},\n \"embedding_models\": {\"terms\": {\"field\": \"embedding_model\", \"size\": 10}},\n },\n \"_source\": [\n \"filename\",\n \"mimetype\",\n \"page\",\n \"text\",\n \"source_url\",\n \"owner\",\n \"embedding_model\",\n \"allowed_users\",\n \"allowed_groups\",\n ],\n \"size\": limit,\n }\n\n if isinstance(score_threshold, (int, float)) and score_threshold > 0:\n body[\"min_score\"] = score_threshold\n\n logger.info(\n f\"Executing multi-model hybrid search with {len(knn_queries_with_candidates)} embedding models: \"\n f\"{list(query_embeddings.keys())}\"\n )\n self.log(f\"[EXEC] Executing search with {len(knn_queries_with_candidates)} KNN queries, limit={limit}\")\n self.log(f\"[EXEC] Embedding models used: {list(query_embeddings.keys())}\")\n self.log(f\"[EXEC] KNN fields being queried: {embedding_fields}\")\n\n try:\n resp = client.search(index=self.index_name, body=body, params={\"terminate_after\": 0})\n except RequestError as e:\n error_message = str(e)\n lowered = error_message.lower()\n if use_num_candidates and \"num_candidates\" in lowered:\n logger.warning(\n \"Retrying search without num_candidates parameter due to cluster capabilities\",\n error=error_message,\n )\n fallback_body = copy.deepcopy(body)\n try:\n fallback_body[\"query\"][\"bool\"][\"should\"][0][\"dis_max\"][\"queries\"] = knn_queries_without_candidates\n except (KeyError, IndexError, TypeError) as inner_err:\n raise e from inner_err\n resp = client.search(\n index=self.index_name,\n body=fallback_body,\n params={\"terminate_after\": 0},\n )\n elif \"knn_vector\" in lowered or (\"field\" in lowered and \"knn\" in lowered):\n fallback_vector = next(iter(query_embeddings.values()), None)\n if fallback_vector is None:\n raise\n fallback_field = legacy_vector_field or \"chunk_embedding\"\n logger.warning(\n \"KNN search failed for dynamic fields; falling back to legacy field '%s'.\",\n fallback_field,\n )\n fallback_body = copy.deepcopy(body)\n fallback_body[\"query\"][\"bool\"][\"filter\"] = filter_clauses\n knn_fallback = {\n \"knn\": {\n fallback_field: {\n \"vector\": fallback_vector,\n \"k\": 50,\n }\n }\n }\n if use_num_candidates:\n knn_fallback[\"knn\"][fallback_field][\"num_candidates\"] = num_candidates\n fallback_body[\"query\"][\"bool\"][\"should\"][0][\"dis_max\"][\"queries\"] = [knn_fallback]\n resp = client.search(\n index=self.index_name,\n body=fallback_body,\n params={\"terminate_after\": 0},\n )\n else:\n raise\n hits = resp.get(\"hits\", {}).get(\"hits\", [])\n\n logger.info(f\"Found {len(hits)} results\")\n self.log(f\"[RESULT] Search complete: {len(hits)} results found\")\n\n if len(hits) == 0:\n self.log(\n f\"[EMPTY] Debug info: \"\n f\"models_in_index={available_models}, \"\n f\"matched_models={matched_models}, \"\n f\"knn_fields={embedding_fields}, \"\n f\"filters={len(filter_clauses)} clauses\"\n )\n\n return [\n {\n \"page_content\": hit[\"_source\"].get(\"text\", \"\"),\n \"metadata\": {k: v for k, v in hit[\"_source\"].items() if k != \"text\"},\n \"score\": hit.get(\"_score\"),\n }\n for hit in hits\n ]\n\n def search_documents(self) -> list[Data]:\n \"\"\"Search documents and return results as Data objects.\n\n This is the main interface method that performs the multi-model search using the\n configured search_query and returns results in Langflow's Data format.\n\n Always builds the vector store (triggering ingestion if needed), then performs\n search only if a query is provided.\n\n Returns:\n List of Data objects containing search results with text and metadata\n\n Raises:\n Exception: If search operation fails\n \"\"\"\n try:\n # Always build/cache the vector store to ensure ingestion happens\n logger.info(f\"Search query: {self.search_query}\")\n if self._cached_vector_store is None:\n self.build_vector_store()\n\n # Only perform search if query is provided\n search_query = (self.search_query or \"\").strip()\n if not search_query:\n self.log(\"No search query provided - ingestion completed, returning empty results\")\n return []\n\n # Perform search with the provided query\n raw = self.search(search_query)\n return [Data(text=hit[\"page_content\"], **hit[\"metadata\"]) for hit in raw]\n except Exception as e:\n self.log(f\"search_documents error: {e}\")\n raise\n\n # -------- dynamic UI handling (auth switch) --------\n async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n \"\"\"Dynamically update component configuration based on field changes.\n\n This method handles real-time UI updates, particularly for authentication\n mode changes that show/hide relevant input fields.\n\n Args:\n build_config: Current component configuration\n field_value: New value for the changed field\n field_name: Name of the field that changed\n\n Returns:\n Updated build configuration with appropriate field visibility\n \"\"\"\n try:\n if field_name == \"auth_mode\":\n mode = (field_value or \"basic\").strip().lower()\n is_basic = mode == \"basic\"\n is_jwt = mode == \"jwt\"\n\n build_config[\"username\"][\"show\"] = is_basic\n build_config[\"password\"][\"show\"] = is_basic\n\n build_config[\"jwt_token\"][\"show\"] = is_jwt\n build_config[\"jwt_header\"][\"show\"] = is_jwt\n build_config[\"bearer_prefix\"][\"show\"] = is_jwt\n\n build_config[\"username\"][\"required\"] = is_basic\n build_config[\"password\"][\"required\"] = is_basic\n\n build_config[\"jwt_token\"][\"required\"] = is_jwt\n build_config[\"jwt_header\"][\"required\"] = is_jwt\n build_config[\"bearer_prefix\"][\"required\"] = False\n\n if is_basic:\n build_config[\"jwt_token\"][\"value\"] = \"\"\n\n return build_config\n\n except (KeyError, ValueError) as e:\n self.log(f\"update_build_config error: {e}\")\n\n return build_config\n" + "value": "from __future__ import annotations\n\nimport copy\nimport json\nimport time\nimport uuid\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom typing import Any\n\nfrom opensearchpy import OpenSearch, helpers\nfrom opensearchpy.exceptions import OpenSearchException, RequestError\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.base.vectorstores.vector_store_connection_decorator import vector_store_connection\nfrom lfx.io import (\n BoolInput,\n DropdownInput,\n HandleInput,\n IntInput,\n MultilineInput,\n Output,\n SecretStrInput,\n StrInput,\n TableInput,\n)\nfrom lfx.log import logger\nfrom lfx.schema.data import Data\n\nREQUEST_TIMEOUT = 60\nMAX_RETRIES = 5\n\n\ndef normalize_model_name(model_name: str) -> str:\n \"\"\"Normalize embedding model name for use as field suffix.\n\n Converts model names to valid OpenSearch field names by replacing\n special characters and ensuring alphanumeric format.\n\n Args:\n model_name: Original embedding model name (e.g., \"text-embedding-3-small\")\n\n Returns:\n Normalized field suffix (e.g., \"text_embedding_3_small\")\n \"\"\"\n normalized = model_name.lower()\n # Replace common separators with underscores\n normalized = normalized.replace(\"-\", \"_\").replace(\":\", \"_\").replace(\"/\", \"_\").replace(\".\", \"_\")\n # Remove any non-alphanumeric characters except underscores\n normalized = \"\".join(c if c.isalnum() or c == \"_\" else \"_\" for c in normalized)\n # Remove duplicate underscores\n while \"__\" in normalized:\n normalized = normalized.replace(\"__\", \"_\")\n return normalized.strip(\"_\")\n\n\ndef get_embedding_field_name(model_name: str) -> str:\n \"\"\"Get the dynamic embedding field name for a model.\n\n Args:\n model_name: Embedding model name\n\n Returns:\n Field name in format: chunk_embedding_{normalized_model_name}\n \"\"\"\n logger.info(f\"chunk_embedding_{normalize_model_name(model_name)}\")\n return f\"chunk_embedding_{normalize_model_name(model_name)}\"\n\n\n@vector_store_connection\nclass OpenSearchVectorStoreComponentMultimodalMultiEmbedding(LCVectorStoreComponent):\n \"\"\"OpenSearch Vector Store Component with Multi-Model Hybrid Search Capabilities.\n\n This component provides vector storage and retrieval using OpenSearch, combining semantic\n similarity search (KNN) with keyword-based search for optimal results. It supports:\n - Multiple embedding models per index with dynamic field names\n - Automatic detection and querying of all available embedding models\n - Parallel embedding generation for multi-model search\n - Document ingestion with model tracking\n - Advanced filtering and aggregations\n - Flexible authentication options\n\n Features:\n - Multi-model vector storage with dynamic fields (chunk_embedding_{model_name})\n - Hybrid search combining multiple KNN queries (dis_max) + keyword matching\n - Auto-detection of available models in the index\n - Parallel query embedding generation for all detected models\n - Vector storage with configurable engines (jvector, nmslib, faiss, lucene)\n - Flexible authentication (Basic auth, JWT tokens)\n\n Model Name Resolution:\n - Priority: deployment > model > model_name attributes\n - This ensures correct matching between embedding objects and index fields\n - When multiple embeddings are provided, specify embedding_model_name to select which one to use\n - During search, each detected model in the index is matched to its corresponding embedding object\n \"\"\"\n\n display_name: str = \"OpenSearch (Multi-Model Multi-Embedding)\"\n icon: str = \"OpenSearch\"\n description: str = (\n \"Store and search documents using OpenSearch with multi-model hybrid semantic and keyword search. \"\n \"To search use the tools search_documents and raw_search. \"\n \"Search documents takes a query for vector search, for example\\n\"\n ' {search_query: \"components in openrag\"}'\n )\n\n # Keys we consider baseline\n default_keys: list[str] = [\n \"opensearch_url\",\n \"index_name\",\n *[i.name for i in LCVectorStoreComponent.inputs], # search_query, add_documents, etc.\n \"embedding\",\n \"embedding_model_name\",\n \"vector_field\",\n \"number_of_results\",\n \"auth_mode\",\n \"username\",\n \"password\",\n \"jwt_token\",\n \"jwt_header\",\n \"bearer_prefix\",\n \"use_ssl\",\n \"verify_certs\",\n \"filter_expression\",\n \"engine\",\n \"space_type\",\n \"ef_construction\",\n \"m\",\n \"num_candidates\",\n \"docs_metadata\",\n \"request_timeout\",\n \"max_retries\",\n ]\n\n inputs = [\n TableInput(\n name=\"docs_metadata\",\n display_name=\"Document Metadata\",\n info=(\n \"Additional metadata key-value pairs to be added to all ingested documents. \"\n \"Useful for tagging documents with source information, categories, or other custom attributes.\"\n ),\n table_schema=[\n {\n \"name\": \"key\",\n \"display_name\": \"Key\",\n \"type\": \"str\",\n \"description\": \"Key name\",\n },\n {\n \"name\": \"value\",\n \"display_name\": \"Value\",\n \"type\": \"str\",\n \"description\": \"Value of the metadata\",\n },\n ],\n value=[],\n input_types=[\"Data\", \"JSON\"],\n ),\n StrInput(\n name=\"opensearch_url\",\n display_name=\"OpenSearch URL\",\n value=\"http://localhost:9200\",\n info=(\n \"The connection URL for your OpenSearch cluster \"\n \"(e.g., http://localhost:9200 for local development or your cloud endpoint).\"\n ),\n ),\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n value=\"langflow\",\n info=(\n \"The OpenSearch index name where documents will be stored and searched. \"\n \"Will be created automatically if it doesn't exist.\"\n ),\n ),\n DropdownInput(\n name=\"engine\",\n display_name=\"Vector Engine\",\n options=[\"nmslib\", \"faiss\", \"lucene\", \"jvector\"],\n value=\"jvector\",\n info=(\n \"Vector search engine for similarity calculations. 'nmslib' works with standard \"\n \"OpenSearch. 'jvector' requires OpenSearch 2.9+. 'lucene' requires index.knn: true. \"\n \"Amazon OpenSearch Serverless only supports 'nmslib' or 'faiss'.\"\n ),\n advanced=True,\n ),\n DropdownInput(\n name=\"space_type\",\n display_name=\"Distance Metric\",\n options=[\"l2\", \"l1\", \"cosinesimil\", \"linf\", \"innerproduct\"],\n value=\"l2\",\n info=(\n \"Distance metric for calculating vector similarity. 'l2' (Euclidean) is most common, \"\n \"'cosinesimil' for cosine similarity, 'innerproduct' for dot product.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"ef_construction\",\n display_name=\"EF Construction\",\n value=512,\n info=(\n \"Size of the dynamic candidate list during index construction. \"\n \"Higher values improve recall but increase indexing time and memory usage.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"m\",\n display_name=\"M Parameter\",\n value=16,\n info=(\n \"Number of bidirectional connections for each vector in the HNSW graph. \"\n \"Higher values improve search quality but increase memory usage and indexing time.\"\n ),\n advanced=True,\n ),\n IntInput(\n name=\"num_candidates\",\n display_name=\"Candidate Pool Size\",\n value=1000,\n info=(\n \"Number of approximate neighbors to consider for each KNN query. \"\n \"Some OpenSearch deployments do not support this parameter; set to 0 to disable.\"\n ),\n advanced=True,\n ),\n *LCVectorStoreComponent.inputs, # includes search_query, add_documents, etc.\n HandleInput(name=\"embedding\", display_name=\"Embedding\", input_types=[\"Embeddings\"], is_list=True),\n StrInput(\n name=\"embedding_model_name\",\n display_name=\"Embedding Model Name\",\n value=\"\",\n info=(\n \"Name of the embedding model to use for ingestion. This selects which embedding from the list \"\n \"will be used to embed documents. Matches on deployment, model, model_id, or model_name. \"\n \"For duplicate deployments, use combined format: 'deployment:model' \"\n \"(e.g., 'text-embedding-ada-002:text-embedding-3-large'). \"\n \"Leave empty to use the first embedding. Error message will show all available identifiers.\"\n ),\n advanced=False,\n ),\n StrInput(\n name=\"vector_field\",\n display_name=\"Legacy Vector Field Name\",\n value=\"chunk_embedding\",\n advanced=True,\n info=(\n \"Legacy field name for backward compatibility. New documents use dynamic fields \"\n \"(chunk_embedding_{model_name}) based on the embedding_model_name.\"\n ),\n ),\n IntInput(\n name=\"number_of_results\",\n display_name=\"Default Result Limit\",\n value=10,\n advanced=True,\n info=(\n \"Default maximum number of search results to return when no limit is \"\n \"specified in the filter expression.\"\n ),\n ),\n MultilineInput(\n name=\"filter_expression\",\n display_name=\"Search Filters (JSON)\",\n value=\"\",\n info=(\n \"Optional JSON configuration for search filtering, result limits, and score thresholds.\\n\\n\"\n \"Format 1 - Explicit filters:\\n\"\n '{\"filter\": [{\"term\": {\"filename\":\"doc.pdf\"}}, '\n '{\"terms\":{\"owner\":[\"user1\",\"user2\"]}}], \"limit\": 10, \"score_threshold\": 1.6}\\n\\n'\n \"Format 2 - Context-style mapping:\\n\"\n '{\"data_sources\":[\"file.pdf\"], \"document_types\":[\"application/pdf\"], \"owners\":[\"user123\"]}\\n\\n'\n \"Use __IMPOSSIBLE_VALUE__ as placeholder to ignore specific filters.\"\n ),\n ),\n # ----- Auth controls (dynamic) -----\n DropdownInput(\n name=\"auth_mode\",\n display_name=\"Authentication Mode\",\n value=\"basic\",\n options=[\"basic\", \"jwt\"],\n info=(\n \"Authentication method: 'basic' for username/password authentication, \"\n \"or 'jwt' for JSON Web Token (Bearer) authentication.\"\n ),\n real_time_refresh=True,\n advanced=False,\n ),\n StrInput(\n name=\"username\",\n display_name=\"Username\",\n value=\"admin\",\n show=True,\n ),\n SecretStrInput(\n name=\"password\",\n display_name=\"OpenSearch Password\",\n value=\"admin\",\n show=True,\n ),\n SecretStrInput(\n name=\"jwt_token\",\n display_name=\"JWT Token\",\n value=\"JWT\",\n load_from_db=False,\n show=False,\n info=(\n \"Valid JSON Web Token for authentication. \"\n \"Will be sent in the Authorization header (with optional 'Bearer ' prefix).\"\n ),\n ),\n StrInput(\n name=\"jwt_header\",\n display_name=\"JWT Header Name\",\n value=\"Authorization\",\n show=False,\n advanced=True,\n ),\n BoolInput(\n name=\"bearer_prefix\",\n display_name=\"Prefix 'Bearer '\",\n value=True,\n show=False,\n advanced=True,\n ),\n # ----- TLS -----\n BoolInput(\n name=\"use_ssl\",\n display_name=\"Use SSL/TLS\",\n value=True,\n advanced=True,\n info=\"Enable SSL/TLS encryption for secure connections to OpenSearch.\",\n ),\n BoolInput(\n name=\"verify_certs\",\n display_name=\"Verify SSL Certificates\",\n value=False,\n advanced=True,\n info=(\n \"Verify SSL certificates when connecting. \"\n \"Disable for self-signed certificates in development environments.\"\n ),\n ),\n # ----- Timeout / Retry -----\n StrInput(\n name=\"request_timeout\",\n display_name=\"Request Timeout (seconds)\",\n value=\"60\",\n advanced=True,\n info=(\n \"Time in seconds to wait for a response from OpenSearch. \"\n \"Increase for large bulk ingestion or complex hybrid queries.\"\n ),\n ),\n StrInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n value=\"3\",\n advanced=True,\n info=\"Number of retries for failed connections before raising an error.\",\n ),\n ]\n outputs = [\n Output(\n display_name=\"Search Results\",\n name=\"search_results\",\n method=\"search_documents\",\n ),\n Output(display_name=\"Raw Search\", name=\"raw_search\", method=\"raw_search\"),\n ]\n\n def raw_search(self, query: str | dict | None = None) -> Data:\n \"\"\"Execute a raw OpenSearch query against the target index.\n\n Args:\n query (dict[str, Any]): The OpenSearch query DSL dictionary.\n\n Returns:\n Data: Search results as a Data object.\n\n Raises:\n ValueError: If 'query' is not a valid OpenSearch query (must be a non-empty dict).\n \"\"\"\n raw_query = query if query is not None else self.search_query\n\n if raw_query is None or (isinstance(raw_query, str) and not raw_query.strip()):\n self.log(\"No query provided for raw search - returning empty results\")\n return Data(data={})\n\n if isinstance(raw_query, dict):\n query_body = raw_query\n elif isinstance(raw_query, str):\n s = raw_query.strip()\n\n # First, optimistically try to parse as JSON DSL\n try:\n query_body = json.loads(s)\n except json.JSONDecodeError:\n # Fallback: treat as a basic text query over common fields\n query_body = {\n \"query\": {\n \"multi_match\": {\n \"query\": s,\n \"fields\": [\"text^2\", \"filename^1.5\"],\n \"type\": \"best_fields\",\n \"fuzziness\": \"AUTO\",\n }\n }\n }\n else:\n msg = f\"Unsupported raw_search query type: {type(raw_query)!r}\"\n raise TypeError(msg)\n\n client = self.build_client()\n logger.info(f\"query: {query_body}\")\n resp = client.search(\n index=self.index_name,\n body=query_body,\n params={\"terminate_after\": 0},\n )\n # Remove any _source keys whose value is a list of floats (embedding vectors)\n # Minimum length threshold to identify embedding vectors\n min_vector_length = 100\n\n def is_vector(val):\n # Accepts if it's a list of numbers (float or int) and has reasonable vector length\n return (\n isinstance(val, list) and len(val) > min_vector_length and all(isinstance(x, (float, int)) for x in val)\n )\n\n if \"hits\" in resp and \"hits\" in resp[\"hits\"]:\n for hit in resp[\"hits\"][\"hits\"]:\n source = hit.get(\"_source\")\n if isinstance(source, dict):\n keys_to_remove = [k for k, v in source.items() if is_vector(v)]\n for k in keys_to_remove:\n source.pop(k)\n logger.info(f\"Raw search response (all embedding vectors removed): {resp}\")\n return Data(**resp)\n\n def _get_embedding_model_name(self, embedding_obj=None) -> str:\n \"\"\"Get the embedding model name from component config or embedding object.\n\n Priority: deployment > model > model_id > model_name\n This ensures we use the actual model being deployed, not just the configured model.\n Supports multiple embedding providers (OpenAI, Watsonx, Cohere, etc.)\n\n Args:\n embedding_obj: Specific embedding object to get name from (optional)\n\n Returns:\n Embedding model name\n\n Raises:\n ValueError: If embedding model name cannot be determined\n \"\"\"\n # First try explicit embedding_model_name input\n if hasattr(self, \"embedding_model_name\") and self.embedding_model_name:\n return self.embedding_model_name.strip()\n\n # Try to get from provided embedding object\n if embedding_obj:\n # Priority: deployment > model > model_id > model_name\n if hasattr(embedding_obj, \"deployment\") and embedding_obj.deployment:\n return str(embedding_obj.deployment)\n if hasattr(embedding_obj, \"model\") and embedding_obj.model:\n return str(embedding_obj.model)\n if hasattr(embedding_obj, \"model_id\") and embedding_obj.model_id:\n return str(embedding_obj.model_id)\n if hasattr(embedding_obj, \"model_name\") and embedding_obj.model_name:\n return str(embedding_obj.model_name)\n\n # Try to get from embedding component (legacy single embedding)\n if hasattr(self, \"embedding\") and self.embedding:\n # Handle list of embeddings\n if isinstance(self.embedding, list) and len(self.embedding) > 0:\n first_emb = self.embedding[0]\n if hasattr(first_emb, \"deployment\") and first_emb.deployment:\n return str(first_emb.deployment)\n if hasattr(first_emb, \"model\") and first_emb.model:\n return str(first_emb.model)\n if hasattr(first_emb, \"model_id\") and first_emb.model_id:\n return str(first_emb.model_id)\n if hasattr(first_emb, \"model_name\") and first_emb.model_name:\n return str(first_emb.model_name)\n # Handle single embedding\n elif not isinstance(self.embedding, list):\n if hasattr(self.embedding, \"deployment\") and self.embedding.deployment:\n return str(self.embedding.deployment)\n if hasattr(self.embedding, \"model\") and self.embedding.model:\n return str(self.embedding.model)\n if hasattr(self.embedding, \"model_id\") and self.embedding.model_id:\n return str(self.embedding.model_id)\n if hasattr(self.embedding, \"model_name\") and self.embedding.model_name:\n return str(self.embedding.model_name)\n\n msg = (\n \"Could not determine embedding model name. \"\n \"Please set the 'embedding_model_name' field or ensure the embedding component \"\n \"has a 'deployment', 'model', 'model_id', or 'model_name' attribute.\"\n )\n raise ValueError(msg)\n\n # ---------- helper functions for index management ----------\n def _default_text_mapping(\n self,\n dim: int,\n engine: str = \"jvector\",\n space_type: str = \"l2\",\n ef_search: int = 512,\n ef_construction: int = 100,\n m: int = 16,\n vector_field: str = \"vector_field\",\n ) -> dict[str, Any]:\n \"\"\"Create the default OpenSearch index mapping for vector search.\n\n This method generates the index configuration with k-NN settings optimized\n for approximate nearest neighbor search using the specified vector engine.\n Includes the embedding_model keyword field for tracking which model was used.\n\n Args:\n dim: Dimensionality of the vector embeddings\n engine: Vector search engine (jvector, nmslib, faiss, lucene)\n space_type: Distance metric for similarity calculation\n ef_search: Size of dynamic list used during search\n ef_construction: Size of dynamic list used during index construction\n m: Number of bidirectional links for each vector\n vector_field: Name of the field storing vector embeddings\n\n Returns:\n Dictionary containing OpenSearch index mapping configuration\n \"\"\"\n return {\n \"settings\": {\"index\": {\"knn\": True, \"knn.algo_param.ef_search\": ef_search}},\n \"mappings\": {\n \"properties\": {\n vector_field: {\n \"type\": \"knn_vector\",\n \"dimension\": dim,\n \"method\": {\n \"name\": \"disk_ann\",\n \"space_type\": space_type,\n \"engine\": engine,\n \"parameters\": {\"ef_construction\": ef_construction, \"m\": m},\n },\n },\n \"embedding_model\": {\"type\": \"keyword\"}, # Track which model was used\n \"embedding_dimensions\": {\"type\": \"integer\"},\n }\n },\n }\n\n def _ensure_embedding_field_mapping(\n self,\n client: OpenSearch,\n index_name: str,\n field_name: str,\n dim: int,\n engine: str,\n space_type: str,\n ef_construction: int,\n m: int,\n ) -> None:\n \"\"\"Lazily add a dynamic embedding field to the index if it doesn't exist.\n\n This allows adding new embedding models without recreating the entire index.\n Also ensures the embedding_model tracking field exists.\n\n Note: Some OpenSearch versions/configurations have issues with dynamically adding\n knn_vector mappings (NullPointerException). This method checks if the field\n already exists before attempting to add it, and gracefully skips if the field\n is already properly configured.\n\n Args:\n client: OpenSearch client instance\n index_name: Target index name\n field_name: Dynamic field name for this embedding model\n dim: Vector dimensionality\n engine: Vector search engine\n space_type: Distance metric\n ef_construction: Construction parameter\n m: HNSW parameter\n \"\"\"\n # First, check if the field already exists and is properly mapped\n properties = self._get_index_properties(client)\n if self._is_knn_vector_field(properties, field_name):\n # Field already exists as knn_vector - verify dimensions match\n existing_dim = self._get_field_dimension(properties, field_name)\n if existing_dim is not None and existing_dim != dim:\n logger.warning(\n f\"Field '{field_name}' exists with dimension {existing_dim}, \"\n f\"but current embedding has dimension {dim}. Using existing mapping.\"\n )\n else:\n logger.info(\n f\"[OpenSearchMultimodel] Field '{field_name}' already exists\"\n f\"as knn_vector with matching dimensions - skipping mapping update\"\n )\n return\n\n # Field doesn't exist, try to add the mapping\n try:\n mapping = {\n \"properties\": {\n field_name: {\n \"type\": \"knn_vector\",\n \"dimension\": dim,\n \"method\": {\n \"name\": \"disk_ann\",\n \"space_type\": space_type,\n \"engine\": engine,\n \"parameters\": {\"ef_construction\": ef_construction, \"m\": m},\n },\n },\n # Also ensure the embedding_model tracking field exists as keyword\n \"embedding_model\": {\"type\": \"keyword\"},\n \"embedding_dimensions\": {\"type\": \"integer\"},\n }\n }\n client.indices.put_mapping(index=index_name, body=mapping)\n logger.info(f\"Added/updated embedding field mapping: {field_name}\")\n except RequestError as e:\n error_str = str(e).lower()\n if \"invalid engine\" in error_str and \"jvector\" in error_str:\n msg = (\n \"The 'jvector' engine is not available in your OpenSearch installation. \"\n \"Use 'nmslib' or 'faiss' for standard OpenSearch, or upgrade to OpenSearch 2.9+.\"\n )\n raise ValueError(msg) from e\n if \"index.knn\" in error_str:\n msg = (\n \"The index has index.knn: false. Delete the existing index and let the \"\n \"component recreate it, or create a new index with a different name.\"\n )\n raise ValueError(msg) from e\n raise\n except Exception as e:\n # Check if this is the known OpenSearch k-NN NullPointerException issue\n error_str = str(e).lower()\n if \"null\" in error_str or \"nullpointerexception\" in error_str:\n logger.warning(\n f\"[OpenSearchMultimodel] Could not add embedding field mapping for {field_name}\"\n f\"due to OpenSearch k-NN plugin issue: {e}. \"\n f\"This is a known issue with some OpenSearch versions. \"\n f\"[OpenSearchMultimodel] Skipping mapping update. \"\n f\"Please ensure the index has the correct mapping for KNN search to work.\"\n )\n # Skip and continue - ingestion will proceed, but KNN search may fail if mapping doesn't exist\n return\n logger.warning(f\"[OpenSearchMultimodel] Could not add embedding field mapping for {field_name}: {e}\")\n raise\n\n # Verify the field was added correctly\n properties = self._get_index_properties(client)\n if not self._is_knn_vector_field(properties, field_name):\n msg = f\"Field '{field_name}' is not mapped as knn_vector. Current mapping: {properties.get(field_name)}\"\n logger.error(msg)\n raise ValueError(msg)\n\n def _validate_aoss_with_engines(self, *, is_aoss: bool, engine: str) -> None:\n \"\"\"Validate engine compatibility with Amazon OpenSearch Serverless (AOSS).\n\n Amazon OpenSearch Serverless has restrictions on which vector engines\n can be used. This method ensures the selected engine is compatible.\n\n Args:\n is_aoss: Whether the connection is to Amazon OpenSearch Serverless\n engine: The selected vector search engine\n\n Raises:\n ValueError: If AOSS is used with an incompatible engine\n \"\"\"\n if is_aoss and engine not in {\"nmslib\", \"faiss\"}:\n msg = \"Amazon OpenSearch Service Serverless only supports `nmslib` or `faiss` engines\"\n raise ValueError(msg)\n\n def _is_aoss_enabled(self, http_auth: Any) -> bool:\n \"\"\"Determine if Amazon OpenSearch Serverless (AOSS) is being used.\n\n Args:\n http_auth: The HTTP authentication object\n\n Returns:\n True if AOSS is enabled, False otherwise\n \"\"\"\n return http_auth is not None and hasattr(http_auth, \"service\") and http_auth.service == \"aoss\"\n\n def _bulk_ingest_embeddings(\n self,\n client: OpenSearch,\n index_name: str,\n embeddings: list[list[float]],\n texts: list[str],\n metadatas: list[dict] | None = None,\n ids: list[str] | None = None,\n vector_field: str = \"vector_field\",\n text_field: str = \"text\",\n embedding_model: str = \"unknown\",\n mapping: dict | None = None,\n max_chunk_bytes: int | None = 1 * 1024 * 1024,\n *,\n is_aoss: bool = False,\n ) -> list[str]:\n \"\"\"Efficiently ingest multiple documents with embeddings into OpenSearch.\n\n This method uses bulk operations to insert documents with their vector\n embeddings and metadata into the specified OpenSearch index. Each document\n is tagged with the embedding_model name for tracking.\n\n Args:\n client: OpenSearch client instance\n index_name: Target index for document storage\n embeddings: List of vector embeddings for each document\n texts: List of document texts\n metadatas: Optional metadata dictionaries for each document\n ids: Optional document IDs (UUIDs generated if not provided)\n vector_field: Field name for storing vector embeddings\n text_field: Field name for storing document text\n embedding_model: Name of the embedding model used\n mapping: Optional index mapping configuration\n max_chunk_bytes: Maximum size per bulk request chunk\n is_aoss: Whether using Amazon OpenSearch Serverless\n\n Returns:\n List of document IDs that were successfully ingested\n \"\"\"\n logger.debug(f\"[OpenSearchMultimodel] Bulk ingesting embeddings for {index_name}\")\n if not mapping:\n mapping = {}\n\n requests = []\n return_ids = []\n vector_dimensions = len(embeddings[0]) if embeddings else None\n\n for i, text in enumerate(texts):\n metadata = metadatas[i] if metadatas else {}\n if vector_dimensions is not None and \"embedding_dimensions\" not in metadata:\n metadata = {**metadata, \"embedding_dimensions\": vector_dimensions}\n\n # Normalize ACL fields that may arrive as JSON strings from flows\n for key in (\"allowed_users\", \"allowed_groups\"):\n value = metadata.get(key)\n if isinstance(value, str):\n try:\n parsed = json.loads(value)\n if isinstance(parsed, list):\n metadata[key] = parsed\n except (json.JSONDecodeError, TypeError):\n # Leave value as-is if it isn't valid JSON\n pass\n\n _id = ids[i] if ids else str(uuid.uuid4())\n request = {\n \"_op_type\": \"index\",\n \"_index\": index_name,\n vector_field: embeddings[i],\n text_field: text,\n \"embedding_model\": embedding_model, # Track which model was used\n **metadata,\n }\n if is_aoss:\n request[\"id\"] = _id\n else:\n request[\"_id\"] = _id\n requests.append(request)\n return_ids.append(_id)\n if metadatas:\n self.log(f\"Sample metadata: {metadatas[0] if metadatas else {}}\")\n helpers.bulk(client, requests, max_chunk_bytes=max_chunk_bytes)\n return return_ids\n\n # ---------- param helpers ----------\n def _parse_int_param(self, attr_name: str, default: int) -> int:\n \"\"\"Parse a string attribute to int, returning *default* on failure.\"\"\"\n raw = getattr(self, attr_name, None)\n if raw is None or str(raw).strip() == \"\":\n return default\n try:\n value = int(str(raw).strip())\n except ValueError:\n logger.warning(f\"Invalid integer value '{raw}' for {attr_name}, using default {default}\")\n return default\n\n if value < 0:\n logger.warning(f\"Negative value '{raw}' for {attr_name}, using default {default}\")\n return default\n\n return value\n\n # ---------- auth / client ----------\n def _build_auth_kwargs(self) -> dict[str, Any]:\n \"\"\"Build authentication configuration for OpenSearch client.\n\n Constructs the appropriate authentication parameters based on the\n selected auth mode (basic username/password or JWT token).\n\n Returns:\n Dictionary containing authentication configuration\n\n Raises:\n ValueError: If required authentication parameters are missing\n \"\"\"\n mode = (self.auth_mode or \"basic\").strip().lower()\n if mode == \"jwt\":\n token = (self.jwt_token or \"\").strip()\n if not token:\n msg = \"Auth Mode is 'jwt' but no jwt_token was provided.\"\n raise ValueError(msg)\n header_name = (self.jwt_header or \"Authorization\").strip()\n header_value = f\"Bearer {token}\" if self.bearer_prefix else token\n return {\"headers\": {header_name: header_value}}\n user = (self.username or \"\").strip()\n pwd = (self.password or \"\").strip()\n if not user or not pwd:\n msg = \"Auth Mode is 'basic' but username/password are missing.\"\n raise ValueError(msg)\n return {\"http_auth\": (user, pwd)}\n\n def build_client(self) -> OpenSearch:\n \"\"\"Create and configure an OpenSearch client instance.\n\n Returns:\n Configured OpenSearch client ready for operations\n \"\"\"\n logger.debug(\"[OpenSearchMultimodel] Building OpenSearch client\")\n auth_kwargs = self._build_auth_kwargs()\n return OpenSearch(\n hosts=[self.opensearch_url],\n use_ssl=self.use_ssl,\n verify_certs=self.verify_certs,\n ssl_assert_hostname=False,\n ssl_show_warn=False,\n timeout=self._parse_int_param(\"request_timeout\", REQUEST_TIMEOUT),\n max_retries=self._parse_int_param(\"max_retries\", MAX_RETRIES),\n retry_on_timeout=True,\n **auth_kwargs,\n )\n\n @check_cached_vector_store\n def build_vector_store(self) -> OpenSearch:\n # Return raw OpenSearch client as our \"vector store.\"\n client = self.build_client()\n\n # Check if we're in ingestion-only mode (no search query)\n has_search_query = bool((self.search_query or \"\").strip())\n if not has_search_query:\n logger.debug(\"[OpenSearchMultimodel] Ingestion-only mode activated: search operations will be skipped\")\n logger.debug(\"[OpenSearchMultimodel] Starting ingestion mode...\")\n\n logger.debug(f\"[OpenSearchMultimodel] Embedding: {self.embedding}\")\n self._add_documents_to_vector_store(client=client)\n return client\n\n # ---------- ingest ----------\n def _add_documents_to_vector_store(self, client: OpenSearch) -> None:\n \"\"\"Process and ingest documents into the OpenSearch vector store.\n\n This method handles the complete document ingestion pipeline:\n - Prepares document data and metadata\n - Generates vector embeddings using the selected model\n - Creates appropriate index mappings with dynamic field names\n - Bulk inserts documents with vectors and model tracking\n\n Args:\n client: OpenSearch client for performing operations\n \"\"\"\n logger.debug(\"[OpenSearchMultimodel][INGESTION] _add_documents_to_vector_store called\")\n # Convert DataFrame to Data if needed using parent's method\n self.ingest_data = self._prepare_ingest_data()\n\n logger.debug(\n f\"[OpenSearchMultimodel][INGESTION] ingest_data type: \"\n f\"{type(self.ingest_data)}, length: {len(self.ingest_data) if self.ingest_data else 0}\"\n )\n logger.debug(\n f\"[OpenSearchMultimodel][INGESTION] ingest_data content: \"\n f\"{self.ingest_data[:2] if self.ingest_data and len(self.ingest_data) > 0 else 'empty'}\"\n )\n\n docs = self.ingest_data or []\n if not docs:\n logger.debug(\"Ingestion complete: No documents provided\")\n return\n\n if not self.embedding:\n msg = \"Embedding handle is required to embed documents.\"\n raise ValueError(msg)\n\n # Normalize embedding to list first\n embeddings_list = self.embedding if isinstance(self.embedding, list) else [self.embedding]\n\n # Filter out None values (fail-safe mode) - do this BEFORE checking if empty\n embeddings_list = [e for e in embeddings_list if e is not None]\n\n # NOW check if we have any valid embeddings left after filtering\n if not embeddings_list:\n logger.warning(\"All embeddings returned None (fail-safe mode enabled). Skipping document ingestion.\")\n self.log(\"Embedding returned None (fail-safe mode enabled). Skipping document ingestion.\")\n return\n\n logger.debug(f\"[OpenSearchMultimodel][INGESTION] Valid embeddings after filtering: {len(embeddings_list)}\")\n self.log(f\"[OpenSearchMultimodel][INGESTION] Available embedding models: {len(embeddings_list)}\")\n\n # Select the embedding to use for ingestion\n selected_embedding = None\n embedding_model = None\n\n # If embedding_model_name is specified, find matching embedding\n if hasattr(self, \"embedding_model_name\") and self.embedding_model_name and self.embedding_model_name.strip():\n target_model_name = self.embedding_model_name.strip()\n self.log(f\"Looking for embedding model: {target_model_name}\")\n\n for emb_obj in embeddings_list:\n # Check all possible model identifiers (deployment, model, model_id, model_name)\n # Also check available_models list from EmbeddingsWithModels\n possible_names = []\n deployment = getattr(emb_obj, \"deployment\", None)\n model = getattr(emb_obj, \"model\", None)\n model_id = getattr(emb_obj, \"model_id\", None)\n model_name = getattr(emb_obj, \"model_name\", None)\n available_models_attr = getattr(emb_obj, \"available_models\", None)\n\n if deployment:\n possible_names.append(str(deployment))\n if model:\n possible_names.append(str(model))\n if model_id:\n possible_names.append(str(model_id))\n if model_name:\n possible_names.append(str(model_name))\n\n # Also add combined identifier\n if deployment and model and deployment != model:\n possible_names.append(f\"{deployment}:{model}\")\n\n # Add all models from available_models dict\n if available_models_attr and isinstance(available_models_attr, dict):\n possible_names.extend(\n str(model_key).strip()\n for model_key in available_models_attr\n if model_key and str(model_key).strip()\n )\n\n # Match if target matches any of the possible names\n if target_model_name in possible_names:\n # Check if target is in available_models dict - use dedicated instance\n if (\n available_models_attr\n and isinstance(available_models_attr, dict)\n and target_model_name in available_models_attr\n ):\n # Use the dedicated embedding instance from the dict\n selected_embedding = available_models_attr[target_model_name]\n embedding_model = target_model_name\n self.log(f\"Found dedicated embedding instance for '{embedding_model}' in available_models dict\")\n else:\n # Traditional identifier match\n selected_embedding = emb_obj\n embedding_model = self._get_embedding_model_name(emb_obj)\n self.log(f\"Found matching embedding model: {embedding_model} (matched on: {target_model_name})\")\n break\n\n if not selected_embedding:\n # Build detailed list of available embeddings with all their identifiers\n available_info = []\n for idx, emb in enumerate(embeddings_list):\n emb_type = type(emb).__name__\n identifiers = []\n deployment = getattr(emb, \"deployment\", None)\n model = getattr(emb, \"model\", None)\n model_id = getattr(emb, \"model_id\", None)\n model_name = getattr(emb, \"model_name\", None)\n available_models_attr = getattr(emb, \"available_models\", None)\n\n if deployment:\n identifiers.append(f\"deployment='{deployment}'\")\n if model:\n identifiers.append(f\"model='{model}'\")\n if model_id:\n identifiers.append(f\"model_id='{model_id}'\")\n if model_name:\n identifiers.append(f\"model_name='{model_name}'\")\n\n # Add combined identifier as an option\n if deployment and model and deployment != model:\n identifiers.append(f\"combined='{deployment}:{model}'\")\n\n # Add available_models dict if present\n if available_models_attr and isinstance(available_models_attr, dict):\n identifiers.append(f\"available_models={list(available_models_attr.keys())}\")\n\n available_info.append(\n f\" [{idx}] {emb_type}: {', '.join(identifiers) if identifiers else 'No identifiers'}\"\n )\n\n msg = (\n f\"Embedding model '{target_model_name}' not found in available embeddings.\\n\\n\"\n f\"Available embeddings:\\n\" + \"\\n\".join(available_info) + \"\\n\\n\"\n \"Please set 'embedding_model_name' to one of the identifier values shown above \"\n \"(use the value after the '=' sign, without quotes).\\n\"\n \"For duplicate deployments, use the 'combined' format.\\n\"\n \"Or leave it empty to use the first embedding.\"\n )\n raise ValueError(msg)\n else:\n # Use first embedding if no model name specified\n selected_embedding = embeddings_list[0]\n embedding_model = self._get_embedding_model_name(selected_embedding)\n self.log(f\"No embedding_model_name specified, using first embedding: {embedding_model}\")\n\n dynamic_field_name = get_embedding_field_name(embedding_model)\n\n logger.info(f\"Selected embedding model for ingestion: '{embedding_model}'\")\n self.log(f\"Using embedding model for ingestion: {embedding_model}\")\n self.log(f\"Dynamic vector field: {dynamic_field_name}\")\n\n # Log embedding details for debugging\n if hasattr(selected_embedding, \"deployment\"):\n logger.info(f\"Embedding deployment: {selected_embedding.deployment}\")\n if hasattr(selected_embedding, \"model\"):\n logger.info(f\"Embedding model: {selected_embedding.model}\")\n if hasattr(selected_embedding, \"model_id\"):\n logger.info(f\"Embedding model_id: {selected_embedding.model_id}\")\n if hasattr(selected_embedding, \"dimensions\"):\n logger.info(f\"Embedding dimensions: {selected_embedding.dimensions}\")\n if hasattr(selected_embedding, \"available_models\"):\n logger.info(f\"Embedding available_models: {selected_embedding.available_models}\")\n\n # No model switching needed - each model in available_models has its own dedicated instance\n # The selected_embedding is already configured correctly for the target model\n logger.info(f\"Using embedding instance for '{embedding_model}' - pre-configured and ready to use\")\n\n # Extract texts and metadata from documents\n texts = []\n metadatas = []\n # Process docs_metadata table input into a dict\n additional_metadata = {}\n logger.debug(f\"[LF] Docs metadata {self.docs_metadata}\")\n if hasattr(self, \"docs_metadata\") and self.docs_metadata:\n logger.info(f\"[LF] Docs metadata {self.docs_metadata}\")\n if isinstance(self.docs_metadata[-1], Data):\n logger.info(f\"[LF] Docs metadata is a Data object {self.docs_metadata}\")\n self.docs_metadata = self.docs_metadata[-1].data\n logger.info(f\"[LF] Docs metadata is a Data object {self.docs_metadata}\")\n additional_metadata.update(self.docs_metadata)\n else:\n for item in self.docs_metadata:\n if isinstance(item, dict) and \"key\" in item and \"value\" in item:\n additional_metadata[item[\"key\"]] = item[\"value\"]\n # Replace string \"None\" values with actual None\n for key, value in additional_metadata.items():\n if value == \"None\":\n additional_metadata[key] = None\n logger.info(f\"[LF] Additional metadata {additional_metadata}\")\n for doc_obj in docs:\n data_copy = json.loads(doc_obj.model_dump_json())\n text = data_copy.pop(doc_obj.text_key, doc_obj.default_value)\n texts.append(text)\n\n # Merge additional metadata from table input\n data_copy.update(additional_metadata)\n\n metadatas.append(data_copy)\n self.log(metadatas)\n\n # Generate embeddings with rate-limit-aware retry logic using tenacity\n from tenacity import (\n retry,\n retry_if_exception,\n stop_after_attempt,\n wait_exponential,\n )\n\n def is_rate_limit_error(exception: Exception) -> bool:\n \"\"\"Check if exception is a rate limit error (429).\"\"\"\n error_str = str(exception).lower()\n return \"429\" in error_str or \"rate_limit\" in error_str or \"rate limit\" in error_str\n\n def is_other_retryable_error(exception: Exception) -> bool:\n \"\"\"Check if exception is retryable but not a rate limit error.\"\"\"\n # Retry on most exceptions except for specific non-retryable ones\n # Add other non-retryable exceptions here if needed\n return not is_rate_limit_error(exception)\n\n # Create retry decorator for rate limit errors (longer backoff)\n retry_on_rate_limit = retry(\n retry=retry_if_exception(is_rate_limit_error),\n stop=stop_after_attempt(5),\n wait=wait_exponential(multiplier=2, min=2, max=30),\n reraise=True,\n before_sleep=lambda retry_state: logger.warning(\n f\"Rate limit hit for chunk (attempt {retry_state.attempt_number}/5), \"\n f\"backing off for {retry_state.next_action.sleep:.1f}s\"\n ),\n )\n\n # Create retry decorator for other errors (shorter backoff)\n retry_on_other_errors = retry(\n retry=retry_if_exception(is_other_retryable_error),\n stop=stop_after_attempt(3),\n wait=wait_exponential(multiplier=1, min=1, max=8),\n reraise=True,\n before_sleep=lambda retry_state: logger.warning(\n f\"Error embedding chunk (attempt {retry_state.attempt_number}/3), \"\n f\"retrying in {retry_state.next_action.sleep:.1f}s: {retry_state.outcome.exception()}\"\n ),\n )\n\n def embed_chunk_with_retry(chunk_text: str, chunk_idx: int) -> list[float]:\n \"\"\"Embed a single chunk with rate-limit-aware retry logic.\"\"\"\n\n @retry_on_rate_limit\n @retry_on_other_errors\n def _embed(text: str) -> list[float]:\n return selected_embedding.embed_documents([text])[0]\n\n try:\n return _embed(chunk_text)\n except Exception as e:\n logger.error(\n f\"Failed to embed chunk {chunk_idx} after all retries: {e}\",\n error=str(e),\n )\n raise\n\n # Restrict concurrency for IBM/Watsonx models to avoid rate limits\n is_ibm = (embedding_model and \"ibm\" in str(embedding_model).lower()) or (\n selected_embedding and \"watsonx\" in type(selected_embedding).__name__.lower()\n )\n logger.debug(f\"Is IBM: {is_ibm}\")\n\n # For IBM models, use sequential processing with rate limiting\n # For other models, use parallel processing\n vectors: list[list[float]] = [None] * len(texts)\n\n if is_ibm:\n # Sequential processing with inter-request delay for IBM models\n inter_request_delay = 0.6 # ~1.67 req/s, safely under 2 req/s limit\n logger.info(f\"Using sequential processing for IBM model with {inter_request_delay}s delay between requests\")\n\n for idx, chunk in enumerate(texts):\n if idx > 0:\n # Add delay between requests (but not before the first one)\n time.sleep(inter_request_delay)\n vectors[idx] = embed_chunk_with_retry(chunk, idx)\n else:\n # Parallel processing for non-IBM models\n max_workers = min(max(len(texts), 1), 8)\n logger.debug(f\"Using parallel processing with {max_workers} workers\")\n\n with ThreadPoolExecutor(max_workers=max_workers) as executor:\n futures = {executor.submit(embed_chunk_with_retry, chunk, idx): idx for idx, chunk in enumerate(texts)}\n for future in as_completed(futures):\n idx = futures[future]\n vectors[idx] = future.result()\n\n if not vectors:\n self.log(f\"No vectors generated from documents for model {embedding_model}.\")\n return\n\n # Get vector dimension for mapping\n dim = len(vectors[0]) if vectors else 768 # default fallback\n\n # Check for AOSS\n auth_kwargs = self._build_auth_kwargs()\n is_aoss = self._is_aoss_enabled(auth_kwargs.get(\"http_auth\"))\n\n # Validate engine with AOSS\n engine = getattr(self, \"engine\", \"jvector\")\n self._validate_aoss_with_engines(is_aoss=is_aoss, engine=engine)\n\n # Create mapping with proper KNN settings\n space_type = getattr(self, \"space_type\", \"l2\")\n ef_construction = getattr(self, \"ef_construction\", 512)\n m = getattr(self, \"m\", 16)\n\n mapping = self._default_text_mapping(\n dim=dim,\n engine=engine,\n space_type=space_type,\n ef_construction=ef_construction,\n m=m,\n vector_field=dynamic_field_name, # Use dynamic field name\n )\n\n # Ensure index exists with baseline mapping (index.knn: true is required for vector search)\n try:\n if not client.indices.exists(index=self.index_name):\n self.log(f\"Creating index '{self.index_name}' with base mapping\")\n client.indices.create(index=self.index_name, body=mapping)\n except RequestError as creation_error:\n if creation_error.error == \"resource_already_exists_exception\":\n pass # Index was created concurrently\n else:\n error_msg = str(creation_error).lower()\n if \"invalid engine\" in error_msg or \"illegal_argument\" in error_msg:\n if \"jvector\" in error_msg:\n msg = (\n \"The 'jvector' engine is not available in your OpenSearch installation. \"\n \"Use 'nmslib' or 'faiss' for standard OpenSearch, or upgrade to 2.9+.\"\n )\n raise ValueError(msg) from creation_error\n if \"index.knn\" in error_msg:\n msg = (\n \"The index has index.knn: false. Delete the existing index and let the \"\n \"component recreate it, or create a new index with a different name.\"\n )\n raise ValueError(msg) from creation_error\n logger.warning(f\"Failed to create index '{self.index_name}': {creation_error}\")\n raise\n\n # Ensure the dynamic field exists in the index\n self._ensure_embedding_field_mapping(\n client=client,\n index_name=self.index_name,\n field_name=dynamic_field_name,\n dim=dim,\n engine=engine,\n space_type=space_type,\n ef_construction=ef_construction,\n m=m,\n )\n\n self.log(f\"Indexing {len(texts)} documents into '{self.index_name}' with model '{embedding_model}'...\")\n logger.info(f\"Will store embeddings in field: {dynamic_field_name}\")\n logger.info(f\"Will tag documents with embedding_model: {embedding_model}\")\n\n # Use the bulk ingestion with model tracking\n return_ids = self._bulk_ingest_embeddings(\n client=client,\n index_name=self.index_name,\n embeddings=vectors,\n texts=texts,\n metadatas=metadatas,\n vector_field=dynamic_field_name, # Use dynamic field name\n text_field=\"text\",\n embedding_model=embedding_model, # Track the model\n mapping=mapping,\n is_aoss=is_aoss,\n )\n self.log(metadatas)\n\n logger.info(\n f\"Ingestion complete: Successfully indexed {len(return_ids)} documents with model '{embedding_model}'\"\n )\n self.log(f\"Successfully indexed {len(return_ids)} documents with model {embedding_model}.\")\n\n # ---------- helpers for filters ----------\n def _is_placeholder_term(self, term_obj: dict) -> bool:\n # term_obj like {\"filename\": \"__IMPOSSIBLE_VALUE__\"}\n return any(v == \"__IMPOSSIBLE_VALUE__\" for v in term_obj.values())\n\n def _coerce_filter_clauses(self, filter_obj: dict | None) -> list[dict]:\n \"\"\"Convert filter expressions into OpenSearch-compatible filter clauses.\n\n This method accepts two filter formats and converts them to standardized\n OpenSearch query clauses:\n\n Format A - Explicit filters:\n {\"filter\": [{\"term\": {\"field\": \"value\"}}, {\"terms\": {\"field\": [\"val1\", \"val2\"]}}],\n \"limit\": 10, \"score_threshold\": 1.5}\n\n Format B - Context-style mapping:\n {\"data_sources\": [\"file1.pdf\"], \"document_types\": [\"pdf\"], \"owners\": [\"user1\"]}\n\n Args:\n filter_obj: Filter configuration dictionary or None\n\n Returns:\n List of OpenSearch filter clauses (term/terms objects)\n Placeholder values with \"__IMPOSSIBLE_VALUE__\" are ignored\n \"\"\"\n if not filter_obj:\n return []\n\n # If it is a string, try to parse it once\n if isinstance(filter_obj, str):\n try:\n filter_obj = json.loads(filter_obj)\n except json.JSONDecodeError:\n # Not valid JSON - treat as no filters\n return []\n\n # Case A: already an explicit list/dict under \"filter\"\n if \"filter\" in filter_obj:\n raw = filter_obj[\"filter\"]\n if isinstance(raw, dict):\n raw = [raw]\n explicit_clauses: list[dict] = []\n for f in raw or []:\n if \"term\" in f and isinstance(f[\"term\"], dict) and not self._is_placeholder_term(f[\"term\"]):\n explicit_clauses.append(f)\n elif \"terms\" in f and isinstance(f[\"terms\"], dict):\n field, vals = next(iter(f[\"terms\"].items()))\n if isinstance(vals, list) and len(vals) > 0:\n explicit_clauses.append(f)\n return explicit_clauses\n\n # Case B: convert context-style maps into clauses\n field_mapping = {\n \"data_sources\": \"filename\",\n \"document_types\": \"mimetype\",\n \"owners\": \"owner\",\n }\n context_clauses: list[dict] = []\n for k, values in filter_obj.items():\n if not isinstance(values, list):\n continue\n field = field_mapping.get(k, k)\n if len(values) == 0:\n # Match-nothing placeholder (kept to mirror your tool semantics)\n context_clauses.append({\"term\": {field: \"__IMPOSSIBLE_VALUE__\"}})\n elif len(values) == 1:\n if values[0] != \"__IMPOSSIBLE_VALUE__\":\n context_clauses.append({\"term\": {field: values[0]}})\n else:\n context_clauses.append({\"terms\": {field: values}})\n return context_clauses\n\n def _detect_available_models(self, client: OpenSearch, filter_clauses: list[dict] | None = None) -> list[str]:\n \"\"\"Detect which embedding models have documents in the index.\n\n Uses aggregation to find all unique embedding_model values, optionally\n filtered to only documents matching the user's filter criteria.\n\n Args:\n client: OpenSearch client instance\n filter_clauses: Optional filter clauses to scope model detection\n\n Returns:\n List of embedding model names found in the index\n \"\"\"\n try:\n agg_query = {\"size\": 0, \"aggs\": {\"embedding_models\": {\"terms\": {\"field\": \"embedding_model\", \"size\": 10}}}}\n\n # Apply filters to model detection if any exist\n if filter_clauses:\n agg_query[\"query\"] = {\"bool\": {\"filter\": filter_clauses}}\n\n logger.debug(f\"Model detection query: {agg_query}\")\n result = client.search(\n index=self.index_name,\n body=agg_query,\n params={\"terminate_after\": 0},\n )\n buckets = result.get(\"aggregations\", {}).get(\"embedding_models\", {}).get(\"buckets\", [])\n models = [b[\"key\"] for b in buckets if b[\"key\"]]\n\n # Log detailed bucket info for debugging\n logger.info(\n f\"Detected embedding models in corpus: {models}\"\n + (f\" (with {len(filter_clauses)} filters)\" if filter_clauses else \"\")\n )\n if not models:\n total_hits = result.get(\"hits\", {}).get(\"total\", {})\n total_count = total_hits.get(\"value\", 0) if isinstance(total_hits, dict) else total_hits\n logger.warning(\n f\"No embedding_model values found in index '{self.index_name}'. \"\n f\"Total docs in index: {total_count}. \"\n f\"This may indicate documents were indexed without the embedding_model field.\"\n )\n except (OpenSearchException, KeyError, ValueError) as e:\n logger.warning(f\"Failed to detect embedding models: {e}\")\n # Fallback to current model\n fallback_model = self._get_embedding_model_name()\n logger.info(f\"Using fallback model: {fallback_model}\")\n return [fallback_model]\n else:\n return models\n\n def _get_index_properties(self, client: OpenSearch) -> dict[str, Any] | None:\n \"\"\"Retrieve flattened mapping properties for the current index.\"\"\"\n try:\n mapping = client.indices.get_mapping(index=self.index_name)\n except OpenSearchException as e:\n logger.warning(\n f\"Failed to fetch mapping for index '{self.index_name}': {e}. Proceeding without mapping metadata.\"\n )\n return None\n\n properties: dict[str, Any] = {}\n for index_data in mapping.values():\n props = index_data.get(\"mappings\", {}).get(\"properties\", {})\n if isinstance(props, dict):\n properties.update(props)\n return properties\n\n def _is_knn_vector_field(self, properties: dict[str, Any] | None, field_name: str) -> bool:\n \"\"\"Check whether the field is mapped as a knn_vector.\"\"\"\n if not field_name:\n return False\n if properties is None:\n logger.warning(f\"Mapping metadata unavailable; assuming field '{field_name}' is usable.\")\n return True\n field_def = properties.get(field_name)\n if not isinstance(field_def, dict):\n return False\n if field_def.get(\"type\") == \"knn_vector\":\n return True\n\n nested_props = field_def.get(\"properties\")\n return bool(isinstance(nested_props, dict) and nested_props.get(\"type\") == \"knn_vector\")\n\n def _get_field_dimension(self, properties: dict[str, Any] | None, field_name: str) -> int | None:\n \"\"\"Get the dimension of a knn_vector field from the index mapping.\n\n Args:\n properties: Index properties from mapping\n field_name: Name of the vector field\n\n Returns:\n Dimension of the field, or None if not found\n \"\"\"\n if not field_name or properties is None:\n return None\n\n field_def = properties.get(field_name)\n if not isinstance(field_def, dict):\n return None\n\n # Check direct knn_vector field\n if field_def.get(\"type\") == \"knn_vector\":\n return field_def.get(\"dimension\")\n\n # Check nested properties\n nested_props = field_def.get(\"properties\")\n if isinstance(nested_props, dict) and nested_props.get(\"type\") == \"knn_vector\":\n return nested_props.get(\"dimension\")\n\n return None\n\n def _get_filename_agg_field(self, index_properties: dict[str, Any] | None) -> str:\n \"\"\"Choose the appropriate field for filename aggregations.\"\"\"\n if not index_properties:\n return \"filename.keyword\"\n\n filename_def = index_properties.get(\"filename\")\n if not isinstance(filename_def, dict):\n return \"filename.keyword\"\n\n field_type = filename_def.get(\"type\")\n fields_def = filename_def.get(\"fields\", {})\n\n # Top-level keyword with no subfields\n if field_type == \"keyword\" and not isinstance(fields_def, dict):\n return \"filename\"\n\n # Text field with keyword subfield\n if isinstance(fields_def, dict) and \"keyword\" in fields_def:\n return \"filename.keyword\"\n\n # Fallback: aggregate on filename directly\n return \"filename\"\n\n # ---------- search (multi-model hybrid) ----------\n def search(self, query: str | None = None) -> list[dict[str, Any]]:\n \"\"\"Perform multi-model hybrid search combining multiple vector similarities and keyword matching.\n\n This method executes a sophisticated search that:\n 1. Auto-detects all embedding models present in the index\n 2. Generates query embeddings for ALL detected models in parallel\n 3. Combines multiple KNN queries using dis_max (picks best match)\n 4. Adds keyword search with fuzzy matching (30% weight)\n 5. Applies optional filtering and score thresholds\n 6. Returns aggregations for faceted search\n\n Search weights:\n - Semantic search (dis_max across all models): 70%\n - Keyword search: 30%\n\n Args:\n query: Search query string (used for both vector embedding and keyword search)\n\n Returns:\n List of search results with page_content, metadata, and relevance scores\n\n Raises:\n ValueError: If embedding component is not provided or filter JSON is invalid\n \"\"\"\n logger.info(self.ingest_data)\n client = self.build_client()\n q = (query or \"\").strip()\n\n # Parse optional filter expression\n filter_obj = None\n if getattr(self, \"filter_expression\", \"\") and self.filter_expression.strip():\n try:\n filter_obj = json.loads(self.filter_expression)\n except json.JSONDecodeError as e:\n msg = f\"Invalid filter_expression JSON: {e}\"\n raise ValueError(msg) from e\n\n if not self.embedding:\n msg = \"Embedding is required to run hybrid search (KNN + keyword).\"\n raise ValueError(msg)\n\n # Check if embedding is None (fail-safe mode)\n if self.embedding is None or (isinstance(self.embedding, list) and all(e is None for e in self.embedding)):\n logger.error(\"Embedding returned None (fail-safe mode enabled). Cannot perform search.\")\n return []\n\n # Build filter clauses first so we can use them in model detection\n filter_clauses = self._coerce_filter_clauses(filter_obj)\n\n # Detect available embedding models in the index (scoped by filters)\n available_models = self._detect_available_models(client, filter_clauses)\n\n if not available_models:\n logger.warning(\"No embedding models found in index, using current model\")\n available_models = [self._get_embedding_model_name()]\n\n # Generate embeddings for ALL detected models\n query_embeddings = {}\n\n # Normalize embedding to list\n embeddings_list = self.embedding if isinstance(self.embedding, list) else [self.embedding]\n # Filter out None values (fail-safe mode)\n embeddings_list = [e for e in embeddings_list if e is not None]\n\n if not embeddings_list:\n logger.error(\n \"No valid embeddings available after filtering None values (fail-safe mode). Cannot perform search.\"\n )\n return []\n\n # Create a comprehensive map of model names to embedding objects\n # Check all possible identifiers (deployment, model, model_id, model_name)\n # Also leverage available_models list from EmbeddingsWithModels\n # Handle duplicate identifiers by creating combined keys\n embedding_by_model = {}\n identifier_conflicts = {} # Track which identifiers have conflicts\n\n for idx, emb_obj in enumerate(embeddings_list):\n # Get all possible identifiers for this embedding\n identifiers = []\n deployment = getattr(emb_obj, \"deployment\", None)\n model = getattr(emb_obj, \"model\", None)\n model_id = getattr(emb_obj, \"model_id\", None)\n model_name = getattr(emb_obj, \"model_name\", None)\n dimensions = getattr(emb_obj, \"dimensions\", None)\n available_models_attr = getattr(emb_obj, \"available_models\", None)\n\n logger.info(\n f\"Embedding object {idx}: deployment={deployment}, model={model}, \"\n f\"model_id={model_id}, model_name={model_name}, dimensions={dimensions}, \"\n f\"available_models={available_models_attr}\"\n )\n\n # If this embedding has available_models dict, map all models to their dedicated instances\n if available_models_attr and isinstance(available_models_attr, dict):\n logger.info(\n f\"Embedding object {idx} provides {len(available_models_attr)} models via available_models dict\"\n )\n for model_name_key, dedicated_embedding in available_models_attr.items():\n if model_name_key and str(model_name_key).strip():\n model_str = str(model_name_key).strip()\n if model_str not in embedding_by_model:\n # Use the dedicated embedding instance from the dict\n embedding_by_model[model_str] = dedicated_embedding\n logger.info(f\"Mapped available model '{model_str}' to dedicated embedding instance\")\n else:\n # Conflict detected - track it\n if model_str not in identifier_conflicts:\n identifier_conflicts[model_str] = [embedding_by_model[model_str]]\n identifier_conflicts[model_str].append(dedicated_embedding)\n logger.warning(f\"Available model '{model_str}' has conflict - used by multiple embeddings\")\n\n # Also map traditional identifiers (for backward compatibility)\n if deployment:\n identifiers.append(str(deployment))\n if model:\n identifiers.append(str(model))\n if model_id:\n identifiers.append(str(model_id))\n if model_name:\n identifiers.append(str(model_name))\n\n # Map all identifiers to this embedding object\n for identifier in identifiers:\n if identifier not in embedding_by_model:\n embedding_by_model[identifier] = emb_obj\n logger.info(f\"Mapped identifier '{identifier}' to embedding object {idx}\")\n else:\n # Conflict detected - track it\n if identifier not in identifier_conflicts:\n identifier_conflicts[identifier] = [embedding_by_model[identifier]]\n identifier_conflicts[identifier].append(emb_obj)\n logger.warning(f\"Identifier '{identifier}' has conflict - used by multiple embeddings\")\n\n # For embeddings with model+deployment, create combined identifier\n # This helps when deployment is the same but model differs\n if deployment and model and deployment != model:\n combined_id = f\"{deployment}:{model}\"\n if combined_id not in embedding_by_model:\n embedding_by_model[combined_id] = emb_obj\n logger.info(f\"Created combined identifier '{combined_id}' for embedding object {idx}\")\n\n # Log conflicts\n if identifier_conflicts:\n logger.warning(\n f\"Found {len(identifier_conflicts)} conflicting identifiers. \"\n f\"Consider using combined format 'deployment:model' or specifying unique model names.\"\n )\n for conflict_id, emb_list in identifier_conflicts.items():\n logger.warning(f\" Conflict on '{conflict_id}': {len(emb_list)} embeddings use this identifier\")\n\n logger.info(f\"Generating embeddings for {len(available_models)} models in index\")\n logger.info(f\"Available embedding identifiers: {list(embedding_by_model.keys())}\")\n self.log(f\"[SEARCH] Models detected in index: {available_models}\")\n self.log(f\"[SEARCH] Available embedding identifiers: {list(embedding_by_model.keys())}\")\n\n # Track matching status for debugging\n matched_models = []\n unmatched_models = []\n\n for model_name in available_models:\n try:\n # Check if we have an embedding object for this model\n if model_name in embedding_by_model:\n # Use the matching embedding object directly\n emb_obj = embedding_by_model[model_name]\n emb_deployment = getattr(emb_obj, \"deployment\", None)\n emb_model = getattr(emb_obj, \"model\", None)\n emb_model_id = getattr(emb_obj, \"model_id\", None)\n emb_dimensions = getattr(emb_obj, \"dimensions\", None)\n emb_available_models = getattr(emb_obj, \"available_models\", None)\n\n logger.info(\n f\"Using embedding object for model '{model_name}': \"\n f\"deployment={emb_deployment}, model={emb_model}, model_id={emb_model_id}, \"\n f\"dimensions={emb_dimensions}\"\n )\n\n # Check if this is a dedicated instance from available_models dict\n if emb_available_models and isinstance(emb_available_models, dict):\n logger.info(\n f\"Model '{model_name}' using dedicated instance from available_models dict \"\n f\"(pre-configured with correct model and dimensions)\"\n )\n\n # Use the embedding instance directly - no model switching needed!\n vec = emb_obj.embed_query(q)\n query_embeddings[model_name] = vec\n matched_models.append(model_name)\n logger.info(f\"Generated embedding for model: {model_name} (actual dimensions: {len(vec)})\")\n self.log(f\"[MATCH] Model '{model_name}' - generated {len(vec)}-dim embedding\")\n else:\n # No matching embedding found for this model\n unmatched_models.append(model_name)\n logger.warning(\n f\"No matching embedding found for model '{model_name}'. \"\n f\"This model will be skipped. Available identifiers: {list(embedding_by_model.keys())}\"\n )\n self.log(f\"[NO MATCH] Model '{model_name}' - available: {list(embedding_by_model.keys())}\")\n except (RuntimeError, ValueError, ConnectionError, TimeoutError, AttributeError, KeyError) as e:\n logger.warning(f\"Failed to generate embedding for {model_name}: {e}\")\n self.log(f\"[ERROR] Embedding generation failed for '{model_name}': {e}\")\n\n # Log summary of model matching\n logger.info(f\"Model matching summary: {len(matched_models)} matched, {len(unmatched_models)} unmatched\")\n self.log(f\"[SUMMARY] Model matching: {len(matched_models)} matched, {len(unmatched_models)} unmatched\")\n if unmatched_models:\n self.log(f\"[WARN] Unmatched models in index: {unmatched_models}\")\n\n if not query_embeddings:\n msg = (\n f\"Failed to generate embeddings for any model. \"\n f\"Index has models: {available_models}, but no matching embedding objects found. \"\n f\"Available embedding identifiers: {list(embedding_by_model.keys())}\"\n )\n self.log(f\"[FAIL] Search failed: {msg}\")\n raise ValueError(msg)\n\n index_properties = self._get_index_properties(client)\n legacy_vector_field = getattr(self, \"vector_field\", \"chunk_embedding\")\n\n # Build KNN queries for each model\n embedding_fields: list[str] = []\n knn_queries_with_candidates = []\n knn_queries_without_candidates = []\n\n raw_num_candidates = getattr(self, \"num_candidates\", 1000)\n try:\n num_candidates = int(raw_num_candidates) if raw_num_candidates is not None else 0\n except (TypeError, ValueError):\n num_candidates = 0\n use_num_candidates = num_candidates > 0\n\n for model_name, embedding_vector in query_embeddings.items():\n field_name = get_embedding_field_name(model_name)\n selected_field = field_name\n vector_dim = len(embedding_vector)\n\n # Only use the expected dynamic field - no legacy fallback\n # This prevents dimension mismatches between models\n if not self._is_knn_vector_field(index_properties, selected_field):\n logger.warning(\n f\"Skipping model {model_name}: field '{field_name}' is not mapped as knn_vector. \"\n f\"Documents must be indexed with this embedding model before querying.\"\n )\n self.log(f\"[SKIP] Field '{selected_field}' not a knn_vector - skipping model '{model_name}'\")\n continue\n\n # Validate vector dimensions match the field dimensions\n field_dim = self._get_field_dimension(index_properties, selected_field)\n if field_dim is not None and field_dim != vector_dim:\n logger.error(\n f\"Dimension mismatch for model '{model_name}': \"\n f\"Query vector has {vector_dim} dimensions but field '{selected_field}' expects {field_dim}. \"\n f\"Skipping this model to prevent search errors.\"\n )\n self.log(f\"[DIM MISMATCH] Model '{model_name}': query={vector_dim} vs field={field_dim} - skipping\")\n continue\n\n logger.info(\n f\"Adding KNN query for model '{model_name}': field='{selected_field}', \"\n f\"query_dims={vector_dim}, field_dims={field_dim or 'unknown'}\"\n )\n embedding_fields.append(selected_field)\n\n base_query = {\n \"knn\": {\n selected_field: {\n \"vector\": embedding_vector,\n \"k\": 50,\n }\n }\n }\n\n if use_num_candidates:\n query_with_candidates = copy.deepcopy(base_query)\n query_with_candidates[\"knn\"][selected_field][\"num_candidates\"] = num_candidates\n else:\n query_with_candidates = base_query\n\n knn_queries_with_candidates.append(query_with_candidates)\n knn_queries_without_candidates.append(base_query)\n\n if not knn_queries_with_candidates:\n # No valid fields found - this can happen when:\n # 1. Index is empty (no documents yet)\n # 2. Embedding model has changed and field doesn't exist yet\n # Return empty results instead of failing\n logger.warning(\n \"No valid knn_vector fields found for embedding models. \"\n \"This may indicate an empty index or missing field mappings. \"\n \"Returning empty search results.\"\n )\n self.log(\n f\"[WARN] No valid KNN queries could be built. \"\n f\"Query embeddings generated: {list(query_embeddings.keys())}, \"\n f\"but no matching knn_vector fields found in index.\"\n )\n return []\n\n # Build exists filter - document must have at least one embedding field\n exists_any_embedding = {\n \"bool\": {\"should\": [{\"exists\": {\"field\": f}} for f in set(embedding_fields)], \"minimum_should_match\": 1}\n }\n\n # Combine user filters with exists filter\n all_filters = [*filter_clauses, exists_any_embedding]\n\n # Get limit and score threshold\n limit = (filter_obj or {}).get(\"limit\", self.number_of_results)\n score_threshold = (filter_obj or {}).get(\"score_threshold\", 0)\n\n # Determine the best aggregation field for filename based on index mapping\n filename_agg_field = self._get_filename_agg_field(index_properties)\n\n # Build multi-model hybrid query\n body = {\n \"query\": {\n \"bool\": {\n \"should\": [\n {\n \"dis_max\": {\n \"tie_breaker\": 0.0, # Take only the best match, no blending\n \"boost\": 0.7, # 70% weight for semantic search\n \"queries\": knn_queries_with_candidates,\n }\n },\n {\n \"multi_match\": {\n \"query\": q,\n \"fields\": [\"text^2\", \"filename^1.5\"],\n \"type\": \"best_fields\",\n \"fuzziness\": \"AUTO\",\n \"boost\": 0.3, # 30% weight for keyword search\n }\n },\n ],\n \"minimum_should_match\": 1,\n \"filter\": all_filters,\n }\n },\n \"aggs\": {\n \"data_sources\": {\"terms\": {\"field\": filename_agg_field, \"size\": 20}},\n \"document_types\": {\"terms\": {\"field\": \"mimetype\", \"size\": 10}},\n \"owners\": {\"terms\": {\"field\": \"owner\", \"size\": 10}},\n \"embedding_models\": {\"terms\": {\"field\": \"embedding_model\", \"size\": 10}},\n },\n \"_source\": [\n \"filename\",\n \"mimetype\",\n \"page\",\n \"text\",\n \"source_url\",\n \"owner\",\n \"embedding_model\",\n \"allowed_users\",\n \"allowed_groups\",\n ],\n \"size\": limit,\n }\n\n if isinstance(score_threshold, (int, float)) and score_threshold > 0:\n body[\"min_score\"] = score_threshold\n\n logger.info(\n f\"Executing multi-model hybrid search with {len(knn_queries_with_candidates)} embedding models: \"\n f\"{list(query_embeddings.keys())}\"\n )\n self.log(f\"[EXEC] Executing search with {len(knn_queries_with_candidates)} KNN queries, limit={limit}\")\n self.log(f\"[EXEC] Embedding models used: {list(query_embeddings.keys())}\")\n self.log(f\"[EXEC] KNN fields being queried: {embedding_fields}\")\n\n try:\n resp = client.search(index=self.index_name, body=body, params={\"terminate_after\": 0})\n except RequestError as e:\n error_message = str(e)\n lowered = error_message.lower()\n if use_num_candidates and \"num_candidates\" in lowered:\n logger.warning(\n \"Retrying search without num_candidates parameter due to cluster capabilities\",\n error=error_message,\n )\n fallback_body = copy.deepcopy(body)\n try:\n fallback_body[\"query\"][\"bool\"][\"should\"][0][\"dis_max\"][\"queries\"] = knn_queries_without_candidates\n except (KeyError, IndexError, TypeError) as inner_err:\n raise e from inner_err\n resp = client.search(\n index=self.index_name,\n body=fallback_body,\n params={\"terminate_after\": 0},\n )\n elif \"knn_vector\" in lowered or (\"field\" in lowered and \"knn\" in lowered):\n fallback_vector = next(iter(query_embeddings.values()), None)\n if fallback_vector is None:\n raise\n fallback_field = legacy_vector_field or \"chunk_embedding\"\n logger.warning(\n \"KNN search failed for dynamic fields; falling back to legacy field '%s'.\",\n fallback_field,\n )\n fallback_body = copy.deepcopy(body)\n fallback_body[\"query\"][\"bool\"][\"filter\"] = filter_clauses\n knn_fallback = {\n \"knn\": {\n fallback_field: {\n \"vector\": fallback_vector,\n \"k\": 50,\n }\n }\n }\n if use_num_candidates:\n knn_fallback[\"knn\"][fallback_field][\"num_candidates\"] = num_candidates\n fallback_body[\"query\"][\"bool\"][\"should\"][0][\"dis_max\"][\"queries\"] = [knn_fallback]\n resp = client.search(\n index=self.index_name,\n body=fallback_body,\n params={\"terminate_after\": 0},\n )\n else:\n raise\n hits = resp.get(\"hits\", {}).get(\"hits\", [])\n\n logger.info(f\"Found {len(hits)} results\")\n self.log(f\"[RESULT] Search complete: {len(hits)} results found\")\n\n if len(hits) == 0:\n self.log(\n f\"[EMPTY] Debug info: \"\n f\"models_in_index={available_models}, \"\n f\"matched_models={matched_models}, \"\n f\"knn_fields={embedding_fields}, \"\n f\"filters={len(filter_clauses)} clauses\"\n )\n\n return [\n {\n \"page_content\": hit[\"_source\"].get(\"text\", \"\"),\n \"metadata\": {k: v for k, v in hit[\"_source\"].items() if k != \"text\"},\n \"score\": hit.get(\"_score\"),\n }\n for hit in hits\n ]\n\n def search_documents(self) -> list[Data]:\n \"\"\"Search documents and return results as Data objects.\n\n This is the main interface method that performs the multi-model search using the\n configured search_query and returns results in Langflow's Data format.\n\n Always builds the vector store (triggering ingestion if needed), then performs\n search only if a query is provided.\n\n Returns:\n List of Data objects containing search results with text and metadata\n\n Raises:\n Exception: If search operation fails\n \"\"\"\n try:\n # Always build/cache the vector store to ensure ingestion happens\n logger.info(f\"Search query: {self.search_query}\")\n if self._cached_vector_store is None:\n self.build_vector_store()\n\n # Only perform search if query is provided\n search_query = (self.search_query or \"\").strip()\n if not search_query:\n self.log(\"No search query provided - ingestion completed, returning empty results\")\n return []\n\n # Perform search with the provided query\n raw = self.search(search_query)\n return [Data(text=hit[\"page_content\"], **hit[\"metadata\"]) for hit in raw]\n except Exception as e:\n self.log(f\"search_documents error: {e}\")\n raise\n\n # -------- dynamic UI handling (auth switch) --------\n async def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n \"\"\"Dynamically update component configuration based on field changes.\n\n This method handles real-time UI updates, particularly for authentication\n mode changes that show/hide relevant input fields.\n\n Args:\n build_config: Current component configuration\n field_value: New value for the changed field\n field_name: Name of the field that changed\n\n Returns:\n Updated build configuration with appropriate field visibility\n \"\"\"\n try:\n if field_name == \"auth_mode\":\n mode = (field_value or \"basic\").strip().lower()\n is_basic = mode == \"basic\"\n is_jwt = mode == \"jwt\"\n\n build_config[\"username\"][\"show\"] = is_basic\n build_config[\"password\"][\"show\"] = is_basic\n\n build_config[\"jwt_token\"][\"show\"] = is_jwt\n build_config[\"jwt_header\"][\"show\"] = is_jwt\n build_config[\"bearer_prefix\"][\"show\"] = is_jwt\n\n build_config[\"username\"][\"required\"] = is_basic\n build_config[\"password\"][\"required\"] = is_basic\n\n build_config[\"jwt_token\"][\"required\"] = is_jwt\n build_config[\"jwt_header\"][\"required\"] = is_jwt\n build_config[\"bearer_prefix\"][\"required\"] = False\n\n if is_basic:\n build_config[\"jwt_token\"][\"value\"] = \"\"\n\n return build_config\n\n except (KeyError, ValueError) as e:\n self.log(f\"update_build_config error: {e}\")\n\n return build_config\n" }, "docs_metadata": { "_input_type": "TableInput", @@ -67889,7 +67932,8 @@ "dynamic": false, "info": "Additional metadata key-value pairs to be added to all ingested documents. Useful for tagging documents with source information, categories, or other custom attributes.", "input_types": [ - "Data" + "Data", + "JSON" ], "is_list": true, "list_add_label": "Add More", @@ -68436,7 +68480,7 @@ { "EmbeddingSimilarityComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -68479,10 +68523,10 @@ "group_outputs": false, "method": "compute_similarity", "name": "similarity_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -68512,13 +68556,14 @@ "value": "from typing import Any\n\nimport numpy as np\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, DropdownInput, Output\nfrom lfx.schema.data import Data\n\n\nclass EmbeddingSimilarityComponent(Component):\n display_name: str = \"Embedding Similarity\"\n description: str = \"Compute selected form of similarity between two embedding vectors.\"\n icon = \"equal\"\n legacy: bool = True\n replacement = [\"datastax.AstraDB\"]\n\n inputs = [\n DataInput(\n name=\"embedding_vectors\",\n display_name=\"Embedding Vectors\",\n info=\"A list containing exactly two data objects with embedding vectors to compare.\",\n is_list=True,\n required=True,\n ),\n DropdownInput(\n name=\"similarity_metric\",\n display_name=\"Similarity Metric\",\n info=\"Select the similarity metric to use.\",\n options=[\"Cosine Similarity\", \"Euclidean Distance\", \"Manhattan Distance\"],\n value=\"Cosine Similarity\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Similarity Data\", name=\"similarity_data\", method=\"compute_similarity\"),\n ]\n\n def compute_similarity(self) -> Data:\n embedding_vectors: list[Data] = self.embedding_vectors\n\n # Assert that the list contains exactly two Data objects\n if len(embedding_vectors) != 2: # noqa: PLR2004\n msg = \"Exactly two embedding vectors are required.\"\n raise ValueError(msg)\n\n embedding_1 = np.array(embedding_vectors[0].data[\"embeddings\"])\n embedding_2 = np.array(embedding_vectors[1].data[\"embeddings\"])\n\n if embedding_1.shape != embedding_2.shape:\n similarity_score: dict[str, Any] = {\"error\": \"Embeddings must have the same dimensions.\"}\n else:\n similarity_metric = self.similarity_metric\n\n if similarity_metric == \"Cosine Similarity\":\n score = np.dot(embedding_1, embedding_2) / (np.linalg.norm(embedding_1) * np.linalg.norm(embedding_2))\n similarity_score = {\"cosine_similarity\": score}\n\n elif similarity_metric == \"Euclidean Distance\":\n score = np.linalg.norm(embedding_1 - embedding_2)\n similarity_score = {\"euclidean_distance\": score}\n\n elif similarity_metric == \"Manhattan Distance\":\n score = np.sum(np.abs(embedding_1 - embedding_2))\n similarity_score = {\"manhattan_distance\": score}\n\n # Create a Data object to encapsulate the similarity score and additional information\n similarity_data = Data(\n data={\n \"embedding_1\": embedding_vectors[0].data[\"embeddings\"],\n \"embedding_2\": embedding_vectors[1].data[\"embeddings\"],\n \"similarity_score\": similarity_score,\n },\n text_key=\"similarity_score\",\n )\n\n self.status = similarity_data\n return similarity_data\n" }, "embedding_vectors": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Embedding Vectors", "dynamic": false, "info": "A list containing exactly two data objects with embedding vectors to compare.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -68568,7 +68613,7 @@ }, "TextEmbedderComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -68607,10 +68652,10 @@ "group_outputs": false, "method": "generate_embeddings", "name": "embeddings", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -68863,7 +68908,7 @@ { "Directory": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -68908,10 +68953,10 @@ "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -69430,6 +69475,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, @@ -69838,7 +69884,7 @@ }, "KnowledgeBase": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -69925,10 +69971,10 @@ "group_outputs": false, "method": "retrieve_data", "name": "retrieve_data", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -70118,7 +70164,7 @@ "icon": "file-text", "legacy": false, "metadata": { - "code_hash": "6d0e4842271e", + "code_hash": "f8b6df3c93c0", "dependencies": { "dependencies": [ { @@ -70329,7 +70375,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nfrom collections.abc import AsyncIterator, Iterator\nfrom pathlib import Path\nfrom typing import Any\n\nimport orjson\nimport pandas as pd\nfrom fastapi import UploadFile\nfrom fastapi.encoders import jsonable_encoder\n\nfrom lfx.custom import Component\nfrom lfx.inputs import SortableListInput\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, SecretStrInput, StrInput\nfrom lfx.schema import Data, DataFrame, Message\nfrom lfx.services.deps import get_settings_service, get_storage_service, session_scope\nfrom lfx.template.field.base import Output\nfrom lfx.utils.validate_cloud import is_astra_cloud_environment\n\n\ndef _get_storage_location_options():\n \"\"\"Get storage location options, filtering out Local if in Astra cloud environment.\"\"\"\n all_options = [{\"name\": \"AWS\", \"icon\": \"Amazon\"}, {\"name\": \"Google Drive\", \"icon\": \"google\"}]\n if is_astra_cloud_environment():\n return all_options\n return [{\"name\": \"Local\", \"icon\": \"hard-drive\"}, *all_options]\n\n\nclass SaveToFileComponent(Component):\n display_name = \"Write File\"\n description = \"Save data to local file, AWS S3, or Google Drive in the selected format.\"\n documentation: str = \"https://docs.langflow.org/write-file\"\n icon = \"file-text\"\n name = \"SaveToFile\"\n\n # File format options for different storage types\n LOCAL_DATA_FORMAT_CHOICES = [\"csv\", \"excel\", \"json\", \"markdown\"]\n LOCAL_MESSAGE_FORMAT_CHOICES = [\"txt\", \"json\", \"markdown\"]\n AWS_FORMAT_CHOICES = [\n \"txt\",\n \"json\",\n \"csv\",\n \"xml\",\n \"html\",\n \"md\",\n \"yaml\",\n \"log\",\n \"tsv\",\n \"jsonl\",\n \"parquet\",\n \"xlsx\",\n \"zip\",\n ]\n GDRIVE_FORMAT_CHOICES = [\"txt\", \"json\", \"csv\", \"xlsx\", \"slides\", \"docs\", \"jpg\", \"mp3\"]\n\n inputs = [\n SortableListInput(\n name=\"storage_location\",\n display_name=\"Storage Location\",\n placeholder=\"Select Location\",\n info=\"Choose where to save the file.\",\n options=_get_storage_location_options(),\n real_time_refresh=True,\n limit=1,\n value=[{\"name\": \"Local\", \"icon\": \"hard-drive\"}],\n advanced=True,\n ),\n # Common inputs\n HandleInput(\n name=\"input\",\n display_name=\"File Content\",\n info=\"The input to save.\",\n dynamic=True,\n input_types=[\"Data\", \"DataFrame\", \"Message\"],\n required=True,\n ),\n StrInput(\n name=\"file_name\",\n display_name=\"File Name\",\n info=\"Name file will be saved as (without extension).\",\n required=True,\n show=False,\n tool_mode=True,\n ),\n BoolInput(\n name=\"append_mode\",\n display_name=\"Append\",\n info=(\n \"Append to file if it exists (only for Local storage with plain text formats). \"\n \"Not supported for cloud storage (AWS/Google Drive).\"\n ),\n value=False,\n show=False,\n ),\n # Format inputs (dynamic based on storage location)\n DropdownInput(\n name=\"local_format\",\n display_name=\"File Format\",\n options=list(dict.fromkeys(LOCAL_DATA_FORMAT_CHOICES + LOCAL_MESSAGE_FORMAT_CHOICES)),\n info=\"Select the file format for local storage.\",\n value=\"json\",\n show=False,\n ),\n DropdownInput(\n name=\"aws_format\",\n display_name=\"File Format\",\n options=AWS_FORMAT_CHOICES,\n info=\"Select the file format for AWS S3 storage.\",\n value=\"txt\",\n show=False,\n ),\n DropdownInput(\n name=\"gdrive_format\",\n display_name=\"File Format\",\n options=GDRIVE_FORMAT_CHOICES,\n info=\"Select the file format for Google Drive storage.\",\n value=\"txt\",\n show=False,\n ),\n # AWS S3 specific inputs\n SecretStrInput(\n name=\"aws_access_key_id\",\n display_name=\"AWS Access Key ID\",\n info=\"AWS Access key ID.\",\n show=False,\n advanced=True,\n required=True,\n ),\n SecretStrInput(\n name=\"aws_secret_access_key\",\n display_name=\"AWS Secret Key\",\n info=\"AWS Secret Key.\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"bucket_name\",\n display_name=\"S3 Bucket Name\",\n info=\"Enter the name of the S3 bucket.\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"aws_region\",\n display_name=\"AWS Region\",\n info=\"AWS region (e.g., us-east-1, eu-west-1).\",\n show=False,\n advanced=True,\n ),\n StrInput(\n name=\"s3_prefix\",\n display_name=\"S3 Prefix\",\n info=\"Prefix for all files in S3.\",\n show=False,\n advanced=True,\n ),\n # Google Drive specific inputs\n SecretStrInput(\n name=\"service_account_key\",\n display_name=\"GCP Credentials Secret Key\",\n info=\"Your Google Cloud Platform service account JSON key as a secret string (complete JSON content).\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"folder_id\",\n display_name=\"Google Drive Folder ID\",\n info=(\n \"The Google Drive folder ID where the file will be uploaded. \"\n \"The folder must be shared with the service account email.\"\n ),\n required=True,\n show=False,\n advanced=True,\n ),\n ]\n\n outputs = [Output(display_name=\"File Path\", name=\"message\", method=\"save_to_file\")]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Update build configuration to show/hide fields based on storage location selection.\"\"\"\n # Update options dynamically based on cloud environment\n # This ensures options are refreshed when build_config is updated\n if \"storage_location\" in build_config:\n updated_options = _get_storage_location_options()\n build_config[\"storage_location\"][\"options\"] = updated_options\n\n if field_name != \"storage_location\":\n return build_config\n\n # Extract selected storage location\n selected = [location[\"name\"] for location in field_value] if isinstance(field_value, list) else []\n\n # Hide all dynamic fields first\n dynamic_fields = [\n \"file_name\", # Common fields (input is always visible)\n \"append_mode\",\n \"local_format\",\n \"aws_format\",\n \"gdrive_format\",\n \"aws_access_key_id\",\n \"aws_secret_access_key\",\n \"bucket_name\",\n \"aws_region\",\n \"s3_prefix\",\n \"service_account_key\",\n \"folder_id\",\n ]\n\n for f_name in dynamic_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = False\n\n # Show fields based on selected storage location\n if len(selected) == 1:\n location = selected[0]\n\n # Show file_name when any storage location is selected\n if \"file_name\" in build_config:\n build_config[\"file_name\"][\"show\"] = True\n\n # Show append_mode only for Local storage (not supported for cloud storage)\n if \"append_mode\" in build_config:\n build_config[\"append_mode\"][\"show\"] = location == \"Local\"\n\n if location == \"Local\":\n if \"local_format\" in build_config:\n build_config[\"local_format\"][\"show\"] = True\n\n elif location == \"AWS\":\n aws_fields = [\n \"aws_format\",\n \"aws_access_key_id\",\n \"aws_secret_access_key\",\n \"bucket_name\",\n \"aws_region\",\n \"s3_prefix\",\n ]\n for f_name in aws_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = True\n build_config[f_name][\"advanced\"] = False\n\n elif location == \"Google Drive\":\n gdrive_fields = [\"gdrive_format\", \"service_account_key\", \"folder_id\"]\n for f_name in gdrive_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = True\n build_config[f_name][\"advanced\"] = False\n\n return build_config\n\n async def save_to_file(self) -> Message:\n \"\"\"Save the input to a file and upload it, returning a confirmation message.\"\"\"\n # Validate inputs\n if not self.file_name:\n msg = \"File name must be provided.\"\n raise ValueError(msg)\n if not self._get_input_type():\n msg = \"Input type is not set.\"\n raise ValueError(msg)\n\n # Get selected storage location\n storage_location = self._get_selected_storage_location()\n if not storage_location:\n msg = \"Storage location must be selected.\"\n raise ValueError(msg)\n\n # Check if Local storage is disabled in cloud environment\n if storage_location == \"Local\" and is_astra_cloud_environment():\n msg = \"Local storage is not available in cloud environment. Please use AWS or Google Drive.\"\n raise ValueError(msg)\n\n # Route to appropriate save method based on storage location\n if storage_location == \"Local\":\n return await self._save_to_local()\n if storage_location == \"AWS\":\n return await self._save_to_aws()\n if storage_location == \"Google Drive\":\n return await self._save_to_google_drive()\n msg = f\"Unsupported storage location: {storage_location}\"\n raise ValueError(msg)\n\n def _get_input_type(self) -> str:\n \"\"\"Determine the input type based on the provided input.\"\"\"\n # Use exact type checking (type() is) instead of isinstance() to avoid inheritance issues.\n # Since Message inherits from Data, isinstance(message, Data) would return True for Message objects,\n # causing Message inputs to be incorrectly identified as Data type.\n if type(self.input) is DataFrame:\n return \"DataFrame\"\n if type(self.input) is Message:\n return \"Message\"\n if type(self.input) is Data:\n return \"Data\"\n msg = f\"Unsupported input type: {type(self.input)}\"\n raise ValueError(msg)\n\n def _get_default_format(self) -> str:\n \"\"\"Return the default file format based on input type.\"\"\"\n if self._get_input_type() == \"DataFrame\":\n return \"csv\"\n if self._get_input_type() == \"Data\":\n return \"json\"\n if self._get_input_type() == \"Message\":\n return \"json\"\n return \"json\" # Fallback\n\n def _adjust_file_path_with_format(self, path: Path, fmt: str) -> Path:\n \"\"\"Adjust the file path to include the correct extension.\"\"\"\n file_extension = path.suffix.lower().lstrip(\".\")\n if fmt == \"excel\":\n return Path(f\"{path}.xlsx\").expanduser() if file_extension not in [\"xlsx\", \"xls\"] else path\n return Path(f\"{path}.{fmt}\").expanduser() if file_extension != fmt else path\n\n def _is_plain_text_format(self, fmt: str) -> bool:\n \"\"\"Check if a file format is plain text (supports appending).\"\"\"\n plain_text_formats = [\"txt\", \"json\", \"markdown\", \"md\", \"csv\", \"xml\", \"html\", \"yaml\", \"log\", \"tsv\", \"jsonl\"]\n return fmt.lower() in plain_text_formats\n\n async def _upload_file(self, file_path: Path) -> None:\n \"\"\"Upload the saved file using the upload_user_file service.\"\"\"\n from langflow.api.v2.files import upload_user_file\n from langflow.services.database.models.user.crud import get_user_by_id\n\n # Ensure the file exists\n if not file_path.exists():\n msg = f\"File not found: {file_path}\"\n raise FileNotFoundError(msg)\n\n # Upload the file - always use append=False because the local file already contains\n # the correct content (either new or appended locally)\n with file_path.open(\"rb\") as f:\n async with session_scope() as db:\n if not self.user_id:\n msg = \"User ID is required for file saving.\"\n raise ValueError(msg)\n current_user = await get_user_by_id(db, self.user_id)\n\n await upload_user_file(\n file=UploadFile(filename=file_path.name, file=f, size=file_path.stat().st_size),\n session=db,\n current_user=current_user,\n storage_service=get_storage_service(),\n settings_service=get_settings_service(),\n append=False,\n )\n\n def _save_dataframe(self, dataframe: DataFrame, path: Path, fmt: str) -> str:\n \"\"\"Save a DataFrame to the specified file format.\"\"\"\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"csv\":\n dataframe.to_csv(path, index=False, mode=\"a\" if should_append else \"w\", header=not should_append)\n elif fmt == \"excel\":\n dataframe.to_excel(path, index=False, engine=\"openpyxl\")\n elif fmt == \"json\":\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new data\n new_records = json.loads(dataframe.to_json(orient=\"records\"))\n existing_data.extend(new_records)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n dataframe.to_json(path, orient=\"records\", indent=2)\n elif fmt == \"markdown\":\n content = dataframe.to_markdown(index=False)\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported DataFrame format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"DataFrame {action} '{path}'\"\n\n def _save_data(self, data: Data, path: Path, fmt: str) -> str:\n \"\"\"Save a Data object to the specified file format.\"\"\"\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"csv\":\n pd.DataFrame(data.data).to_csv(\n path,\n index=False,\n mode=\"a\" if should_append else \"w\",\n header=not should_append,\n )\n elif fmt == \"excel\":\n pd.DataFrame(data.data).to_excel(path, index=False, engine=\"openpyxl\")\n elif fmt == \"json\":\n new_data = jsonable_encoder(data.data)\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new data\n if isinstance(new_data, list):\n existing_data.extend(new_data)\n else:\n existing_data.append(new_data)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n content = orjson.dumps(new_data, option=orjson.OPT_INDENT_2).decode(\"utf-8\")\n path.write_text(content, encoding=\"utf-8\")\n elif fmt == \"markdown\":\n content = pd.DataFrame(data.data).to_markdown(index=False)\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported Data format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"Data {action} '{path}'\"\n\n async def _save_message(self, message: Message, path: Path, fmt: str) -> str:\n \"\"\"Save a Message to the specified file format, handling async iterators.\"\"\"\n content = \"\"\n if message.text is None:\n content = \"\"\n elif isinstance(message.text, AsyncIterator):\n async for item in message.text:\n content += str(item) + \" \"\n content = content.strip()\n elif isinstance(message.text, Iterator):\n content = \" \".join(str(item) for item in message.text)\n else:\n content = str(message.text)\n\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"txt\":\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n elif fmt == \"json\":\n new_message = {\"message\": content}\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new message\n existing_data.append(new_message)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n path.write_text(json.dumps(new_message, indent=2), encoding=\"utf-8\")\n elif fmt == \"markdown\":\n md_content = f\"**Message:**\\n\\n{content}\"\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + md_content, encoding=\"utf-8\")\n else:\n path.write_text(md_content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported Message format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"Message {action} '{path}'\"\n\n def _get_selected_storage_location(self) -> str:\n \"\"\"Get the selected storage location from the SortableListInput.\"\"\"\n if hasattr(self, \"storage_location\") and self.storage_location:\n if isinstance(self.storage_location, list) and len(self.storage_location) > 0:\n return self.storage_location[0].get(\"name\", \"\")\n if isinstance(self.storage_location, dict):\n return self.storage_location.get(\"name\", \"\")\n return \"\"\n\n def _get_file_format_for_location(self, location: str) -> str:\n \"\"\"Get the appropriate file format based on storage location.\"\"\"\n if location == \"Local\":\n return getattr(self, \"local_format\", None) or self._get_default_format()\n if location == \"AWS\":\n return getattr(self, \"aws_format\", \"txt\")\n if location == \"Google Drive\":\n return getattr(self, \"gdrive_format\", \"txt\")\n return self._get_default_format()\n\n async def _save_to_local(self) -> Message:\n \"\"\"Save file to local storage (original functionality).\"\"\"\n file_format = self._get_file_format_for_location(\"Local\")\n\n # Validate file format based on input type\n allowed_formats = (\n self.LOCAL_MESSAGE_FORMAT_CHOICES if self._get_input_type() == \"Message\" else self.LOCAL_DATA_FORMAT_CHOICES\n )\n if file_format not in allowed_formats:\n msg = f\"Invalid file format '{file_format}' for {self._get_input_type()}. Allowed: {allowed_formats}\"\n raise ValueError(msg)\n\n # Prepare file path\n file_path = Path(self.file_name).expanduser()\n if not file_path.parent.exists():\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path = self._adjust_file_path_with_format(file_path, file_format)\n\n # Save the input to file based on type\n if self._get_input_type() == \"DataFrame\":\n confirmation = self._save_dataframe(self.input, file_path, file_format)\n elif self._get_input_type() == \"Data\":\n confirmation = self._save_data(self.input, file_path, file_format)\n elif self._get_input_type() == \"Message\":\n confirmation = await self._save_message(self.input, file_path, file_format)\n else:\n msg = f\"Unsupported input type: {self._get_input_type()}\"\n raise ValueError(msg)\n\n # Upload the saved file\n await self._upload_file(file_path)\n\n # Return the final file path and confirmation message\n final_path = Path.cwd() / file_path if not file_path.is_absolute() else file_path\n return Message(text=f\"{confirmation} at {final_path}\")\n\n async def _save_to_aws(self) -> Message:\n \"\"\"Save file to AWS S3 using S3 functionality.\"\"\"\n import os\n\n import boto3\n\n from lfx.base.data.cloud_storage_utils import create_s3_client, validate_aws_credentials\n\n # Get AWS credentials from component inputs or fall back to environment variables\n aws_access_key_id = getattr(self, \"aws_access_key_id\", None)\n if aws_access_key_id and hasattr(aws_access_key_id, \"get_secret_value\"):\n aws_access_key_id = aws_access_key_id.get_secret_value()\n if not aws_access_key_id:\n aws_access_key_id = os.getenv(\"AWS_ACCESS_KEY_ID\")\n\n aws_secret_access_key = getattr(self, \"aws_secret_access_key\", None)\n if aws_secret_access_key and hasattr(aws_secret_access_key, \"get_secret_value\"):\n aws_secret_access_key = aws_secret_access_key.get_secret_value()\n if not aws_secret_access_key:\n aws_secret_access_key = os.getenv(\"AWS_SECRET_ACCESS_KEY\")\n\n bucket_name = getattr(self, \"bucket_name\", None)\n if not bucket_name:\n # Try to get from storage service settings\n settings = get_settings_service().settings\n bucket_name = settings.object_storage_bucket_name\n\n # Validate AWS credentials\n if not aws_access_key_id:\n msg = (\n \"AWS Access Key ID is required for S3 storage. Provide it as a component input \"\n \"or set AWS_ACCESS_KEY_ID environment variable.\"\n )\n raise ValueError(msg)\n if not aws_secret_access_key:\n msg = (\n \"AWS Secret Key is required for S3 storage. Provide it as a component input \"\n \"or set AWS_SECRET_ACCESS_KEY environment variable.\"\n )\n raise ValueError(msg)\n if not bucket_name:\n msg = (\n \"S3 Bucket Name is required for S3 storage. Provide it as a component input \"\n \"or set LANGFLOW_OBJECT_STORAGE_BUCKET_NAME environment variable.\"\n )\n raise ValueError(msg)\n\n # Validate AWS credentials\n validate_aws_credentials(self)\n\n # Create S3 client\n s3_client = create_s3_client(self)\n client_config: dict[str, Any] = {\n \"aws_access_key_id\": str(aws_access_key_id),\n \"aws_secret_access_key\": str(aws_secret_access_key),\n }\n\n # Get region from component input, environment variable, or settings\n aws_region = getattr(self, \"aws_region\", None)\n if not aws_region:\n aws_region = os.getenv(\"AWS_DEFAULT_REGION\") or os.getenv(\"AWS_REGION\")\n if aws_region:\n client_config[\"region_name\"] = str(aws_region)\n\n s3_client = boto3.client(\"s3\", **client_config)\n\n # Extract content\n content = self._extract_content_for_upload()\n file_format = self._get_file_format_for_location(\"AWS\")\n\n # Generate file path\n file_path = f\"{self.file_name}.{file_format}\"\n if hasattr(self, \"s3_prefix\") and self.s3_prefix:\n file_path = f\"{self.s3_prefix.rstrip('/')}/{file_path}\"\n\n # Create temporary file\n import tempfile\n\n with tempfile.NamedTemporaryFile(\n mode=\"w\", encoding=\"utf-8\", suffix=f\".{file_format}\", delete=False\n ) as temp_file:\n temp_file.write(content)\n temp_file_path = temp_file.name\n\n try:\n # Upload to S3\n s3_client.upload_file(temp_file_path, bucket_name, file_path)\n s3_url = f\"s3://{bucket_name}/{file_path}\"\n return Message(text=f\"File successfully uploaded to {s3_url}\")\n finally:\n # Clean up temp file\n if Path(temp_file_path).exists():\n Path(temp_file_path).unlink()\n\n async def _save_to_google_drive(self) -> Message:\n \"\"\"Save file to Google Drive using Google Drive functionality.\"\"\"\n import tempfile\n\n from googleapiclient.http import MediaFileUpload\n\n from lfx.base.data.cloud_storage_utils import create_google_drive_service\n\n # Validate Google Drive credentials\n if not getattr(self, \"service_account_key\", None):\n msg = \"GCP Credentials Secret Key is required for Google Drive storage\"\n raise ValueError(msg)\n if not getattr(self, \"folder_id\", None):\n msg = \"Google Drive Folder ID is required for Google Drive storage\"\n raise ValueError(msg)\n\n # Create Google Drive service with full drive scope (needed for folder operations)\n drive_service, credentials = create_google_drive_service(\n self.service_account_key, scopes=[\"https://www.googleapis.com/auth/drive\"], return_credentials=True\n )\n\n # Extract content and format\n content = self._extract_content_for_upload()\n file_format = self._get_file_format_for_location(\"Google Drive\")\n\n # Handle special Google Drive formats\n if file_format in [\"slides\", \"docs\"]:\n return await self._save_to_google_apps(drive_service, credentials, content, file_format)\n\n # Create temporary file\n file_path = f\"{self.file_name}.{file_format}\"\n with tempfile.NamedTemporaryFile(\n mode=\"w\",\n encoding=\"utf-8\",\n suffix=f\".{file_format}\",\n delete=False,\n ) as temp_file:\n temp_file.write(content)\n temp_file_path = temp_file.name\n\n try:\n # Upload to Google Drive\n # Note: We skip explicit folder verification since it requires broader permissions.\n # If the folder doesn't exist or isn't accessible, the create() call will fail with a clear error.\n file_metadata = {\"name\": file_path, \"parents\": [self.folder_id]}\n media = MediaFileUpload(temp_file_path, resumable=True)\n\n try:\n uploaded_file = (\n drive_service.files().create(body=file_metadata, media_body=media, fields=\"id\").execute()\n )\n except Exception as e:\n msg = (\n f\"Unable to upload file to Google Drive folder '{self.folder_id}'. \"\n f\"Error: {e!s}. \"\n \"Please ensure: 1) The folder ID is correct, 2) The folder exists, \"\n \"3) The service account has been granted access to this folder.\"\n )\n raise ValueError(msg) from e\n\n file_id = uploaded_file.get(\"id\")\n file_url = f\"https://drive.google.com/file/d/{file_id}/view\"\n return Message(text=f\"File successfully uploaded to Google Drive: {file_url}\")\n finally:\n # Clean up temp file\n if Path(temp_file_path).exists():\n Path(temp_file_path).unlink()\n\n async def _save_to_google_apps(self, drive_service, credentials, content: str, app_type: str) -> Message:\n \"\"\"Save content to Google Apps (Slides or Docs).\"\"\"\n import time\n\n if app_type == \"slides\":\n from googleapiclient.discovery import build\n\n slides_service = build(\"slides\", \"v1\", credentials=credentials)\n\n file_metadata = {\n \"name\": self.file_name,\n \"mimeType\": \"application/vnd.google-apps.presentation\",\n \"parents\": [self.folder_id],\n }\n\n created_file = drive_service.files().create(body=file_metadata, fields=\"id\").execute()\n presentation_id = created_file[\"id\"]\n\n time.sleep(2) # Wait for file to be available # noqa: ASYNC251\n\n presentation = slides_service.presentations().get(presentationId=presentation_id).execute()\n slide_id = presentation[\"slides\"][0][\"objectId\"]\n\n # Add content to slide\n requests = [\n {\n \"createShape\": {\n \"objectId\": \"TextBox_01\",\n \"shapeType\": \"TEXT_BOX\",\n \"elementProperties\": {\n \"pageObjectId\": slide_id,\n \"size\": {\n \"height\": {\"magnitude\": 3000000, \"unit\": \"EMU\"},\n \"width\": {\"magnitude\": 6000000, \"unit\": \"EMU\"},\n },\n \"transform\": {\n \"scaleX\": 1,\n \"scaleY\": 1,\n \"translateX\": 1000000,\n \"translateY\": 1000000,\n \"unit\": \"EMU\",\n },\n },\n }\n },\n {\"insertText\": {\"objectId\": \"TextBox_01\", \"insertionIndex\": 0, \"text\": content}},\n ]\n\n slides_service.presentations().batchUpdate(\n presentationId=presentation_id, body={\"requests\": requests}\n ).execute()\n file_url = f\"https://docs.google.com/presentation/d/{presentation_id}/edit\"\n\n elif app_type == \"docs\":\n from googleapiclient.discovery import build\n\n docs_service = build(\"docs\", \"v1\", credentials=credentials)\n\n file_metadata = {\n \"name\": self.file_name,\n \"mimeType\": \"application/vnd.google-apps.document\",\n \"parents\": [self.folder_id],\n }\n\n created_file = drive_service.files().create(body=file_metadata, fields=\"id\").execute()\n document_id = created_file[\"id\"]\n\n time.sleep(2) # Wait for file to be available # noqa: ASYNC251\n\n # Add content to document\n requests = [{\"insertText\": {\"location\": {\"index\": 1}, \"text\": content}}]\n docs_service.documents().batchUpdate(documentId=document_id, body={\"requests\": requests}).execute()\n file_url = f\"https://docs.google.com/document/d/{document_id}/edit\"\n\n return Message(text=f\"File successfully created in Google {app_type.title()}: {file_url}\")\n\n def _extract_content_for_upload(self) -> str:\n \"\"\"Extract content from input for upload to cloud services.\"\"\"\n if self._get_input_type() == \"DataFrame\":\n return self.input.to_csv(index=False)\n if self._get_input_type() == \"Data\":\n if hasattr(self.input, \"data\") and self.input.data:\n if isinstance(self.input.data, dict):\n import json\n\n return json.dumps(self.input.data, indent=2, ensure_ascii=False)\n return str(self.input.data)\n return str(self.input)\n if self._get_input_type() == \"Message\":\n return str(self.input.text) if self.input.text else str(self.input)\n return str(self.input)\n" + "value": "import json\nfrom collections.abc import AsyncIterator, Iterator\nfrom pathlib import Path\nfrom typing import Any\n\nimport orjson\nimport pandas as pd\nfrom fastapi import UploadFile\nfrom fastapi.encoders import jsonable_encoder\n\nfrom lfx.custom import Component\nfrom lfx.inputs import SortableListInput\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, SecretStrInput, StrInput\nfrom lfx.schema import Data, DataFrame, Message\nfrom lfx.services.deps import get_settings_service, get_storage_service, session_scope\nfrom lfx.template.field.base import Output\nfrom lfx.utils.validate_cloud import is_astra_cloud_environment\n\n\ndef _get_storage_location_options():\n \"\"\"Get storage location options, filtering out Local if in Astra cloud environment.\"\"\"\n all_options = [{\"name\": \"AWS\", \"icon\": \"Amazon\"}, {\"name\": \"Google Drive\", \"icon\": \"google\"}]\n if is_astra_cloud_environment():\n return all_options\n return [{\"name\": \"Local\", \"icon\": \"hard-drive\"}, *all_options]\n\n\nclass SaveToFileComponent(Component):\n display_name = \"Write File\"\n description = \"Save data to local file, AWS S3, or Google Drive in the selected format.\"\n documentation: str = \"https://docs.langflow.org/write-file\"\n icon = \"file-text\"\n name = \"SaveToFile\"\n\n # File format options for different storage types\n LOCAL_DATA_FORMAT_CHOICES = [\"csv\", \"excel\", \"json\", \"markdown\"]\n LOCAL_MESSAGE_FORMAT_CHOICES = [\"txt\", \"json\", \"markdown\"]\n AWS_FORMAT_CHOICES = [\n \"txt\",\n \"json\",\n \"csv\",\n \"xml\",\n \"html\",\n \"md\",\n \"yaml\",\n \"log\",\n \"tsv\",\n \"jsonl\",\n \"parquet\",\n \"xlsx\",\n \"zip\",\n ]\n GDRIVE_FORMAT_CHOICES = [\"txt\", \"json\", \"csv\", \"xlsx\", \"slides\", \"docs\", \"jpg\", \"mp3\"]\n\n inputs = [\n SortableListInput(\n name=\"storage_location\",\n display_name=\"Storage Location\",\n placeholder=\"Select Location\",\n info=\"Choose where to save the file.\",\n options=_get_storage_location_options(),\n real_time_refresh=True,\n limit=1,\n value=[{\"name\": \"Local\", \"icon\": \"hard-drive\"}],\n advanced=True,\n ),\n # Common inputs\n HandleInput(\n name=\"input\",\n display_name=\"File Content\",\n info=\"The input to save.\",\n dynamic=True,\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\", \"Message\"],\n required=True,\n ),\n StrInput(\n name=\"file_name\",\n display_name=\"File Name\",\n info=\"Name file will be saved as (without extension).\",\n required=True,\n show=False,\n tool_mode=True,\n ),\n BoolInput(\n name=\"append_mode\",\n display_name=\"Append\",\n info=(\n \"Append to file if it exists (only for Local storage with plain text formats). \"\n \"Not supported for cloud storage (AWS/Google Drive).\"\n ),\n value=False,\n show=False,\n ),\n # Format inputs (dynamic based on storage location)\n DropdownInput(\n name=\"local_format\",\n display_name=\"File Format\",\n options=list(dict.fromkeys(LOCAL_DATA_FORMAT_CHOICES + LOCAL_MESSAGE_FORMAT_CHOICES)),\n info=\"Select the file format for local storage.\",\n value=\"json\",\n show=False,\n ),\n DropdownInput(\n name=\"aws_format\",\n display_name=\"File Format\",\n options=AWS_FORMAT_CHOICES,\n info=\"Select the file format for AWS S3 storage.\",\n value=\"txt\",\n show=False,\n ),\n DropdownInput(\n name=\"gdrive_format\",\n display_name=\"File Format\",\n options=GDRIVE_FORMAT_CHOICES,\n info=\"Select the file format for Google Drive storage.\",\n value=\"txt\",\n show=False,\n ),\n # AWS S3 specific inputs\n SecretStrInput(\n name=\"aws_access_key_id\",\n display_name=\"AWS Access Key ID\",\n info=\"AWS Access key ID.\",\n show=False,\n advanced=True,\n required=True,\n ),\n SecretStrInput(\n name=\"aws_secret_access_key\",\n display_name=\"AWS Secret Key\",\n info=\"AWS Secret Key.\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"bucket_name\",\n display_name=\"S3 Bucket Name\",\n info=\"Enter the name of the S3 bucket.\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"aws_region\",\n display_name=\"AWS Region\",\n info=\"AWS region (e.g., us-east-1, eu-west-1).\",\n show=False,\n advanced=True,\n ),\n StrInput(\n name=\"s3_prefix\",\n display_name=\"S3 Prefix\",\n info=\"Prefix for all files in S3.\",\n show=False,\n advanced=True,\n ),\n # Google Drive specific inputs\n SecretStrInput(\n name=\"service_account_key\",\n display_name=\"GCP Credentials Secret Key\",\n info=\"Your Google Cloud Platform service account JSON key as a secret string (complete JSON content).\",\n show=False,\n advanced=True,\n required=True,\n ),\n StrInput(\n name=\"folder_id\",\n display_name=\"Google Drive Folder ID\",\n info=(\n \"The Google Drive folder ID where the file will be uploaded. \"\n \"The folder must be shared with the service account email.\"\n ),\n required=True,\n show=False,\n advanced=True,\n ),\n ]\n\n outputs = [Output(display_name=\"File Path\", name=\"message\", method=\"save_to_file\")]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Update build configuration to show/hide fields based on storage location selection.\"\"\"\n # Update options dynamically based on cloud environment\n # This ensures options are refreshed when build_config is updated\n if \"storage_location\" in build_config:\n updated_options = _get_storage_location_options()\n build_config[\"storage_location\"][\"options\"] = updated_options\n\n if field_name != \"storage_location\":\n return build_config\n\n # Extract selected storage location\n selected = [location[\"name\"] for location in field_value] if isinstance(field_value, list) else []\n\n # Hide all dynamic fields first\n dynamic_fields = [\n \"file_name\", # Common fields (input is always visible)\n \"append_mode\",\n \"local_format\",\n \"aws_format\",\n \"gdrive_format\",\n \"aws_access_key_id\",\n \"aws_secret_access_key\",\n \"bucket_name\",\n \"aws_region\",\n \"s3_prefix\",\n \"service_account_key\",\n \"folder_id\",\n ]\n\n for f_name in dynamic_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = False\n\n # Show fields based on selected storage location\n if len(selected) == 1:\n location = selected[0]\n\n # Show file_name when any storage location is selected\n if \"file_name\" in build_config:\n build_config[\"file_name\"][\"show\"] = True\n\n # Show append_mode only for Local storage (not supported for cloud storage)\n if \"append_mode\" in build_config:\n build_config[\"append_mode\"][\"show\"] = location == \"Local\"\n\n if location == \"Local\":\n if \"local_format\" in build_config:\n build_config[\"local_format\"][\"show\"] = True\n\n elif location == \"AWS\":\n aws_fields = [\n \"aws_format\",\n \"aws_access_key_id\",\n \"aws_secret_access_key\",\n \"bucket_name\",\n \"aws_region\",\n \"s3_prefix\",\n ]\n for f_name in aws_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = True\n build_config[f_name][\"advanced\"] = False\n\n elif location == \"Google Drive\":\n gdrive_fields = [\"gdrive_format\", \"service_account_key\", \"folder_id\"]\n for f_name in gdrive_fields:\n if f_name in build_config:\n build_config[f_name][\"show\"] = True\n build_config[f_name][\"advanced\"] = False\n\n return build_config\n\n async def save_to_file(self) -> Message:\n \"\"\"Save the input to a file and upload it, returning a confirmation message.\"\"\"\n # Validate inputs\n if not self.file_name:\n msg = \"File name must be provided.\"\n raise ValueError(msg)\n if not self._get_input_type():\n msg = \"Input type is not set.\"\n raise ValueError(msg)\n\n # Get selected storage location\n storage_location = self._get_selected_storage_location()\n if not storage_location:\n msg = \"Storage location must be selected.\"\n raise ValueError(msg)\n\n # Check if Local storage is disabled in cloud environment\n if storage_location == \"Local\" and is_astra_cloud_environment():\n msg = \"Local storage is not available in cloud environment. Please use AWS or Google Drive.\"\n raise ValueError(msg)\n\n # Route to appropriate save method based on storage location\n if storage_location == \"Local\":\n return await self._save_to_local()\n if storage_location == \"AWS\":\n return await self._save_to_aws()\n if storage_location == \"Google Drive\":\n return await self._save_to_google_drive()\n msg = f\"Unsupported storage location: {storage_location}\"\n raise ValueError(msg)\n\n def _get_input_type(self) -> str:\n \"\"\"Determine the input type based on the provided input.\"\"\"\n # Use exact type checking (type() is) instead of isinstance() to avoid inheritance issues.\n # Since Message inherits from Data, isinstance(message, Data) would return True for Message objects,\n # causing Message inputs to be incorrectly identified as Data type.\n if type(self.input) is DataFrame:\n return \"DataFrame\"\n if type(self.input) is Message:\n return \"Message\"\n if type(self.input) is Data:\n return \"Data\"\n msg = f\"Unsupported input type: {type(self.input)}\"\n raise ValueError(msg)\n\n def _get_default_format(self) -> str:\n \"\"\"Return the default file format based on input type.\"\"\"\n if self._get_input_type() == \"DataFrame\":\n return \"csv\"\n if self._get_input_type() == \"Data\":\n return \"json\"\n if self._get_input_type() == \"Message\":\n return \"json\"\n return \"json\" # Fallback\n\n def _adjust_file_path_with_format(self, path: Path, fmt: str) -> Path:\n \"\"\"Adjust the file path to include the correct extension.\"\"\"\n file_extension = path.suffix.lower().lstrip(\".\")\n if fmt == \"excel\":\n return Path(f\"{path}.xlsx\").expanduser() if file_extension not in [\"xlsx\", \"xls\"] else path\n return Path(f\"{path}.{fmt}\").expanduser() if file_extension != fmt else path\n\n def _is_plain_text_format(self, fmt: str) -> bool:\n \"\"\"Check if a file format is plain text (supports appending).\"\"\"\n plain_text_formats = [\"txt\", \"json\", \"markdown\", \"md\", \"csv\", \"xml\", \"html\", \"yaml\", \"log\", \"tsv\", \"jsonl\"]\n return fmt.lower() in plain_text_formats\n\n async def _upload_file(self, file_path: Path) -> None:\n \"\"\"Upload the saved file using the upload_user_file service.\"\"\"\n from langflow.api.v2.files import upload_user_file\n from langflow.services.database.models.user.crud import get_user_by_id\n\n # Ensure the file exists\n if not file_path.exists():\n msg = f\"File not found: {file_path}\"\n raise FileNotFoundError(msg)\n\n # Upload the file - always use append=False because the local file already contains\n # the correct content (either new or appended locally)\n with file_path.open(\"rb\") as f:\n async with session_scope() as db:\n if not self.user_id:\n msg = \"User ID is required for file saving.\"\n raise ValueError(msg)\n current_user = await get_user_by_id(db, self.user_id)\n\n await upload_user_file(\n file=UploadFile(filename=file_path.name, file=f, size=file_path.stat().st_size),\n session=db,\n current_user=current_user,\n storage_service=get_storage_service(),\n settings_service=get_settings_service(),\n append=False,\n )\n\n def _save_dataframe(self, dataframe: DataFrame, path: Path, fmt: str) -> str:\n \"\"\"Save a DataFrame to the specified file format.\"\"\"\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"csv\":\n dataframe.to_csv(path, index=False, mode=\"a\" if should_append else \"w\", header=not should_append)\n elif fmt == \"excel\":\n dataframe.to_excel(path, index=False, engine=\"openpyxl\")\n elif fmt == \"json\":\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new data\n new_records = json.loads(dataframe.to_json(orient=\"records\"))\n existing_data.extend(new_records)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n dataframe.to_json(path, orient=\"records\", indent=2)\n elif fmt == \"markdown\":\n content = dataframe.to_markdown(index=False)\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported DataFrame format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"DataFrame {action} '{path}'\"\n\n def _save_data(self, data: Data, path: Path, fmt: str) -> str:\n \"\"\"Save a Data object to the specified file format.\"\"\"\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"csv\":\n pd.DataFrame(data.data).to_csv(\n path,\n index=False,\n mode=\"a\" if should_append else \"w\",\n header=not should_append,\n )\n elif fmt == \"excel\":\n pd.DataFrame(data.data).to_excel(path, index=False, engine=\"openpyxl\")\n elif fmt == \"json\":\n new_data = jsonable_encoder(data.data)\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new data\n if isinstance(new_data, list):\n existing_data.extend(new_data)\n else:\n existing_data.append(new_data)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n content = orjson.dumps(new_data, option=orjson.OPT_INDENT_2).decode(\"utf-8\")\n path.write_text(content, encoding=\"utf-8\")\n elif fmt == \"markdown\":\n content = pd.DataFrame(data.data).to_markdown(index=False)\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported Data format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"Data {action} '{path}'\"\n\n async def _save_message(self, message: Message, path: Path, fmt: str) -> str:\n \"\"\"Save a Message to the specified file format, handling async iterators.\"\"\"\n content = \"\"\n if message.text is None:\n content = \"\"\n elif isinstance(message.text, AsyncIterator):\n async for item in message.text:\n content += str(item) + \" \"\n content = content.strip()\n elif isinstance(message.text, Iterator):\n content = \" \".join(str(item) for item in message.text)\n else:\n content = str(message.text)\n\n append_mode = getattr(self, \"append_mode\", False)\n should_append = append_mode and path.exists() and self._is_plain_text_format(fmt)\n\n if fmt == \"txt\":\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\" + content, encoding=\"utf-8\")\n else:\n path.write_text(content, encoding=\"utf-8\")\n elif fmt == \"json\":\n new_message = {\"message\": content}\n if should_append:\n # Read and parse existing JSON\n existing_data = []\n try:\n existing_content = path.read_text(encoding=\"utf-8\").strip()\n if existing_content:\n parsed = json.loads(existing_content)\n # Handle case where existing content is a single object\n if isinstance(parsed, dict):\n existing_data = [parsed]\n elif isinstance(parsed, list):\n existing_data = parsed\n except (json.JSONDecodeError, FileNotFoundError):\n # Treat parse errors or missing file as empty array\n existing_data = []\n\n # Append new message\n existing_data.append(new_message)\n\n # Write back as a single JSON array\n path.write_text(json.dumps(existing_data, indent=2), encoding=\"utf-8\")\n else:\n path.write_text(json.dumps(new_message, indent=2), encoding=\"utf-8\")\n elif fmt == \"markdown\":\n md_content = f\"**Message:**\\n\\n{content}\"\n if should_append:\n path.write_text(path.read_text(encoding=\"utf-8\") + \"\\n\\n\" + md_content, encoding=\"utf-8\")\n else:\n path.write_text(md_content, encoding=\"utf-8\")\n else:\n msg = f\"Unsupported Message format: {fmt}\"\n raise ValueError(msg)\n action = \"appended to\" if should_append else \"saved successfully as\"\n return f\"Message {action} '{path}'\"\n\n def _get_selected_storage_location(self) -> str:\n \"\"\"Get the selected storage location from the SortableListInput.\"\"\"\n if hasattr(self, \"storage_location\") and self.storage_location:\n if isinstance(self.storage_location, list) and len(self.storage_location) > 0:\n return self.storage_location[0].get(\"name\", \"\")\n if isinstance(self.storage_location, dict):\n return self.storage_location.get(\"name\", \"\")\n return \"\"\n\n def _get_file_format_for_location(self, location: str) -> str:\n \"\"\"Get the appropriate file format based on storage location.\"\"\"\n if location == \"Local\":\n return getattr(self, \"local_format\", None) or self._get_default_format()\n if location == \"AWS\":\n return getattr(self, \"aws_format\", \"txt\")\n if location == \"Google Drive\":\n return getattr(self, \"gdrive_format\", \"txt\")\n return self._get_default_format()\n\n async def _save_to_local(self) -> Message:\n \"\"\"Save file to local storage (original functionality).\"\"\"\n file_format = self._get_file_format_for_location(\"Local\")\n\n # Validate file format based on input type\n allowed_formats = (\n self.LOCAL_MESSAGE_FORMAT_CHOICES if self._get_input_type() == \"Message\" else self.LOCAL_DATA_FORMAT_CHOICES\n )\n if file_format not in allowed_formats:\n msg = f\"Invalid file format '{file_format}' for {self._get_input_type()}. Allowed: {allowed_formats}\"\n raise ValueError(msg)\n\n # Prepare file path\n file_path = Path(self.file_name).expanduser()\n if not file_path.parent.exists():\n file_path.parent.mkdir(parents=True, exist_ok=True)\n file_path = self._adjust_file_path_with_format(file_path, file_format)\n\n # Save the input to file based on type\n if self._get_input_type() == \"DataFrame\":\n confirmation = self._save_dataframe(self.input, file_path, file_format)\n elif self._get_input_type() == \"Data\":\n confirmation = self._save_data(self.input, file_path, file_format)\n elif self._get_input_type() == \"Message\":\n confirmation = await self._save_message(self.input, file_path, file_format)\n else:\n msg = f\"Unsupported input type: {self._get_input_type()}\"\n raise ValueError(msg)\n\n # Upload the saved file\n await self._upload_file(file_path)\n\n # Return the final file path and confirmation message\n final_path = Path.cwd() / file_path if not file_path.is_absolute() else file_path\n return Message(text=f\"{confirmation} at {final_path}\")\n\n async def _save_to_aws(self) -> Message:\n \"\"\"Save file to AWS S3 using S3 functionality.\"\"\"\n import os\n\n import boto3\n\n from lfx.base.data.cloud_storage_utils import create_s3_client, validate_aws_credentials\n\n # Get AWS credentials from component inputs or fall back to environment variables\n aws_access_key_id = getattr(self, \"aws_access_key_id\", None)\n if aws_access_key_id and hasattr(aws_access_key_id, \"get_secret_value\"):\n aws_access_key_id = aws_access_key_id.get_secret_value()\n if not aws_access_key_id:\n aws_access_key_id = os.getenv(\"AWS_ACCESS_KEY_ID\")\n\n aws_secret_access_key = getattr(self, \"aws_secret_access_key\", None)\n if aws_secret_access_key and hasattr(aws_secret_access_key, \"get_secret_value\"):\n aws_secret_access_key = aws_secret_access_key.get_secret_value()\n if not aws_secret_access_key:\n aws_secret_access_key = os.getenv(\"AWS_SECRET_ACCESS_KEY\")\n\n bucket_name = getattr(self, \"bucket_name\", None)\n if not bucket_name:\n # Try to get from storage service settings\n settings = get_settings_service().settings\n bucket_name = settings.object_storage_bucket_name\n\n # Validate AWS credentials\n if not aws_access_key_id:\n msg = (\n \"AWS Access Key ID is required for S3 storage. Provide it as a component input \"\n \"or set AWS_ACCESS_KEY_ID environment variable.\"\n )\n raise ValueError(msg)\n if not aws_secret_access_key:\n msg = (\n \"AWS Secret Key is required for S3 storage. Provide it as a component input \"\n \"or set AWS_SECRET_ACCESS_KEY environment variable.\"\n )\n raise ValueError(msg)\n if not bucket_name:\n msg = (\n \"S3 Bucket Name is required for S3 storage. Provide it as a component input \"\n \"or set LANGFLOW_OBJECT_STORAGE_BUCKET_NAME environment variable.\"\n )\n raise ValueError(msg)\n\n # Validate AWS credentials\n validate_aws_credentials(self)\n\n # Create S3 client\n s3_client = create_s3_client(self)\n client_config: dict[str, Any] = {\n \"aws_access_key_id\": str(aws_access_key_id),\n \"aws_secret_access_key\": str(aws_secret_access_key),\n }\n\n # Get region from component input, environment variable, or settings\n aws_region = getattr(self, \"aws_region\", None)\n if not aws_region:\n aws_region = os.getenv(\"AWS_DEFAULT_REGION\") or os.getenv(\"AWS_REGION\")\n if aws_region:\n client_config[\"region_name\"] = str(aws_region)\n\n s3_client = boto3.client(\"s3\", **client_config)\n\n # Extract content\n content = self._extract_content_for_upload()\n file_format = self._get_file_format_for_location(\"AWS\")\n\n # Generate file path\n file_path = f\"{self.file_name}.{file_format}\"\n if hasattr(self, \"s3_prefix\") and self.s3_prefix:\n file_path = f\"{self.s3_prefix.rstrip('/')}/{file_path}\"\n\n # Create temporary file\n import tempfile\n\n with tempfile.NamedTemporaryFile(\n mode=\"w\", encoding=\"utf-8\", suffix=f\".{file_format}\", delete=False\n ) as temp_file:\n temp_file.write(content)\n temp_file_path = temp_file.name\n\n try:\n # Upload to S3\n s3_client.upload_file(temp_file_path, bucket_name, file_path)\n s3_url = f\"s3://{bucket_name}/{file_path}\"\n return Message(text=f\"File successfully uploaded to {s3_url}\")\n finally:\n # Clean up temp file\n if Path(temp_file_path).exists():\n Path(temp_file_path).unlink()\n\n async def _save_to_google_drive(self) -> Message:\n \"\"\"Save file to Google Drive using Google Drive functionality.\"\"\"\n import tempfile\n\n from googleapiclient.http import MediaFileUpload\n\n from lfx.base.data.cloud_storage_utils import create_google_drive_service\n\n # Validate Google Drive credentials\n if not getattr(self, \"service_account_key\", None):\n msg = \"GCP Credentials Secret Key is required for Google Drive storage\"\n raise ValueError(msg)\n if not getattr(self, \"folder_id\", None):\n msg = \"Google Drive Folder ID is required for Google Drive storage\"\n raise ValueError(msg)\n\n # Create Google Drive service with full drive scope (needed for folder operations)\n drive_service, credentials = create_google_drive_service(\n self.service_account_key, scopes=[\"https://www.googleapis.com/auth/drive\"], return_credentials=True\n )\n\n # Extract content and format\n content = self._extract_content_for_upload()\n file_format = self._get_file_format_for_location(\"Google Drive\")\n\n # Handle special Google Drive formats\n if file_format in [\"slides\", \"docs\"]:\n return await self._save_to_google_apps(drive_service, credentials, content, file_format)\n\n # Create temporary file\n file_path = f\"{self.file_name}.{file_format}\"\n with tempfile.NamedTemporaryFile(\n mode=\"w\",\n encoding=\"utf-8\",\n suffix=f\".{file_format}\",\n delete=False,\n ) as temp_file:\n temp_file.write(content)\n temp_file_path = temp_file.name\n\n try:\n # Upload to Google Drive\n # Note: We skip explicit folder verification since it requires broader permissions.\n # If the folder doesn't exist or isn't accessible, the create() call will fail with a clear error.\n file_metadata = {\"name\": file_path, \"parents\": [self.folder_id]}\n media = MediaFileUpload(temp_file_path, resumable=True)\n\n try:\n uploaded_file = (\n drive_service.files().create(body=file_metadata, media_body=media, fields=\"id\").execute()\n )\n except Exception as e:\n msg = (\n f\"Unable to upload file to Google Drive folder '{self.folder_id}'. \"\n f\"Error: {e!s}. \"\n \"Please ensure: 1) The folder ID is correct, 2) The folder exists, \"\n \"3) The service account has been granted access to this folder.\"\n )\n raise ValueError(msg) from e\n\n file_id = uploaded_file.get(\"id\")\n file_url = f\"https://drive.google.com/file/d/{file_id}/view\"\n return Message(text=f\"File successfully uploaded to Google Drive: {file_url}\")\n finally:\n # Clean up temp file\n if Path(temp_file_path).exists():\n Path(temp_file_path).unlink()\n\n async def _save_to_google_apps(self, drive_service, credentials, content: str, app_type: str) -> Message:\n \"\"\"Save content to Google Apps (Slides or Docs).\"\"\"\n import time\n\n if app_type == \"slides\":\n from googleapiclient.discovery import build\n\n slides_service = build(\"slides\", \"v1\", credentials=credentials)\n\n file_metadata = {\n \"name\": self.file_name,\n \"mimeType\": \"application/vnd.google-apps.presentation\",\n \"parents\": [self.folder_id],\n }\n\n created_file = drive_service.files().create(body=file_metadata, fields=\"id\").execute()\n presentation_id = created_file[\"id\"]\n\n time.sleep(2) # Wait for file to be available # noqa: ASYNC251\n\n presentation = slides_service.presentations().get(presentationId=presentation_id).execute()\n slide_id = presentation[\"slides\"][0][\"objectId\"]\n\n # Add content to slide\n requests = [\n {\n \"createShape\": {\n \"objectId\": \"TextBox_01\",\n \"shapeType\": \"TEXT_BOX\",\n \"elementProperties\": {\n \"pageObjectId\": slide_id,\n \"size\": {\n \"height\": {\"magnitude\": 3000000, \"unit\": \"EMU\"},\n \"width\": {\"magnitude\": 6000000, \"unit\": \"EMU\"},\n },\n \"transform\": {\n \"scaleX\": 1,\n \"scaleY\": 1,\n \"translateX\": 1000000,\n \"translateY\": 1000000,\n \"unit\": \"EMU\",\n },\n },\n }\n },\n {\"insertText\": {\"objectId\": \"TextBox_01\", \"insertionIndex\": 0, \"text\": content}},\n ]\n\n slides_service.presentations().batchUpdate(\n presentationId=presentation_id, body={\"requests\": requests}\n ).execute()\n file_url = f\"https://docs.google.com/presentation/d/{presentation_id}/edit\"\n\n elif app_type == \"docs\":\n from googleapiclient.discovery import build\n\n docs_service = build(\"docs\", \"v1\", credentials=credentials)\n\n file_metadata = {\n \"name\": self.file_name,\n \"mimeType\": \"application/vnd.google-apps.document\",\n \"parents\": [self.folder_id],\n }\n\n created_file = drive_service.files().create(body=file_metadata, fields=\"id\").execute()\n document_id = created_file[\"id\"]\n\n time.sleep(2) # Wait for file to be available # noqa: ASYNC251\n\n # Add content to document\n requests = [{\"insertText\": {\"location\": {\"index\": 1}, \"text\": content}}]\n docs_service.documents().batchUpdate(documentId=document_id, body={\"requests\": requests}).execute()\n file_url = f\"https://docs.google.com/document/d/{document_id}/edit\"\n\n return Message(text=f\"File successfully created in Google {app_type.title()}: {file_url}\")\n\n def _extract_content_for_upload(self) -> str:\n \"\"\"Extract content from input for upload to cloud services.\"\"\"\n if self._get_input_type() == \"DataFrame\":\n return self.input.to_csv(index=False)\n if self._get_input_type() == \"Data\":\n if hasattr(self.input, \"data\") and self.input.data:\n if isinstance(self.input.data, dict):\n import json\n\n return json.dumps(self.input.data, indent=2, ensure_ascii=False)\n return str(self.input.data)\n return str(self.input)\n if self._get_input_type() == \"Message\":\n return str(self.input.text) if self.input.text else str(self.input)\n return str(self.input)\n" }, "file_name": { "_input_type": "StrInput", @@ -70414,7 +70460,9 @@ "info": "The input to save.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -70550,7 +70598,7 @@ { "FirecrawlCrawlApi": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -70570,7 +70618,7 @@ "frozen": false, "legacy": false, "metadata": { - "code_hash": "22fd75efce27", + "code_hash": "21b4965f8b53", "dependencies": { "dependencies": [ { @@ -70592,14 +70640,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "crawl", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -70642,16 +70690,17 @@ "show": true, "title_case": false, "type": "code", - "value": "import uuid\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, IntInput, MultilineInput, Output, SecretStrInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass FirecrawlCrawlApi(Component):\n display_name: str = \"Firecrawl Crawl API\"\n description: str = \"Crawls a URL and returns the results.\"\n name = \"FirecrawlCrawlApi\"\n\n documentation: str = \"https://docs.firecrawl.dev/v1/api-reference/endpoint/crawl-post\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Firecrawl API Key\",\n required=True,\n password=True,\n info=\"The API key to use Firecrawl API.\",\n ),\n MultilineInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL to scrape.\",\n tool_mode=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout in milliseconds for the request.\",\n ),\n StrInput(\n name=\"idempotency_key\",\n display_name=\"Idempotency Key\",\n info=\"Optional idempotency key to ensure unique requests.\",\n ),\n DataInput(\n name=\"crawlerOptions\",\n display_name=\"Crawler Options\",\n info=\"The crawler options to send with the request.\",\n ),\n DataInput(\n name=\"scrapeOptions\",\n display_name=\"Scrape Options\",\n info=\"The page options to send with the request.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"crawl\"),\n ]\n idempotency_key: str | None = None\n\n def crawl(self) -> Data:\n try:\n from firecrawl import FirecrawlApp\n except ImportError as e:\n msg = \"Could not import firecrawl integration package. Please install it with `pip install firecrawl-py`.\"\n raise ImportError(msg) from e\n\n params = self.crawlerOptions.__dict__[\"data\"] if self.crawlerOptions else {}\n scrape_options_dict = self.scrapeOptions.__dict__[\"data\"] if self.scrapeOptions else {}\n if scrape_options_dict:\n params[\"scrapeOptions\"] = scrape_options_dict\n\n # Set default values for new parameters in v1\n params.setdefault(\"maxDepth\", 2)\n params.setdefault(\"limit\", 10000)\n params.setdefault(\"allowExternalLinks\", False)\n params.setdefault(\"allowBackwardLinks\", False)\n params.setdefault(\"ignoreSitemap\", False)\n params.setdefault(\"ignoreQueryParameters\", False)\n\n # Ensure onlyMainContent is explicitly set if not provided\n if \"scrapeOptions\" in params:\n params[\"scrapeOptions\"].setdefault(\"onlyMainContent\", True)\n else:\n params[\"scrapeOptions\"] = {\"onlyMainContent\": True}\n\n if not self.idempotency_key:\n self.idempotency_key = str(uuid.uuid4())\n\n app = FirecrawlApp(api_key=self.api_key)\n crawl_result = app.crawl_url(self.url, params=params, idempotency_key=self.idempotency_key)\n return Data(data={\"results\": crawl_result})\n" + "value": "import uuid\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, IntInput, MultilineInput, Output, SecretStrInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass FirecrawlCrawlApi(Component):\n display_name: str = \"Firecrawl Crawl API\"\n description: str = \"Crawls a URL and returns the results.\"\n name = \"FirecrawlCrawlApi\"\n\n documentation: str = \"https://docs.firecrawl.dev/v1/api-reference/endpoint/crawl-post\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Firecrawl API Key\",\n required=True,\n password=True,\n info=\"The API key to use Firecrawl API.\",\n ),\n MultilineInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL to scrape.\",\n tool_mode=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout in milliseconds for the request.\",\n ),\n StrInput(\n name=\"idempotency_key\",\n display_name=\"Idempotency Key\",\n info=\"Optional idempotency key to ensure unique requests.\",\n ),\n DataInput(\n name=\"crawlerOptions\",\n display_name=\"Crawler Options\",\n info=\"The crawler options to send with the request.\",\n ),\n DataInput(\n name=\"scrapeOptions\",\n display_name=\"Scrape Options\",\n info=\"The page options to send with the request.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"crawl\"),\n ]\n idempotency_key: str | None = None\n\n def crawl(self) -> Data:\n try:\n from firecrawl import FirecrawlApp\n except ImportError as e:\n msg = \"Could not import firecrawl integration package. Please install it with `pip install firecrawl-py`.\"\n raise ImportError(msg) from e\n\n params = self.crawlerOptions.__dict__[\"data\"] if self.crawlerOptions else {}\n scrape_options_dict = self.scrapeOptions.__dict__[\"data\"] if self.scrapeOptions else {}\n if scrape_options_dict:\n params[\"scrapeOptions\"] = scrape_options_dict\n\n # Set default values for new parameters in v1\n params.setdefault(\"maxDepth\", 2)\n params.setdefault(\"limit\", 10000)\n params.setdefault(\"allowExternalLinks\", False)\n params.setdefault(\"allowBackwardLinks\", False)\n params.setdefault(\"ignoreSitemap\", False)\n params.setdefault(\"ignoreQueryParameters\", False)\n\n # Ensure onlyMainContent is explicitly set if not provided\n if \"scrapeOptions\" in params:\n params[\"scrapeOptions\"].setdefault(\"onlyMainContent\", True)\n else:\n params[\"scrapeOptions\"] = {\"onlyMainContent\": True}\n\n if not self.idempotency_key:\n self.idempotency_key = str(uuid.uuid4())\n\n app = FirecrawlApp(api_key=self.api_key)\n crawl_result = app.crawl_url(self.url, params=params, idempotency_key=self.idempotency_key)\n return Data(data={\"results\": crawl_result})\n" }, "crawlerOptions": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Crawler Options", "dynamic": false, "info": "The crawler options to send with the request.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -70690,13 +70739,14 @@ "value": "" }, "scrapeOptions": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Scrape Options", "dynamic": false, "info": "The page options to send with the request.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -70767,7 +70817,7 @@ }, "FirecrawlExtractApi": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -70786,7 +70836,7 @@ "frozen": false, "legacy": false, "metadata": { - "code_hash": "8083782c2c28", + "code_hash": "1363b7da7bf7", "dependencies": { "dependencies": [ { @@ -70808,14 +70858,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "extract", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -70858,7 +70908,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DataInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass FirecrawlExtractApi(Component):\n display_name: str = \"Firecrawl Extract API\"\n description: str = \"Extracts data from a URL.\"\n name = \"FirecrawlExtractApi\"\n\n documentation: str = \"https://docs.firecrawl.dev/api-reference/endpoint/extract\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Firecrawl API Key\",\n required=True,\n password=True,\n info=\"The API key to use Firecrawl API.\",\n ),\n MultilineInput(\n name=\"urls\",\n display_name=\"URLs\",\n required=True,\n info=\"List of URLs to extract data from (separated by commas or new lines).\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n required=True,\n info=\"Prompt to guide the extraction process.\",\n tool_mode=True,\n ),\n DataInput(\n name=\"schema\",\n display_name=\"Schema\",\n required=False,\n info=\"Schema to define the structure of the extracted data.\",\n ),\n BoolInput(\n name=\"enable_web_search\",\n display_name=\"Enable Web Search\",\n info=\"When true, the extraction will use web search to find additional data.\",\n ),\n # # Optional: Not essential for basic extraction\n # BoolInput(\n # name=\"ignore_sitemap\",\n # display_name=\"Ignore Sitemap\",\n # info=\"When true, sitemap.xml files will be ignored during website scanning.\",\n # ),\n # # Optional: Not essential for basic extraction\n # BoolInput(\n # name=\"include_subdomains\",\n # display_name=\"Include Subdomains\",\n # info=\"When true, subdomains of the provided URLs will also be scanned.\",\n # ),\n # # Optional: Not essential for basic extraction\n # BoolInput(\n # name=\"show_sources\",\n # display_name=\"Show Sources\",\n # info=\"When true, the sources used to extract the data will be included in the response.\",\n # ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"extract\"),\n ]\n\n def extract(self) -> Data:\n try:\n from firecrawl import FirecrawlApp\n except ImportError as e:\n msg = \"Could not import firecrawl integration package. Please install it with `pip install firecrawl-py`.\"\n raise ImportError(msg) from e\n\n # Validate API key\n if not self.api_key:\n msg = \"API key is required\"\n raise ValueError(msg)\n\n # Validate URLs\n if not self.urls:\n msg = \"URLs are required\"\n raise ValueError(msg)\n\n # Split and validate URLs (handle both commas and newlines)\n urls = [url.strip() for url in self.urls.replace(\"\\n\", \",\").split(\",\") if url.strip()]\n if not urls:\n msg = \"No valid URLs provided\"\n raise ValueError(msg)\n\n # Validate and process prompt\n if not self.prompt:\n msg = \"Prompt is required\"\n raise ValueError(msg)\n\n # Get the prompt text (handling both string and multiline input)\n prompt_text = self.prompt.strip()\n\n # Enhance the prompt to encourage comprehensive extraction\n enhanced_prompt = prompt_text\n if \"schema\" not in prompt_text.lower():\n enhanced_prompt = f\"{prompt_text}. Please extract all instances in a comprehensive, structured format.\"\n\n params = {\n \"prompt\": enhanced_prompt,\n \"enableWebSearch\": self.enable_web_search,\n # Optional parameters - not essential for basic extraction\n \"ignoreSitemap\": self.ignore_sitemap,\n \"includeSubdomains\": self.include_subdomains,\n \"showSources\": self.show_sources,\n \"timeout\": 300,\n }\n\n # Only add schema to params if it's provided and is a valid schema structure\n if self.schema:\n try:\n if isinstance(self.schema, dict) and \"type\" in self.schema:\n params[\"schema\"] = self.schema\n elif hasattr(self.schema, \"dict\") and \"type\" in self.schema.dict():\n params[\"schema\"] = self.schema.dict()\n else:\n # Skip invalid schema without raising an error\n pass\n except Exception as e: # noqa: BLE001\n logger.error(f\"Invalid schema: {e!s}\")\n\n try:\n app = FirecrawlApp(api_key=self.api_key)\n extract_result = app.extract(urls, params=params)\n return Data(data=extract_result)\n except Exception as e:\n msg = f\"Error during extraction: {e!s}\"\n raise ValueError(msg) from e\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DataInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass FirecrawlExtractApi(Component):\n display_name: str = \"Firecrawl Extract API\"\n description: str = \"Extracts data from a URL.\"\n name = \"FirecrawlExtractApi\"\n\n documentation: str = \"https://docs.firecrawl.dev/api-reference/endpoint/extract\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Firecrawl API Key\",\n required=True,\n password=True,\n info=\"The API key to use Firecrawl API.\",\n ),\n MultilineInput(\n name=\"urls\",\n display_name=\"URLs\",\n required=True,\n info=\"List of URLs to extract data from (separated by commas or new lines).\",\n tool_mode=True,\n ),\n MultilineInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n required=True,\n info=\"Prompt to guide the extraction process.\",\n tool_mode=True,\n ),\n DataInput(\n name=\"schema\",\n display_name=\"Schema\",\n required=False,\n info=\"Schema to define the structure of the extracted data.\",\n ),\n BoolInput(\n name=\"enable_web_search\",\n display_name=\"Enable Web Search\",\n info=\"When true, the extraction will use web search to find additional data.\",\n ),\n # # Optional: Not essential for basic extraction\n # BoolInput(\n # name=\"ignore_sitemap\",\n # display_name=\"Ignore Sitemap\",\n # info=\"When true, sitemap.xml files will be ignored during website scanning.\",\n # ),\n # # Optional: Not essential for basic extraction\n # BoolInput(\n # name=\"include_subdomains\",\n # display_name=\"Include Subdomains\",\n # info=\"When true, subdomains of the provided URLs will also be scanned.\",\n # ),\n # # Optional: Not essential for basic extraction\n # BoolInput(\n # name=\"show_sources\",\n # display_name=\"Show Sources\",\n # info=\"When true, the sources used to extract the data will be included in the response.\",\n # ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"extract\"),\n ]\n\n def extract(self) -> Data:\n try:\n from firecrawl import FirecrawlApp\n except ImportError as e:\n msg = \"Could not import firecrawl integration package. Please install it with `pip install firecrawl-py`.\"\n raise ImportError(msg) from e\n\n # Validate API key\n if not self.api_key:\n msg = \"API key is required\"\n raise ValueError(msg)\n\n # Validate URLs\n if not self.urls:\n msg = \"URLs are required\"\n raise ValueError(msg)\n\n # Split and validate URLs (handle both commas and newlines)\n urls = [url.strip() for url in self.urls.replace(\"\\n\", \",\").split(\",\") if url.strip()]\n if not urls:\n msg = \"No valid URLs provided\"\n raise ValueError(msg)\n\n # Validate and process prompt\n if not self.prompt:\n msg = \"Prompt is required\"\n raise ValueError(msg)\n\n # Get the prompt text (handling both string and multiline input)\n prompt_text = self.prompt.strip()\n\n # Enhance the prompt to encourage comprehensive extraction\n enhanced_prompt = prompt_text\n if \"schema\" not in prompt_text.lower():\n enhanced_prompt = f\"{prompt_text}. Please extract all instances in a comprehensive, structured format.\"\n\n params = {\n \"prompt\": enhanced_prompt,\n \"enableWebSearch\": self.enable_web_search,\n # Optional parameters - not essential for basic extraction\n \"ignoreSitemap\": self.ignore_sitemap,\n \"includeSubdomains\": self.include_subdomains,\n \"showSources\": self.show_sources,\n \"timeout\": 300,\n }\n\n # Only add schema to params if it's provided and is a valid schema structure\n if self.schema:\n try:\n if isinstance(self.schema, dict) and \"type\" in self.schema:\n params[\"schema\"] = self.schema\n elif hasattr(self.schema, \"dict\") and \"type\" in self.schema.dict():\n params[\"schema\"] = self.schema.dict()\n else:\n # Skip invalid schema without raising an error\n pass\n except Exception as e: # noqa: BLE001\n logger.error(f\"Invalid schema: {e!s}\")\n\n try:\n app = FirecrawlApp(api_key=self.api_key)\n extract_result = app.extract(urls, params=params)\n return Data(data=extract_result)\n except Exception as e:\n msg = f\"Error during extraction: {e!s}\"\n raise ValueError(msg) from e\n" }, "enable_web_search": { "_input_type": "BoolInput", @@ -70910,13 +70960,14 @@ "value": "" }, "schema": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Schema", "dynamic": false, "info": "Schema to define the structure of the extracted data.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -70967,7 +71018,7 @@ }, "FirecrawlMapApi": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -70986,7 +71037,7 @@ "frozen": false, "legacy": false, "metadata": { - "code_hash": "e326e840049b", + "code_hash": "31e75312e67e", "dependencies": { "dependencies": [ { @@ -71008,14 +71059,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "map", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -71058,7 +71109,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n BoolInput,\n MultilineInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass FirecrawlMapApi(Component):\n display_name: str = \"Firecrawl Map API\"\n description: str = \"Maps a URL and returns the results.\"\n name = \"FirecrawlMapApi\"\n\n documentation: str = \"https://docs.firecrawl.dev/api-reference/endpoint/map\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Firecrawl API Key\",\n required=True,\n password=True,\n info=\"The API key to use Firecrawl API.\",\n ),\n MultilineInput(\n name=\"urls\",\n display_name=\"URLs\",\n required=True,\n info=\"List of URLs to create maps from (separated by commas or new lines).\",\n tool_mode=True,\n ),\n BoolInput(\n name=\"ignore_sitemap\",\n display_name=\"Ignore Sitemap\",\n info=\"When true, the sitemap.xml file will be ignored during crawling.\",\n ),\n BoolInput(\n name=\"sitemap_only\",\n display_name=\"Sitemap Only\",\n info=\"When true, only links found in the sitemap will be returned.\",\n ),\n BoolInput(\n name=\"include_subdomains\",\n display_name=\"Include Subdomains\",\n info=\"When true, subdomains of the provided URL will also be scanned.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"map\"),\n ]\n\n def map(self) -> Data:\n try:\n from firecrawl import FirecrawlApp\n except ImportError as e:\n msg = \"Could not import firecrawl integration package. Please install it with `pip install firecrawl-py`.\"\n raise ImportError(msg) from e\n\n # Validate URLs\n if not self.urls:\n msg = \"URLs are required\"\n raise ValueError(msg)\n\n # Split and validate URLs (handle both commas and newlines)\n urls = [url.strip() for url in self.urls.replace(\"\\n\", \",\").split(\",\") if url.strip()]\n if not urls:\n msg = \"No valid URLs provided\"\n raise ValueError(msg)\n\n params = {\n \"ignoreSitemap\": self.ignore_sitemap,\n \"sitemapOnly\": self.sitemap_only,\n \"includeSubdomains\": self.include_subdomains,\n }\n\n app = FirecrawlApp(api_key=self.api_key)\n\n # Map all provided URLs and combine results\n combined_links = []\n for url in urls:\n result = app.map_url(url, params=params)\n if isinstance(result, dict) and \"links\" in result:\n combined_links.extend(result[\"links\"])\n\n map_result = {\"success\": True, \"links\": combined_links}\n\n return Data(data=map_result)\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n BoolInput,\n MultilineInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass FirecrawlMapApi(Component):\n display_name: str = \"Firecrawl Map API\"\n description: str = \"Maps a URL and returns the results.\"\n name = \"FirecrawlMapApi\"\n\n documentation: str = \"https://docs.firecrawl.dev/api-reference/endpoint/map\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Firecrawl API Key\",\n required=True,\n password=True,\n info=\"The API key to use Firecrawl API.\",\n ),\n MultilineInput(\n name=\"urls\",\n display_name=\"URLs\",\n required=True,\n info=\"List of URLs to create maps from (separated by commas or new lines).\",\n tool_mode=True,\n ),\n BoolInput(\n name=\"ignore_sitemap\",\n display_name=\"Ignore Sitemap\",\n info=\"When true, the sitemap.xml file will be ignored during crawling.\",\n ),\n BoolInput(\n name=\"sitemap_only\",\n display_name=\"Sitemap Only\",\n info=\"When true, only links found in the sitemap will be returned.\",\n ),\n BoolInput(\n name=\"include_subdomains\",\n display_name=\"Include Subdomains\",\n info=\"When true, subdomains of the provided URL will also be scanned.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"map\"),\n ]\n\n def map(self) -> Data:\n try:\n from firecrawl import FirecrawlApp\n except ImportError as e:\n msg = \"Could not import firecrawl integration package. Please install it with `pip install firecrawl-py`.\"\n raise ImportError(msg) from e\n\n # Validate URLs\n if not self.urls:\n msg = \"URLs are required\"\n raise ValueError(msg)\n\n # Split and validate URLs (handle both commas and newlines)\n urls = [url.strip() for url in self.urls.replace(\"\\n\", \",\").split(\",\") if url.strip()]\n if not urls:\n msg = \"No valid URLs provided\"\n raise ValueError(msg)\n\n params = {\n \"ignoreSitemap\": self.ignore_sitemap,\n \"sitemapOnly\": self.sitemap_only,\n \"includeSubdomains\": self.include_subdomains,\n }\n\n app = FirecrawlApp(api_key=self.api_key)\n\n # Map all provided URLs and combine results\n combined_links = []\n for url in urls:\n result = app.map_url(url, params=params)\n if isinstance(result, dict) and \"links\" in result:\n combined_links.extend(result[\"links\"])\n\n map_result = {\"success\": True, \"links\": combined_links}\n\n return Data(data=map_result)\n" }, "ignore_sitemap": { "_input_type": "BoolInput", @@ -71154,7 +71205,7 @@ }, "FirecrawlScrapeApi": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -71173,7 +71224,7 @@ "frozen": false, "legacy": false, "metadata": { - "code_hash": "857b7da04207", + "code_hash": "a56c999d7a42", "dependencies": { "dependencies": [ { @@ -71195,14 +71246,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "scrape", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -71245,16 +71296,17 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n DataInput,\n IntInput,\n MultilineInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass FirecrawlScrapeApi(Component):\n display_name: str = \"Firecrawl Scrape API\"\n description: str = \"Scrapes a URL and returns the results.\"\n name = \"FirecrawlScrapeApi\"\n\n documentation: str = \"https://docs.firecrawl.dev/api-reference/endpoint/scrape\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Firecrawl API Key\",\n required=True,\n password=True,\n info=\"The API key to use Firecrawl API.\",\n ),\n MultilineInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL to scrape.\",\n tool_mode=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout in milliseconds for the request.\",\n ),\n DataInput(\n name=\"scrapeOptions\",\n display_name=\"Scrape Options\",\n info=\"The page options to send with the request.\",\n ),\n DataInput(\n name=\"extractorOptions\",\n display_name=\"Extractor Options\",\n info=\"The extractor options to send with the request.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"scrape\"),\n ]\n\n def scrape(self) -> Data:\n try:\n from firecrawl import FirecrawlApp\n except ImportError as e:\n msg = \"Could not import firecrawl integration package. Please install it with `pip install firecrawl-py`.\"\n raise ImportError(msg) from e\n\n params = self.scrapeOptions.__dict__.get(\"data\", {}) if self.scrapeOptions else {}\n extractor_options_dict = self.extractorOptions.__dict__.get(\"data\", {}) if self.extractorOptions else {}\n if extractor_options_dict:\n params[\"extract\"] = extractor_options_dict\n\n # Set default values for parameters\n params.setdefault(\"formats\", [\"markdown\"]) # Default output format\n params.setdefault(\"onlyMainContent\", True) # Default to only main content\n\n app = FirecrawlApp(api_key=self.api_key)\n results = app.scrape_url(self.url, params=params)\n return Data(data=results)\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n DataInput,\n IntInput,\n MultilineInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass FirecrawlScrapeApi(Component):\n display_name: str = \"Firecrawl Scrape API\"\n description: str = \"Scrapes a URL and returns the results.\"\n name = \"FirecrawlScrapeApi\"\n\n documentation: str = \"https://docs.firecrawl.dev/api-reference/endpoint/scrape\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Firecrawl API Key\",\n required=True,\n password=True,\n info=\"The API key to use Firecrawl API.\",\n ),\n MultilineInput(\n name=\"url\",\n display_name=\"URL\",\n required=True,\n info=\"The URL to scrape.\",\n tool_mode=True,\n ),\n IntInput(\n name=\"timeout\",\n display_name=\"Timeout\",\n info=\"Timeout in milliseconds for the request.\",\n ),\n DataInput(\n name=\"scrapeOptions\",\n display_name=\"Scrape Options\",\n info=\"The page options to send with the request.\",\n ),\n DataInput(\n name=\"extractorOptions\",\n display_name=\"Extractor Options\",\n info=\"The extractor options to send with the request.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"scrape\"),\n ]\n\n def scrape(self) -> Data:\n try:\n from firecrawl import FirecrawlApp\n except ImportError as e:\n msg = \"Could not import firecrawl integration package. Please install it with `pip install firecrawl-py`.\"\n raise ImportError(msg) from e\n\n params = self.scrapeOptions.__dict__.get(\"data\", {}) if self.scrapeOptions else {}\n extractor_options_dict = self.extractorOptions.__dict__.get(\"data\", {}) if self.extractorOptions else {}\n if extractor_options_dict:\n params[\"extract\"] = extractor_options_dict\n\n # Set default values for parameters\n params.setdefault(\"formats\", [\"markdown\"]) # Default output format\n params.setdefault(\"onlyMainContent\", True) # Default to only main content\n\n app = FirecrawlApp(api_key=self.api_key)\n results = app.scrape_url(self.url, params=params)\n return Data(data=results)\n" }, "extractorOptions": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Extractor Options", "dynamic": false, "info": "The extractor options to send with the request.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -71272,13 +71324,14 @@ "value": "" }, "scrapeOptions": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Scrape Options", "dynamic": false, "info": "The page options to send with the request.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -71650,7 +71703,7 @@ }, "DataConditionalRouter": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -71691,10 +71744,10 @@ "group_outputs": false, "method": "process_data", "name": "true_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -71705,10 +71758,10 @@ "group_outputs": false, "method": "process_data", "name": "false_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -71763,13 +71816,14 @@ "value": "" }, "data_input": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Data Input", "dynamic": false, "info": "The Data object or list of Data objects to process", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -72016,7 +72070,7 @@ }, "Listen": { "base_classes": [ - "Data" + "JSON" ], "beta": true, "conditional_paths": [], @@ -72032,7 +72086,7 @@ "icon": "Radio", "legacy": false, "metadata": { - "code_hash": "93fc11377c96", + "code_hash": "7f4e3f36b7e2", "dependencies": { "dependencies": [ { @@ -72050,14 +72104,14 @@ { "allows_loop": false, "cache": false, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "listen_for_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -72081,7 +72135,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom import Component\nfrom lfx.io import Output, StrInput\nfrom lfx.schema.data import Data\n\n\nclass ListenComponent(Component):\n display_name = \"Listen\"\n description = \"A component to listen for a notification.\"\n name = \"Listen\"\n beta: bool = True\n icon = \"Radio\"\n inputs = [\n StrInput(\n name=\"context_key\",\n display_name=\"Context Key\",\n info=\"The key of the context to listen for.\",\n input_types=[\"Message\"],\n required=True,\n )\n ]\n\n outputs = [Output(name=\"data\", display_name=\"Data\", method=\"listen_for_data\", cache=False)]\n\n def listen_for_data(self) -> Data:\n \"\"\"Retrieves a Data object from the component context using the provided context key.\n\n If the specified context key does not exist in the context, returns an empty Data object.\n \"\"\"\n return self.ctx.get(self.context_key, Data(text=\"\"))\n" + "value": "from lfx.custom import Component\nfrom lfx.io import Output, StrInput\nfrom lfx.schema.data import Data\n\n\nclass ListenComponent(Component):\n display_name = \"Listen\"\n description = \"A component to listen for a notification.\"\n name = \"Listen\"\n beta: bool = True\n icon = \"Radio\"\n inputs = [\n StrInput(\n name=\"context_key\",\n display_name=\"Context Key\",\n info=\"The key of the context to listen for.\",\n input_types=[\"Message\"],\n required=True,\n )\n ]\n\n outputs = [Output(name=\"data\", display_name=\"JSON\", method=\"listen_for_data\", cache=False)]\n\n def listen_for_data(self) -> Data:\n \"\"\"Retrieves a Data object from the component context using the provided context key.\n\n If the specified context key does not exist in the context, returns an empty Data object.\n \"\"\"\n return self.ctx.get(self.context_key, Data(text=\"\"))\n" }, "context_key": { "_input_type": "StrInput", @@ -72112,8 +72166,8 @@ }, "LoopComponent": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -72129,7 +72183,7 @@ "icon": "infinity", "legacy": false, "metadata": { - "code_hash": "e516ea99611c", + "code_hash": "f789817c7cd3", "dependencies": { "dependencies": [ { @@ -72154,10 +72208,10 @@ ], "method": "item_output", "name": "item", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -72168,10 +72222,10 @@ "group_outputs": true, "method": "done_output", "name": "done", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -72195,7 +72249,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.flow_controls.loop_utils import (\n execute_loop_body,\n extract_loop_output,\n get_loop_body_start_edge,\n get_loop_body_start_vertex,\n get_loop_body_vertices,\n validate_data_input,\n)\nfrom lfx.components.processing.converter import convert_to_data\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass LoopComponent(Component):\n display_name = \"Loop\"\n description = (\n \"Iterates through Data or Message objects, processing items individually \"\n \"and aggregating results from loop inputs.\"\n )\n documentation: str = \"https://docs.langflow.org/loop\"\n icon = \"infinity\"\n\n inputs = [\n HandleInput(\n name=\"data\",\n display_name=\"Inputs\",\n info=\"The initial DataFrame to iterate over.\",\n input_types=[\"DataFrame\"],\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Item\",\n name=\"item\",\n method=\"item_output\",\n allows_loop=True,\n loop_types=[\"Message\"],\n group_outputs=True,\n ),\n Output(display_name=\"Done\", name=\"done\", method=\"done_output\", group_outputs=True),\n ]\n\n def initialize_data(self) -> None:\n \"\"\"Initialize the data list, context index, and aggregated list.\"\"\"\n if self.ctx.get(f\"{self._id}_initialized\", False):\n return\n\n # Ensure data is a list of Data objects\n data_list = self._validate_data(self.data)\n\n # Store the initial data and context variables\n self.update_ctx(\n {\n f\"{self._id}_data\": data_list,\n f\"{self._id}_index\": 0,\n f\"{self._id}_aggregated\": [],\n f\"{self._id}_initialized\": True,\n }\n )\n\n def _convert_message_to_data(self, message: Message) -> Data:\n \"\"\"Convert a Message object to a Data object using Type Convert logic.\"\"\"\n return convert_to_data(message, auto_parse=False)\n\n def _validate_data(self, data):\n \"\"\"Validate and return a list of Data objects.\"\"\"\n return validate_data_input(data)\n\n def get_loop_body_vertices(self) -> set[str]:\n \"\"\"Identify vertices in this loop's body via graph traversal.\n\n Traverses from the loop's \"item\" output to the vertex that feeds back\n to the loop's \"item\" input, collecting all vertices in between.\n This naturally handles nested loops by stopping at this loop's feedback edge.\n\n Returns:\n Set of vertex IDs that form this loop's body\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return set()\n\n return get_loop_body_vertices(\n vertex=self._vertex,\n graph=self.graph,\n get_incoming_edge_by_target_param_fn=self.get_incoming_edge_by_target_param,\n )\n\n def _get_loop_body_start_vertex(self) -> str | None:\n \"\"\"Get the first vertex in the loop body (connected to loop's item output).\n\n Returns:\n The vertex ID of the first vertex in the loop body, or None if not found\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return None\n\n return get_loop_body_start_vertex(vertex=self._vertex)\n\n def _extract_loop_output(self, results: list) -> Data:\n \"\"\"Extract the output from subgraph execution results.\n\n Args:\n results: List of VertexBuildResult objects from subgraph execution\n\n Returns:\n Data object containing the loop iteration output\n \"\"\"\n # Get the vertex ID that feeds back to the item input (end of loop body)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n return extract_loop_output(results=results, end_vertex_id=end_vertex_id)\n\n async def execute_loop_body(self, data_list: list[Data], event_manager=None) -> list[Data]:\n \"\"\"Execute loop body for each data item.\n\n Creates an isolated subgraph for the loop body and executes it\n for each item in the data list, collecting results.\n\n Args:\n data_list: List of Data objects to iterate over\n event_manager: Optional event manager to pass to subgraph execution for UI events\n\n Returns:\n List of Data objects containing results from each iteration\n \"\"\"\n # Get the loop body configuration once\n loop_body_vertex_ids = self.get_loop_body_vertices()\n start_vertex_id = self._get_loop_body_start_vertex()\n start_edge = get_loop_body_start_edge(self._vertex)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n\n return await execute_loop_body(\n graph=self.graph,\n data_list=data_list,\n loop_body_vertex_ids=loop_body_vertex_ids,\n start_vertex_id=start_vertex_id,\n start_edge=start_edge,\n end_vertex_id=end_vertex_id,\n event_manager=event_manager,\n )\n\n def item_output(self) -> Data:\n \"\"\"Output is no longer used - loop executes internally now.\n\n This method is kept for backward compatibility but does nothing.\n The actual loop execution happens in done_output().\n \"\"\"\n self.stop(\"item\")\n return Data(text=\"\")\n\n async def done_output(self) -> DataFrame:\n \"\"\"Execute the loop body for all items and return aggregated results.\n\n This is now the main execution point for the loop. It:\n 1. Gets the data list to iterate over\n 2. Executes the loop body as an isolated subgraph for each item\n 3. Returns the aggregated results\n\n Args:\n event_manager: Optional event manager for UI event emission\n \"\"\"\n self.initialize_data()\n\n # Get data list\n data_list = self.ctx.get(f\"{self._id}_data\", [])\n\n if not data_list:\n return DataFrame([])\n\n # Execute loop body for all items\n try:\n aggregated_results = await self.execute_loop_body(data_list, event_manager=self._event_manager)\n return DataFrame(aggregated_results)\n except Exception as e:\n # Log error and return empty DataFrame\n from lfx.log.logger import logger\n\n await logger.aerror(f\"Error executing loop body: {e}\")\n raise\n" + "value": "from lfx.base.flow_controls.loop_utils import (\n execute_loop_body,\n extract_loop_output,\n get_loop_body_start_edge,\n get_loop_body_start_vertex,\n get_loop_body_vertices,\n validate_data_input,\n)\nfrom lfx.components.processing.converter import convert_to_data\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass LoopComponent(Component):\n display_name = \"Loop\"\n description = (\n \"Iterates through Data or Message objects, processing items individually \"\n \"and aggregating results from loop inputs.\"\n )\n documentation: str = \"https://docs.langflow.org/loop\"\n icon = \"infinity\"\n\n inputs = [\n HandleInput(\n name=\"data\",\n display_name=\"Inputs\",\n info=\"The initial DataFrame to iterate over.\",\n input_types=[\"DataFrame\", \"Table\"],\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Item\",\n name=\"item\",\n method=\"item_output\",\n allows_loop=True,\n loop_types=[\"Message\"],\n group_outputs=True,\n ),\n Output(display_name=\"Done\", name=\"done\", method=\"done_output\", group_outputs=True),\n ]\n\n def initialize_data(self) -> None:\n \"\"\"Initialize the data list, context index, and aggregated list.\"\"\"\n if self.ctx.get(f\"{self._id}_initialized\", False):\n return\n\n # Ensure data is a list of Data objects\n data_list = self._validate_data(self.data)\n\n # Store the initial data and context variables\n self.update_ctx(\n {\n f\"{self._id}_data\": data_list,\n f\"{self._id}_index\": 0,\n f\"{self._id}_aggregated\": [],\n f\"{self._id}_initialized\": True,\n }\n )\n\n def _convert_message_to_data(self, message: Message) -> Data:\n \"\"\"Convert a Message object to a Data object using Type Convert logic.\"\"\"\n return convert_to_data(message, auto_parse=False)\n\n def _validate_data(self, data):\n \"\"\"Validate and return a list of Data objects.\"\"\"\n return validate_data_input(data)\n\n def get_loop_body_vertices(self) -> set[str]:\n \"\"\"Identify vertices in this loop's body via graph traversal.\n\n Traverses from the loop's \"item\" output to the vertex that feeds back\n to the loop's \"item\" input, collecting all vertices in between.\n This naturally handles nested loops by stopping at this loop's feedback edge.\n\n Returns:\n Set of vertex IDs that form this loop's body\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return set()\n\n return get_loop_body_vertices(\n vertex=self._vertex,\n graph=self.graph,\n get_incoming_edge_by_target_param_fn=self.get_incoming_edge_by_target_param,\n )\n\n def _get_loop_body_start_vertex(self) -> str | None:\n \"\"\"Get the first vertex in the loop body (connected to loop's item output).\n\n Returns:\n The vertex ID of the first vertex in the loop body, or None if not found\n \"\"\"\n # Check if we have a proper graph context\n if not hasattr(self, \"_vertex\") or self._vertex is None:\n return None\n\n return get_loop_body_start_vertex(vertex=self._vertex)\n\n def _extract_loop_output(self, results: list) -> Data:\n \"\"\"Extract the output from subgraph execution results.\n\n Args:\n results: List of VertexBuildResult objects from subgraph execution\n\n Returns:\n Data object containing the loop iteration output\n \"\"\"\n # Get the vertex ID that feeds back to the item input (end of loop body)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n return extract_loop_output(results=results, end_vertex_id=end_vertex_id)\n\n async def execute_loop_body(self, data_list: list[Data], event_manager=None) -> list[Data]:\n \"\"\"Execute loop body for each data item.\n\n Creates an isolated subgraph for the loop body and executes it\n for each item in the data list, collecting results.\n\n Args:\n data_list: List of Data objects to iterate over\n event_manager: Optional event manager to pass to subgraph execution for UI events\n\n Returns:\n List of Data objects containing results from each iteration\n \"\"\"\n # Get the loop body configuration once\n loop_body_vertex_ids = self.get_loop_body_vertices()\n start_vertex_id = self._get_loop_body_start_vertex()\n start_edge = get_loop_body_start_edge(self._vertex)\n end_vertex_id = self.get_incoming_edge_by_target_param(\"item\")\n\n return await execute_loop_body(\n graph=self.graph,\n data_list=data_list,\n loop_body_vertex_ids=loop_body_vertex_ids,\n start_vertex_id=start_vertex_id,\n start_edge=start_edge,\n end_vertex_id=end_vertex_id,\n event_manager=event_manager,\n )\n\n def item_output(self) -> Data:\n \"\"\"Output is no longer used - loop executes internally now.\n\n This method is kept for backward compatibility but does nothing.\n The actual loop execution happens in done_output().\n \"\"\"\n self.stop(\"item\")\n return Data(text=\"\")\n\n async def done_output(self) -> DataFrame:\n \"\"\"Execute the loop body for all items and return aggregated results.\n\n This is now the main execution point for the loop. It:\n 1. Gets the data list to iterate over\n 2. Executes the loop body as an isolated subgraph for each item\n 3. Returns the aggregated results\n\n Args:\n event_manager: Optional event manager for UI event emission\n \"\"\"\n self.initialize_data()\n\n # Get data list\n data_list = self.ctx.get(f\"{self._id}_data\", [])\n\n if not data_list:\n return DataFrame([])\n\n # Execute loop body for all items\n try:\n aggregated_results = await self.execute_loop_body(data_list, event_manager=self._event_manager)\n return DataFrame(aggregated_results)\n except Exception as e:\n # Log error and return empty DataFrame\n from lfx.log.logger import logger\n\n await logger.aerror(f\"Error executing loop body: {e}\")\n raise\n" }, "data": { "_input_type": "HandleInput", @@ -72204,7 +72258,8 @@ "dynamic": false, "info": "The initial DataFrame to iterate over.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -72224,7 +72279,7 @@ }, "Notify": { "base_classes": [ - "Data" + "JSON" ], "beta": true, "conditional_paths": [], @@ -72242,7 +72297,7 @@ "icon": "Notify", "legacy": false, "metadata": { - "code_hash": "03d68ba28530", + "code_hash": "a22284d4b01e", "dependencies": { "dependencies": [ { @@ -72260,14 +72315,14 @@ { "allows_loop": false, "cache": false, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "notify_components", "name": "result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -72311,7 +72366,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import cast\n\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, HandleInput, Output, StrInput\nfrom lfx.schema.data import Data\n\n\nclass NotifyComponent(Component):\n display_name = \"Notify\"\n description = \"A component to generate a notification to Get Notified component.\"\n icon = \"Notify\"\n name = \"Notify\"\n beta: bool = True\n\n inputs = [\n StrInput(\n name=\"context_key\",\n display_name=\"Context Key\",\n info=\"The key of the context to store the notification.\",\n required=True,\n ),\n HandleInput(\n name=\"input_value\",\n display_name=\"Input Data\",\n info=\"The data to store.\",\n required=False,\n input_types=[\"Data\", \"Message\", \"DataFrame\"],\n ),\n BoolInput(\n name=\"append\",\n display_name=\"Append\",\n info=\"If True, the record will be appended to the notification.\",\n value=False,\n required=False,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Data\",\n name=\"result\",\n method=\"notify_components\",\n cache=False,\n ),\n ]\n\n async def notify_components(self) -> Data:\n \"\"\"Processes and stores a notification in the component's context.\n\n Normalizes the input value to a `Data` object and stores it under the\n specified context key. If `append` is True, adds the value to a list\n of notifications; otherwise, replaces the existing value. Updates the\n component's status and activates related state vertices in the graph.\n\n Returns:\n The processed `Data` object stored in the context.\n\n Raises:\n ValueError: If the component is not part of a graph.\n \"\"\"\n if not self._vertex:\n msg = \"Notify component must be used in a graph.\"\n raise ValueError(msg)\n input_value: Data | str | dict | None = self.input_value\n if input_value is None:\n input_value = Data(text=\"\")\n elif not isinstance(input_value, Data):\n if isinstance(input_value, str):\n input_value = Data(text=input_value)\n elif isinstance(input_value, dict):\n input_value = Data(data=input_value)\n else:\n input_value = Data(text=str(input_value))\n if input_value:\n if self.append:\n current_data = self.ctx.get(self.context_key, [])\n if not isinstance(current_data, list):\n current_data = [current_data]\n current_data.append(input_value)\n self.update_ctx({self.context_key: current_data})\n else:\n self.update_ctx({self.context_key: input_value})\n self.status = input_value\n else:\n self.status = \"No record provided.\"\n self._vertex.is_state = True\n self.graph.activate_state_vertices(name=self.context_key, caller=self._id)\n return cast(\"Data\", input_value)\n" + "value": "from typing import cast\n\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, HandleInput, Output, StrInput\nfrom lfx.schema.data import Data\n\n\nclass NotifyComponent(Component):\n display_name = \"Notify\"\n description = \"A component to generate a notification to Get Notified component.\"\n icon = \"Notify\"\n name = \"Notify\"\n beta: bool = True\n\n inputs = [\n StrInput(\n name=\"context_key\",\n display_name=\"Context Key\",\n info=\"The key of the context to store the notification.\",\n required=True,\n ),\n HandleInput(\n name=\"input_value\",\n display_name=\"Input Data\",\n info=\"The data to store.\",\n required=False,\n input_types=[\"Data\", \"JSON\", \"Message\", \"DataFrame\", \"Table\"],\n ),\n BoolInput(\n name=\"append\",\n display_name=\"Append\",\n info=\"If True, the record will be appended to the notification.\",\n value=False,\n required=False,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"JSON\",\n name=\"result\",\n method=\"notify_components\",\n cache=False,\n ),\n ]\n\n async def notify_components(self) -> Data:\n \"\"\"Processes and stores a notification in the component's context.\n\n Normalizes the input value to a `Data` object and stores it under the\n specified context key. If `append` is True, adds the value to a list\n of notifications; otherwise, replaces the existing value. Updates the\n component's status and activates related state vertices in the graph.\n\n Returns:\n The processed `Data` object stored in the context.\n\n Raises:\n ValueError: If the component is not part of a graph.\n \"\"\"\n if not self._vertex:\n msg = \"Notify component must be used in a graph.\"\n raise ValueError(msg)\n input_value: Data | str | dict | None = self.input_value\n if input_value is None:\n input_value = Data(text=\"\")\n elif not isinstance(input_value, Data):\n if isinstance(input_value, str):\n input_value = Data(text=input_value)\n elif isinstance(input_value, dict):\n input_value = Data(data=input_value)\n else:\n input_value = Data(text=str(input_value))\n if input_value:\n if self.append:\n current_data = self.ctx.get(self.context_key, [])\n if not isinstance(current_data, list):\n current_data = [current_data]\n current_data.append(input_value)\n self.update_ctx({self.context_key: current_data})\n else:\n self.update_ctx({self.context_key: input_value})\n self.status = input_value\n else:\n self.status = \"No record provided.\"\n self._vertex.is_state = True\n self.graph.activate_state_vertices(name=self.context_key, caller=self._id)\n return cast(\"Data\", input_value)\n" }, "context_key": { "_input_type": "StrInput", @@ -72342,8 +72397,10 @@ "info": "The data to store.", "input_types": [ "Data", + "JSON", "Message", - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -72637,7 +72694,7 @@ }, "SubFlow": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -72675,10 +72732,10 @@ "group_outputs": false, "method": "generate_results", "name": "flow_outputs", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -72743,7 +72800,7 @@ { "GitExtractorComponent": { "base_classes": [ - "Data", + "JSON", "Message" ], "beta": false, @@ -72818,10 +72875,10 @@ "group_outputs": false, "method": "get_repository_info", "name": "repository_info", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -72832,10 +72889,10 @@ "group_outputs": false, "method": "get_statistics", "name": "statistics", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -72846,10 +72903,10 @@ "group_outputs": false, "method": "get_files_content", "name": "files_content", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -72905,7 +72962,7 @@ }, "GitLoaderComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -72926,7 +72983,7 @@ "icon": "GitLoader", "legacy": false, "metadata": { - "code_hash": "ac5de0564a4f", + "code_hash": "7797832cc23c", "dependencies": { "dependencies": [ { @@ -72952,14 +73009,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "load_documents", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -73033,7 +73090,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import re\nimport tempfile\nfrom contextlib import asynccontextmanager\nfrom fnmatch import fnmatch\nfrom pathlib import Path\n\nimport anyio\nfrom langchain_community.document_loaders.git import GitLoader\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DropdownInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\n\n\nclass GitLoaderComponent(Component):\n display_name = \"Git\"\n description = (\n \"Load and filter documents from a local or remote Git repository. \"\n \"Use a local repo path or clone from a remote URL.\"\n )\n trace_type = \"tool\"\n icon = \"GitLoader\"\n\n inputs = [\n DropdownInput(\n name=\"repo_source\",\n display_name=\"Repository Source\",\n options=[\"Local\", \"Remote\"],\n required=True,\n info=\"Select whether to use a local repo path or clone from a remote URL.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"repo_path\",\n display_name=\"Local Repository Path\",\n required=False,\n info=\"The local path to the existing Git repository (used if 'Local' is selected).\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"clone_url\",\n display_name=\"Clone URL\",\n required=False,\n info=\"The URL of the Git repository to clone (used if 'Clone' is selected).\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"branch\",\n display_name=\"Branch\",\n required=False,\n value=\"main\",\n info=\"The branch to load files from. Defaults to 'main'.\",\n ),\n MessageTextInput(\n name=\"file_filter\",\n display_name=\"File Filter\",\n required=False,\n advanced=True,\n info=(\n \"Patterns to filter files. For example:\\n\"\n \"Include only .py files: '*.py'\\n\"\n \"Exclude .py files: '!*.py'\\n\"\n \"Multiple patterns can be separated by commas.\"\n ),\n ),\n MessageTextInput(\n name=\"content_filter\",\n display_name=\"Content Filter\",\n required=False,\n advanced=True,\n info=\"A regex pattern to filter files based on their content.\",\n ),\n ]\n\n outputs = [\n Output(name=\"data\", display_name=\"Data\", method=\"load_documents\"),\n ]\n\n @staticmethod\n def is_binary(file_path: str | Path) -> bool:\n \"\"\"Check if a file is binary by looking for null bytes.\"\"\"\n try:\n with Path(file_path).open(\"rb\") as file:\n content = file.read(1024)\n return b\"\\x00\" in content\n except Exception: # noqa: BLE001\n return True\n\n @staticmethod\n def check_file_patterns(file_path: str | Path, patterns: str) -> bool:\n \"\"\"Check if a file matches the given patterns.\n\n Args:\n file_path: Path to the file to check\n patterns: Comma-separated list of glob patterns\n\n Returns:\n bool: True if file should be included, False if excluded\n \"\"\"\n # Handle empty or whitespace-only patterns\n if not patterns or patterns.isspace():\n return True\n\n path_str = str(file_path)\n file_name = Path(path_str).name\n pattern_list: list[str] = [pattern.strip() for pattern in patterns.split(\",\") if pattern.strip()]\n\n # If no valid patterns after stripping, treat as include all\n if not pattern_list:\n return True\n\n # Process exclusion patterns first\n for pattern in pattern_list:\n if pattern.startswith(\"!\"):\n # For exclusions, match against both full path and filename\n exclude_pattern = pattern[1:]\n if fnmatch(path_str, exclude_pattern) or fnmatch(file_name, exclude_pattern):\n return False\n\n # Then check inclusion patterns\n include_patterns = [p for p in pattern_list if not p.startswith(\"!\")]\n # If no include patterns, treat as include all\n if not include_patterns:\n return True\n\n # For inclusions, match against both full path and filename\n return any(fnmatch(path_str, pattern) or fnmatch(file_name, pattern) for pattern in include_patterns)\n\n @staticmethod\n def check_content_pattern(file_path: str | Path, pattern: str) -> bool:\n \"\"\"Check if file content matches the given regex pattern.\n\n Args:\n file_path: Path to the file to check\n pattern: Regex pattern to match against content\n\n Returns:\n bool: True if content matches, False otherwise\n \"\"\"\n try:\n # Check if file is binary\n with Path(file_path).open(\"rb\") as file:\n content = file.read(1024)\n if b\"\\x00\" in content:\n return False\n\n # Try to compile the regex pattern first\n try:\n # Use the MULTILINE flag to better handle text content\n content_regex = re.compile(pattern, re.MULTILINE)\n # Test the pattern with a simple string to catch syntax errors\n test_str = \"test\\nstring\"\n if not content_regex.search(test_str):\n # Pattern is valid but doesn't match test string\n pass\n except (re.error, TypeError, ValueError):\n return False\n\n # If not binary and regex is valid, check content\n with Path(file_path).open(encoding=\"utf-8\") as file:\n file_content = file.read()\n return bool(content_regex.search(file_content))\n except (OSError, UnicodeDecodeError):\n return False\n\n def build_combined_filter(self, file_filter_patterns: str | None = None, content_filter_pattern: str | None = None):\n \"\"\"Build a combined filter function from file and content patterns.\n\n Args:\n file_filter_patterns: Comma-separated glob patterns\n content_filter_pattern: Regex pattern for content\n\n Returns:\n callable: Filter function that takes a file path and returns bool\n \"\"\"\n\n def combined_filter(file_path: str) -> bool:\n try:\n path = Path(file_path)\n\n # Check if file exists and is readable\n if not path.exists():\n return False\n\n # Check if file is binary\n if self.is_binary(path):\n return False\n\n # Apply file pattern filters\n if file_filter_patterns and not self.check_file_patterns(path, file_filter_patterns):\n return False\n\n # Apply content filter\n return not (content_filter_pattern and not self.check_content_pattern(path, content_filter_pattern))\n except Exception: # noqa: BLE001\n return False\n\n return combined_filter\n\n @asynccontextmanager\n async def temp_clone_dir(self):\n \"\"\"Context manager for handling temporary clone directory.\"\"\"\n temp_dir = None\n try:\n temp_dir = tempfile.mkdtemp(prefix=\"langflow_clone_\")\n yield temp_dir\n finally:\n if temp_dir:\n await anyio.Path(temp_dir).rmdir()\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n # Hide fields by default\n build_config[\"repo_path\"][\"show\"] = False\n build_config[\"clone_url\"][\"show\"] = False\n\n if field_name == \"repo_source\":\n if field_value == \"Local\":\n build_config[\"repo_path\"][\"show\"] = True\n build_config[\"repo_path\"][\"required\"] = True\n build_config[\"clone_url\"][\"required\"] = False\n elif field_value == \"Remote\":\n build_config[\"clone_url\"][\"show\"] = True\n build_config[\"clone_url\"][\"required\"] = True\n build_config[\"repo_path\"][\"required\"] = False\n\n return build_config\n\n async def build_gitloader(self) -> GitLoader:\n file_filter_patterns = getattr(self, \"file_filter\", None)\n content_filter_pattern = getattr(self, \"content_filter\", None)\n\n combined_filter = self.build_combined_filter(file_filter_patterns, content_filter_pattern)\n\n repo_source = getattr(self, \"repo_source\", None)\n if repo_source == \"Local\":\n repo_path = self.repo_path\n clone_url = None\n else:\n # Clone source\n clone_url = self.clone_url\n async with self.temp_clone_dir() as temp_dir:\n repo_path = temp_dir\n\n # Only pass branch if it's explicitly set\n branch = getattr(self, \"branch\", None)\n if not branch:\n branch = None\n\n return GitLoader(\n repo_path=repo_path,\n clone_url=clone_url if repo_source == \"Remote\" else None,\n branch=branch,\n file_filter=combined_filter,\n )\n\n async def load_documents(self) -> list[Data]:\n gitloader = await self.build_gitloader()\n data = [Data.from_document(doc) async for doc in gitloader.alazy_load()]\n self.status = data\n return data\n" + "value": "import re\nimport tempfile\nfrom contextlib import asynccontextmanager\nfrom fnmatch import fnmatch\nfrom pathlib import Path\n\nimport anyio\nfrom langchain_community.document_loaders.git import GitLoader\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DropdownInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\n\n\nclass GitLoaderComponent(Component):\n display_name = \"Git\"\n description = (\n \"Load and filter documents from a local or remote Git repository. \"\n \"Use a local repo path or clone from a remote URL.\"\n )\n trace_type = \"tool\"\n icon = \"GitLoader\"\n\n inputs = [\n DropdownInput(\n name=\"repo_source\",\n display_name=\"Repository Source\",\n options=[\"Local\", \"Remote\"],\n required=True,\n info=\"Select whether to use a local repo path or clone from a remote URL.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"repo_path\",\n display_name=\"Local Repository Path\",\n required=False,\n info=\"The local path to the existing Git repository (used if 'Local' is selected).\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"clone_url\",\n display_name=\"Clone URL\",\n required=False,\n info=\"The URL of the Git repository to clone (used if 'Clone' is selected).\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"branch\",\n display_name=\"Branch\",\n required=False,\n value=\"main\",\n info=\"The branch to load files from. Defaults to 'main'.\",\n ),\n MessageTextInput(\n name=\"file_filter\",\n display_name=\"File Filter\",\n required=False,\n advanced=True,\n info=(\n \"Patterns to filter files. For example:\\n\"\n \"Include only .py files: '*.py'\\n\"\n \"Exclude .py files: '!*.py'\\n\"\n \"Multiple patterns can be separated by commas.\"\n ),\n ),\n MessageTextInput(\n name=\"content_filter\",\n display_name=\"Content Filter\",\n required=False,\n advanced=True,\n info=\"A regex pattern to filter files based on their content.\",\n ),\n ]\n\n outputs = [\n Output(name=\"data\", display_name=\"JSON\", method=\"load_documents\"),\n ]\n\n @staticmethod\n def is_binary(file_path: str | Path) -> bool:\n \"\"\"Check if a file is binary by looking for null bytes.\"\"\"\n try:\n with Path(file_path).open(\"rb\") as file:\n content = file.read(1024)\n return b\"\\x00\" in content\n except Exception: # noqa: BLE001\n return True\n\n @staticmethod\n def check_file_patterns(file_path: str | Path, patterns: str) -> bool:\n \"\"\"Check if a file matches the given patterns.\n\n Args:\n file_path: Path to the file to check\n patterns: Comma-separated list of glob patterns\n\n Returns:\n bool: True if file should be included, False if excluded\n \"\"\"\n # Handle empty or whitespace-only patterns\n if not patterns or patterns.isspace():\n return True\n\n path_str = str(file_path)\n file_name = Path(path_str).name\n pattern_list: list[str] = [pattern.strip() for pattern in patterns.split(\",\") if pattern.strip()]\n\n # If no valid patterns after stripping, treat as include all\n if not pattern_list:\n return True\n\n # Process exclusion patterns first\n for pattern in pattern_list:\n if pattern.startswith(\"!\"):\n # For exclusions, match against both full path and filename\n exclude_pattern = pattern[1:]\n if fnmatch(path_str, exclude_pattern) or fnmatch(file_name, exclude_pattern):\n return False\n\n # Then check inclusion patterns\n include_patterns = [p for p in pattern_list if not p.startswith(\"!\")]\n # If no include patterns, treat as include all\n if not include_patterns:\n return True\n\n # For inclusions, match against both full path and filename\n return any(fnmatch(path_str, pattern) or fnmatch(file_name, pattern) for pattern in include_patterns)\n\n @staticmethod\n def check_content_pattern(file_path: str | Path, pattern: str) -> bool:\n \"\"\"Check if file content matches the given regex pattern.\n\n Args:\n file_path: Path to the file to check\n pattern: Regex pattern to match against content\n\n Returns:\n bool: True if content matches, False otherwise\n \"\"\"\n try:\n # Check if file is binary\n with Path(file_path).open(\"rb\") as file:\n content = file.read(1024)\n if b\"\\x00\" in content:\n return False\n\n # Try to compile the regex pattern first\n try:\n # Use the MULTILINE flag to better handle text content\n content_regex = re.compile(pattern, re.MULTILINE)\n # Test the pattern with a simple string to catch syntax errors\n test_str = \"test\\nstring\"\n if not content_regex.search(test_str):\n # Pattern is valid but doesn't match test string\n pass\n except (re.error, TypeError, ValueError):\n return False\n\n # If not binary and regex is valid, check content\n with Path(file_path).open(encoding=\"utf-8\") as file:\n file_content = file.read()\n return bool(content_regex.search(file_content))\n except (OSError, UnicodeDecodeError):\n return False\n\n def build_combined_filter(self, file_filter_patterns: str | None = None, content_filter_pattern: str | None = None):\n \"\"\"Build a combined filter function from file and content patterns.\n\n Args:\n file_filter_patterns: Comma-separated glob patterns\n content_filter_pattern: Regex pattern for content\n\n Returns:\n callable: Filter function that takes a file path and returns bool\n \"\"\"\n\n def combined_filter(file_path: str) -> bool:\n try:\n path = Path(file_path)\n\n # Check if file exists and is readable\n if not path.exists():\n return False\n\n # Check if file is binary\n if self.is_binary(path):\n return False\n\n # Apply file pattern filters\n if file_filter_patterns and not self.check_file_patterns(path, file_filter_patterns):\n return False\n\n # Apply content filter\n return not (content_filter_pattern and not self.check_content_pattern(path, content_filter_pattern))\n except Exception: # noqa: BLE001\n return False\n\n return combined_filter\n\n @asynccontextmanager\n async def temp_clone_dir(self):\n \"\"\"Context manager for handling temporary clone directory.\"\"\"\n temp_dir = None\n try:\n temp_dir = tempfile.mkdtemp(prefix=\"langflow_clone_\")\n yield temp_dir\n finally:\n if temp_dir:\n await anyio.Path(temp_dir).rmdir()\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n # Hide fields by default\n build_config[\"repo_path\"][\"show\"] = False\n build_config[\"clone_url\"][\"show\"] = False\n\n if field_name == \"repo_source\":\n if field_value == \"Local\":\n build_config[\"repo_path\"][\"show\"] = True\n build_config[\"repo_path\"][\"required\"] = True\n build_config[\"clone_url\"][\"required\"] = False\n elif field_value == \"Remote\":\n build_config[\"clone_url\"][\"show\"] = True\n build_config[\"clone_url\"][\"required\"] = True\n build_config[\"repo_path\"][\"required\"] = False\n\n return build_config\n\n async def build_gitloader(self) -> GitLoader:\n file_filter_patterns = getattr(self, \"file_filter\", None)\n content_filter_pattern = getattr(self, \"content_filter\", None)\n\n combined_filter = self.build_combined_filter(file_filter_patterns, content_filter_pattern)\n\n repo_source = getattr(self, \"repo_source\", None)\n if repo_source == \"Local\":\n repo_path = self.repo_path\n clone_url = None\n else:\n # Clone source\n clone_url = self.clone_url\n async with self.temp_clone_dir() as temp_dir:\n repo_path = temp_dir\n\n # Only pass branch if it's explicitly set\n branch = getattr(self, \"branch\", None)\n if not branch:\n branch = None\n\n return GitLoader(\n repo_path=repo_path,\n clone_url=clone_url if repo_source == \"Remote\" else None,\n branch=branch,\n file_filter=combined_filter,\n )\n\n async def load_documents(self) -> list[Data]:\n gitloader = await self.build_gitloader()\n data = [Data.from_document(doc) async for doc in gitloader.alazy_load()]\n self.status = data\n return data\n" }, "content_filter": { "_input_type": "MessageTextInput", @@ -73148,7 +73205,7 @@ { "GleanSearchAPIComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -73168,7 +73225,7 @@ "icon": "Glean", "legacy": false, "metadata": { - "code_hash": "493ca281d420", + "code_hash": "469618609b03", "dependencies": { "dependencies": [ { @@ -73198,14 +73255,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -73229,7 +73286,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nfrom langchain_core.tools import StructuredTool, ToolException\nfrom pydantic import BaseModel\nfrom pydantic.v1 import Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import IntInput, MultilineInput, NestedDictInput, SecretStrInput, StrInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass GleanSearchAPISchema(BaseModel):\n query: str = Field(..., description=\"The search query\")\n page_size: int = Field(10, description=\"Maximum number of results to return\")\n request_options: dict[str, Any] | None = Field(default_factory=dict, description=\"Request Options\")\n\n\nclass GleanAPIWrapper(BaseModel):\n \"\"\"Wrapper around Glean API.\"\"\"\n\n glean_api_url: str\n glean_access_token: str\n act_as: str = \"langflow-component@datastax.com\" # TODO: Detect this\n\n def _prepare_request(\n self,\n query: str,\n page_size: int = 10,\n request_options: dict[str, Any] | None = None,\n ) -> dict:\n # Ensure there's a trailing slash\n url = self.glean_api_url\n if not url.endswith(\"/\"):\n url += \"/\"\n\n return {\n \"url\": urljoin(url, \"search\"),\n \"headers\": {\n \"Authorization\": f\"Bearer {self.glean_access_token}\",\n \"X-Scio-ActAs\": self.act_as,\n },\n \"payload\": {\n \"query\": query,\n \"pageSize\": page_size,\n \"requestOptions\": request_options,\n },\n }\n\n def results(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:\n results = self._search_api_results(query, **kwargs)\n\n if len(results) == 0:\n msg = \"No good Glean Search Result was found\"\n raise AssertionError(msg)\n\n return results\n\n def run(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:\n try:\n results = self.results(query, **kwargs)\n\n processed_results = []\n for result in results:\n if \"title\" in result:\n result[\"snippets\"] = result.get(\"snippets\", [{\"snippet\": {\"text\": result[\"title\"]}}])\n if \"text\" not in result[\"snippets\"][0]:\n result[\"snippets\"][0][\"text\"] = result[\"title\"]\n\n processed_results.append(result)\n except Exception as e:\n error_message = f\"Error in Glean Search API: {e!s}\"\n raise ToolException(error_message) from e\n\n return processed_results\n\n def _search_api_results(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:\n request_details = self._prepare_request(query, **kwargs)\n\n response = httpx.post(\n request_details[\"url\"],\n json=request_details[\"payload\"],\n headers=request_details[\"headers\"],\n )\n\n response.raise_for_status()\n response_json = response.json()\n\n return response_json.get(\"results\", [])\n\n @staticmethod\n def _result_as_string(result: dict) -> str:\n return json.dumps(result, indent=4)\n\n\nclass GleanSearchAPIComponent(LCToolComponent):\n display_name: str = \"Glean Search API\"\n description: str = \"Search using Glean's API.\"\n documentation: str = \"https://docs.langflow.org/bundles-glean\"\n icon: str = \"Glean\"\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n inputs = [\n StrInput(name=\"glean_api_url\", display_name=\"Glean API URL\", required=True),\n SecretStrInput(name=\"glean_access_token\", display_name=\"Glean Access Token\", required=True),\n MultilineInput(name=\"query\", display_name=\"Query\", required=True, tool_mode=True),\n IntInput(name=\"page_size\", display_name=\"Page Size\", value=10),\n NestedDictInput(name=\"request_options\", display_name=\"Request Options\", required=False),\n ]\n\n def build_tool(self) -> Tool:\n wrapper = self._build_wrapper(\n glean_api_url=self.glean_api_url,\n glean_access_token=self.glean_access_token,\n )\n\n tool = StructuredTool.from_function(\n name=\"glean_search_api\",\n description=\"Search Glean for relevant results.\",\n func=wrapper.run,\n args_schema=GleanSearchAPISchema,\n )\n\n self.status = \"Glean Search API Tool for Langchain\"\n\n return tool\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n tool = self.build_tool()\n\n results = tool.run(\n {\n \"query\": self.query,\n \"page_size\": self.page_size,\n \"request_options\": self.request_options,\n }\n )\n\n # Build the data\n data = [Data(data=result, text=result[\"snippets\"][0][\"text\"]) for result in results]\n self.status = data # type: ignore[assignment]\n\n return data\n\n def _build_wrapper(\n self,\n glean_api_url: str,\n glean_access_token: str,\n ):\n return GleanAPIWrapper(\n glean_api_url=glean_api_url,\n glean_access_token=glean_access_token,\n )\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the Glean search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import json\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nfrom langchain_core.tools import StructuredTool, ToolException\nfrom pydantic import BaseModel\nfrom pydantic.v1 import Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import IntInput, MultilineInput, NestedDictInput, SecretStrInput, StrInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass GleanSearchAPISchema(BaseModel):\n query: str = Field(..., description=\"The search query\")\n page_size: int = Field(10, description=\"Maximum number of results to return\")\n request_options: dict[str, Any] | None = Field(default_factory=dict, description=\"Request Options\")\n\n\nclass GleanAPIWrapper(BaseModel):\n \"\"\"Wrapper around Glean API.\"\"\"\n\n glean_api_url: str\n glean_access_token: str\n act_as: str = \"langflow-component@datastax.com\" # TODO: Detect this\n\n def _prepare_request(\n self,\n query: str,\n page_size: int = 10,\n request_options: dict[str, Any] | None = None,\n ) -> dict:\n # Ensure there's a trailing slash\n url = self.glean_api_url\n if not url.endswith(\"/\"):\n url += \"/\"\n\n return {\n \"url\": urljoin(url, \"search\"),\n \"headers\": {\n \"Authorization\": f\"Bearer {self.glean_access_token}\",\n \"X-Scio-ActAs\": self.act_as,\n },\n \"payload\": {\n \"query\": query,\n \"pageSize\": page_size,\n \"requestOptions\": request_options,\n },\n }\n\n def results(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:\n results = self._search_api_results(query, **kwargs)\n\n if len(results) == 0:\n msg = \"No good Glean Search Result was found\"\n raise AssertionError(msg)\n\n return results\n\n def run(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:\n try:\n results = self.results(query, **kwargs)\n\n processed_results = []\n for result in results:\n if \"title\" in result:\n result[\"snippets\"] = result.get(\"snippets\", [{\"snippet\": {\"text\": result[\"title\"]}}])\n if \"text\" not in result[\"snippets\"][0]:\n result[\"snippets\"][0][\"text\"] = result[\"title\"]\n\n processed_results.append(result)\n except Exception as e:\n error_message = f\"Error in Glean Search API: {e!s}\"\n raise ToolException(error_message) from e\n\n return processed_results\n\n def _search_api_results(self, query: str, **kwargs: Any) -> list[dict[str, Any]]:\n request_details = self._prepare_request(query, **kwargs)\n\n response = httpx.post(\n request_details[\"url\"],\n json=request_details[\"payload\"],\n headers=request_details[\"headers\"],\n )\n\n response.raise_for_status()\n response_json = response.json()\n\n return response_json.get(\"results\", [])\n\n @staticmethod\n def _result_as_string(result: dict) -> str:\n return json.dumps(result, indent=4)\n\n\nclass GleanSearchAPIComponent(LCToolComponent):\n display_name: str = \"Glean Search API\"\n description: str = \"Search using Glean's API.\"\n documentation: str = \"https://docs.langflow.org/bundles-glean\"\n icon: str = \"Glean\"\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n inputs = [\n StrInput(name=\"glean_api_url\", display_name=\"Glean API URL\", required=True),\n SecretStrInput(name=\"glean_access_token\", display_name=\"Glean Access Token\", required=True),\n MultilineInput(name=\"query\", display_name=\"Query\", required=True, tool_mode=True),\n IntInput(name=\"page_size\", display_name=\"Page Size\", value=10),\n NestedDictInput(name=\"request_options\", display_name=\"Request Options\", required=False),\n ]\n\n def build_tool(self) -> Tool:\n wrapper = self._build_wrapper(\n glean_api_url=self.glean_api_url,\n glean_access_token=self.glean_access_token,\n )\n\n tool = StructuredTool.from_function(\n name=\"glean_search_api\",\n description=\"Search Glean for relevant results.\",\n func=wrapper.run,\n args_schema=GleanSearchAPISchema,\n )\n\n self.status = \"Glean Search API Tool for Langchain\"\n\n return tool\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n tool = self.build_tool()\n\n results = tool.run(\n {\n \"query\": self.query,\n \"page_size\": self.page_size,\n \"request_options\": self.request_options,\n }\n )\n\n # Build the data\n data = [Data(data=result, text=result[\"snippets\"][0][\"text\"]) for result in results]\n self.status = data # type: ignore[assignment]\n\n return data\n\n def _build_wrapper(\n self,\n glean_api_url: str,\n glean_access_token: str,\n ):\n return GleanAPIWrapper(\n glean_api_url=glean_api_url,\n glean_access_token=glean_access_token,\n )\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the Glean search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" }, "glean_access_token": { "_input_type": "SecretStrInput", @@ -73351,7 +73408,7 @@ { "BigQueryExecutor": { "base_classes": [ - "DataFrame" + "Table" ], "beta": true, "conditional_paths": [], @@ -73395,10 +73452,10 @@ "group_outputs": false, "method": "execute_sql", "name": "query_results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -73499,7 +73556,7 @@ }, "GmailLoaderComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -73517,7 +73574,7 @@ "icon": "Google", "legacy": true, "metadata": { - "code_hash": "b973c5a1987b", + "code_hash": "6ef945902cfd", "dependencies": { "dependencies": [ { @@ -73551,14 +73608,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "load_emails", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -73585,7 +73642,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import base64\nimport json\nimport re\nfrom collections.abc import Iterator\nfrom json.decoder import JSONDecodeError\nfrom typing import Any\n\nfrom google.auth.exceptions import RefreshError\nfrom google.oauth2.credentials import Credentials\nfrom googleapiclient.discovery import build\nfrom langchain_core.chat_sessions import ChatSession\nfrom langchain_core.messages import HumanMessage\nfrom langchain_google_community.gmail.loader import GMailLoader\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.template.field.base import Output\n\n\nclass GmailLoaderComponent(Component):\n display_name = \"Gmail Loader\"\n description = \"Loads emails from Gmail using provided credentials.\"\n icon = \"Google\"\n legacy: bool = True\n replacement = [\"composio.ComposioGmailAPIComponent\"]\n\n inputs = [\n SecretStrInput(\n name=\"json_string\",\n display_name=\"JSON String of the Service Account Token\",\n info=\"JSON string containing OAuth 2.0 access token information for service account access\",\n required=True,\n value=\"\"\"{\n \"account\": \"\",\n \"client_id\": \"\",\n \"client_secret\": \"\",\n \"expiry\": \"\",\n \"refresh_token\": \"\",\n \"scopes\": [\n \"https://www.googleapis.com/auth/gmail.readonly\",\n ],\n \"token\": \"\",\n \"token_uri\": \"https://oauth2.googleapis.com/token\",\n \"universe_domain\": \"googleapis.com\"\n }\"\"\",\n ),\n MessageTextInput(\n name=\"label_ids\",\n display_name=\"Label IDs\",\n info=\"Comma-separated list of label IDs to filter emails.\",\n required=True,\n value=\"INBOX,SENT,UNREAD,IMPORTANT\",\n ),\n MessageTextInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"Maximum number of emails to load.\",\n required=True,\n value=\"10\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"load_emails\"),\n ]\n\n def load_emails(self) -> Data:\n class CustomGMailLoader(GMailLoader):\n def __init__(\n self, creds: Any, *, n: int = 100, label_ids: list[str] | None = None, raise_error: bool = False\n ) -> None:\n super().__init__(creds, n, raise_error)\n self.label_ids = label_ids if label_ids is not None else [\"SENT\"]\n\n def clean_message_content(self, message):\n # Remove URLs\n message = re.sub(r\"http\\S+|www\\S+|https\\S+\", \"\", message, flags=re.MULTILINE)\n\n # Remove email addresses\n message = re.sub(r\"\\S+@\\S+\", \"\", message)\n\n # Remove special characters and excessive whitespace\n message = re.sub(r\"[^A-Za-z0-9\\s]+\", \" \", message)\n message = re.sub(r\"\\s{2,}\", \" \", message)\n\n # Trim leading and trailing whitespace\n return message.strip()\n\n def _extract_email_content(self, msg: Any) -> HumanMessage:\n from_email = None\n for values in msg[\"payload\"][\"headers\"]:\n name = values[\"name\"]\n if name == \"From\":\n from_email = values[\"value\"]\n if from_email is None:\n msg = \"From email not found.\"\n raise ValueError(msg)\n\n parts = msg[\"payload\"][\"parts\"] if \"parts\" in msg[\"payload\"] else [msg[\"payload\"]]\n\n for part in parts:\n if part[\"mimeType\"] == \"text/plain\":\n data = part[\"body\"][\"data\"]\n data = base64.urlsafe_b64decode(data).decode(\"utf-8\")\n pattern = re.compile(r\"\\r\\nOn .+(\\r\\n)*wrote:\\r\\n\")\n newest_response = re.split(pattern, data)[0]\n return HumanMessage(\n content=self.clean_message_content(newest_response),\n additional_kwargs={\"sender\": from_email},\n )\n msg = \"No plain text part found in the email.\"\n raise ValueError(msg)\n\n def _get_message_data(self, service: Any, message: Any) -> ChatSession:\n msg = service.users().messages().get(userId=\"me\", id=message[\"id\"]).execute()\n message_content = self._extract_email_content(msg)\n\n in_reply_to = None\n email_data = msg[\"payload\"][\"headers\"]\n for values in email_data:\n name = values[\"name\"]\n if name == \"In-Reply-To\":\n in_reply_to = values[\"value\"]\n\n thread_id = msg[\"threadId\"]\n\n if in_reply_to:\n thread = service.users().threads().get(userId=\"me\", id=thread_id).execute()\n messages = thread[\"messages\"]\n\n response_email = None\n for _message in messages:\n email_data = _message[\"payload\"][\"headers\"]\n for values in email_data:\n if values[\"name\"] == \"Message-ID\":\n message_id = values[\"value\"]\n if message_id == in_reply_to:\n response_email = _message\n if response_email is None:\n msg = \"Response email not found in the thread.\"\n raise ValueError(msg)\n starter_content = self._extract_email_content(response_email)\n return ChatSession(messages=[starter_content, message_content])\n return ChatSession(messages=[message_content])\n\n def lazy_load(self) -> Iterator[ChatSession]:\n service = build(\"gmail\", \"v1\", credentials=self.creds)\n results = (\n service.users().messages().list(userId=\"me\", labelIds=self.label_ids, maxResults=self.n).execute()\n )\n messages = results.get(\"messages\", [])\n if not messages:\n logger.warning(\"No messages found with the specified labels.\")\n for message in messages:\n try:\n yield self._get_message_data(service, message)\n except Exception:\n if self.raise_error:\n raise\n else:\n logger.exception(f\"Error processing message {message['id']}\")\n\n json_string = self.json_string\n label_ids = self.label_ids.split(\",\") if self.label_ids else [\"INBOX\"]\n max_results = int(self.max_results) if self.max_results else 100\n\n # Load the token information from the JSON string\n try:\n token_info = json.loads(json_string)\n except JSONDecodeError as e:\n msg = \"Invalid JSON string\"\n raise ValueError(msg) from e\n\n creds = Credentials.from_authorized_user_info(token_info)\n\n # Initialize the custom loader with the provided credentials\n loader = CustomGMailLoader(creds=creds, n=max_results, label_ids=label_ids)\n\n try:\n docs = loader.load()\n except RefreshError as e:\n msg = \"Authentication error: Unable to refresh authentication token. Please try to reauthenticate.\"\n raise ValueError(msg) from e\n except Exception as e:\n msg = f\"Error loading documents: {e}\"\n raise ValueError(msg) from e\n\n # Return the loaded documents\n self.status = docs\n return Data(data={\"text\": docs})\n" + "value": "import base64\nimport json\nimport re\nfrom collections.abc import Iterator\nfrom json.decoder import JSONDecodeError\nfrom typing import Any\n\nfrom google.auth.exceptions import RefreshError\nfrom google.oauth2.credentials import Credentials\nfrom googleapiclient.discovery import build\nfrom langchain_core.chat_sessions import ChatSession\nfrom langchain_core.messages import HumanMessage\nfrom langchain_google_community.gmail.loader import GMailLoader\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.template.field.base import Output\n\n\nclass GmailLoaderComponent(Component):\n display_name = \"Gmail Loader\"\n description = \"Loads emails from Gmail using provided credentials.\"\n icon = \"Google\"\n legacy: bool = True\n replacement = [\"composio.ComposioGmailAPIComponent\"]\n\n inputs = [\n SecretStrInput(\n name=\"json_string\",\n display_name=\"JSON String of the Service Account Token\",\n info=\"JSON string containing OAuth 2.0 access token information for service account access\",\n required=True,\n value=\"\"\"{\n \"account\": \"\",\n \"client_id\": \"\",\n \"client_secret\": \"\",\n \"expiry\": \"\",\n \"refresh_token\": \"\",\n \"scopes\": [\n \"https://www.googleapis.com/auth/gmail.readonly\",\n ],\n \"token\": \"\",\n \"token_uri\": \"https://oauth2.googleapis.com/token\",\n \"universe_domain\": \"googleapis.com\"\n }\"\"\",\n ),\n MessageTextInput(\n name=\"label_ids\",\n display_name=\"Label IDs\",\n info=\"Comma-separated list of label IDs to filter emails.\",\n required=True,\n value=\"INBOX,SENT,UNREAD,IMPORTANT\",\n ),\n MessageTextInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"Maximum number of emails to load.\",\n required=True,\n value=\"10\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"load_emails\"),\n ]\n\n def load_emails(self) -> Data:\n class CustomGMailLoader(GMailLoader):\n def __init__(\n self, creds: Any, *, n: int = 100, label_ids: list[str] | None = None, raise_error: bool = False\n ) -> None:\n super().__init__(creds, n, raise_error)\n self.label_ids = label_ids if label_ids is not None else [\"SENT\"]\n\n def clean_message_content(self, message):\n # Remove URLs\n message = re.sub(r\"http\\S+|www\\S+|https\\S+\", \"\", message, flags=re.MULTILINE)\n\n # Remove email addresses\n message = re.sub(r\"\\S+@\\S+\", \"\", message)\n\n # Remove special characters and excessive whitespace\n message = re.sub(r\"[^A-Za-z0-9\\s]+\", \" \", message)\n message = re.sub(r\"\\s{2,}\", \" \", message)\n\n # Trim leading and trailing whitespace\n return message.strip()\n\n def _extract_email_content(self, msg: Any) -> HumanMessage:\n from_email = None\n for values in msg[\"payload\"][\"headers\"]:\n name = values[\"name\"]\n if name == \"From\":\n from_email = values[\"value\"]\n if from_email is None:\n msg = \"From email not found.\"\n raise ValueError(msg)\n\n parts = msg[\"payload\"][\"parts\"] if \"parts\" in msg[\"payload\"] else [msg[\"payload\"]]\n\n for part in parts:\n if part[\"mimeType\"] == \"text/plain\":\n data = part[\"body\"][\"data\"]\n data = base64.urlsafe_b64decode(data).decode(\"utf-8\")\n pattern = re.compile(r\"\\r\\nOn .+(\\r\\n)*wrote:\\r\\n\")\n newest_response = re.split(pattern, data)[0]\n return HumanMessage(\n content=self.clean_message_content(newest_response),\n additional_kwargs={\"sender\": from_email},\n )\n msg = \"No plain text part found in the email.\"\n raise ValueError(msg)\n\n def _get_message_data(self, service: Any, message: Any) -> ChatSession:\n msg = service.users().messages().get(userId=\"me\", id=message[\"id\"]).execute()\n message_content = self._extract_email_content(msg)\n\n in_reply_to = None\n email_data = msg[\"payload\"][\"headers\"]\n for values in email_data:\n name = values[\"name\"]\n if name == \"In-Reply-To\":\n in_reply_to = values[\"value\"]\n\n thread_id = msg[\"threadId\"]\n\n if in_reply_to:\n thread = service.users().threads().get(userId=\"me\", id=thread_id).execute()\n messages = thread[\"messages\"]\n\n response_email = None\n for _message in messages:\n email_data = _message[\"payload\"][\"headers\"]\n for values in email_data:\n if values[\"name\"] == \"Message-ID\":\n message_id = values[\"value\"]\n if message_id == in_reply_to:\n response_email = _message\n if response_email is None:\n msg = \"Response email not found in the thread.\"\n raise ValueError(msg)\n starter_content = self._extract_email_content(response_email)\n return ChatSession(messages=[starter_content, message_content])\n return ChatSession(messages=[message_content])\n\n def lazy_load(self) -> Iterator[ChatSession]:\n service = build(\"gmail\", \"v1\", credentials=self.creds)\n results = (\n service.users().messages().list(userId=\"me\", labelIds=self.label_ids, maxResults=self.n).execute()\n )\n messages = results.get(\"messages\", [])\n if not messages:\n logger.warning(\"No messages found with the specified labels.\")\n for message in messages:\n try:\n yield self._get_message_data(service, message)\n except Exception:\n if self.raise_error:\n raise\n else:\n logger.exception(f\"Error processing message {message['id']}\")\n\n json_string = self.json_string\n label_ids = self.label_ids.split(\",\") if self.label_ids else [\"INBOX\"]\n max_results = int(self.max_results) if self.max_results else 100\n\n # Load the token information from the JSON string\n try:\n token_info = json.loads(json_string)\n except JSONDecodeError as e:\n msg = \"Invalid JSON string\"\n raise ValueError(msg) from e\n\n creds = Credentials.from_authorized_user_info(token_info)\n\n # Initialize the custom loader with the provided credentials\n loader = CustomGMailLoader(creds=creds, n=max_results, label_ids=label_ids)\n\n try:\n docs = loader.load()\n except RefreshError as e:\n msg = \"Authentication error: Unable to refresh authentication token. Please try to reauthenticate.\"\n raise ValueError(msg) from e\n except Exception as e:\n msg = f\"Error loading documents: {e}\"\n raise ValueError(msg) from e\n\n # Return the loaded documents\n self.status = docs\n return Data(data={\"text\": docs})\n" }, "json_string": { "_input_type": "SecretStrInput", @@ -73790,7 +73847,7 @@ }, "GoogleDriveComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -73837,10 +73894,10 @@ "group_outputs": false, "method": "load_documents", "name": "docs", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -73915,7 +73972,7 @@ }, "GoogleDriveSearchComponent": { "base_classes": [ - "Data", + "JSON", "Text" ], "beta": false, @@ -73936,7 +73993,7 @@ "icon": "Google", "legacy": true, "metadata": { - "code_hash": "8f8dbdf04aaf", + "code_hash": "35528aa332d1", "dependencies": { "dependencies": [ { @@ -74004,14 +74061,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "search_data", "name": "Data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -74035,7 +74092,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\n\nfrom google.oauth2.credentials import Credentials\nfrom googleapiclient.discovery import build\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DropdownInput, MessageTextInput\nfrom lfx.io import SecretStrInput\nfrom lfx.schema.data import Data\nfrom lfx.template.field.base import Output\n\n\nclass GoogleDriveSearchComponent(Component):\n display_name = \"Google Drive Search\"\n description = \"Searches Google Drive files using provided credentials and query parameters.\"\n icon = \"Google\"\n legacy: bool = True\n\n inputs = [\n SecretStrInput(\n name=\"token_string\",\n display_name=\"Token String\",\n info=\"JSON string containing OAuth 2.0 access token information for service account access\",\n required=True,\n ),\n DropdownInput(\n name=\"query_item\",\n display_name=\"Query Item\",\n options=[\n \"name\",\n \"fullText\",\n \"mimeType\",\n \"modifiedTime\",\n \"viewedByMeTime\",\n \"trashed\",\n \"starred\",\n \"parents\",\n \"owners\",\n \"writers\",\n \"readers\",\n \"sharedWithMe\",\n \"createdTime\",\n \"properties\",\n \"appProperties\",\n \"visibility\",\n \"shortcutDetails.targetId\",\n ],\n info=\"The field to query.\",\n required=True,\n ),\n DropdownInput(\n name=\"valid_operator\",\n display_name=\"Valid Operator\",\n options=[\"contains\", \"=\", \"!=\", \"<=\", \"<\", \">\", \">=\", \"in\", \"has\"],\n info=\"Operator to use in the query.\",\n required=True,\n ),\n MessageTextInput(\n name=\"search_term\",\n display_name=\"Search Term\",\n info=\"The value to search for in the specified query item.\",\n required=True,\n ),\n MessageTextInput(\n name=\"query_string\",\n display_name=\"Query String\",\n info=\"The query string used for searching. You can edit this manually.\",\n value=\"\", # This will be updated with the generated query string\n ),\n ]\n\n outputs = [\n Output(display_name=\"Document URLs\", name=\"doc_urls\", method=\"search_doc_urls\"),\n Output(display_name=\"Document IDs\", name=\"doc_ids\", method=\"search_doc_ids\"),\n Output(display_name=\"Document Titles\", name=\"doc_titles\", method=\"search_doc_titles\"),\n Output(display_name=\"Data\", name=\"Data\", method=\"search_data\"),\n ]\n\n def generate_query_string(self) -> str:\n query_item = self.query_item\n valid_operator = self.valid_operator\n search_term = self.search_term\n\n # Construct the query string\n query = f\"{query_item} {valid_operator} '{search_term}'\"\n\n # Update the editable query string input with the generated query\n self.query_string = query\n\n return query\n\n def on_inputs_changed(self) -> None:\n # Automatically regenerate the query string when inputs change\n self.generate_query_string()\n\n def generate_file_url(self, file_id: str, mime_type: str) -> str:\n \"\"\"Generates the appropriate Google Drive URL for a file based on its MIME type.\"\"\"\n return {\n \"application/vnd.google-apps.document\": f\"https://docs.google.com/document/d/{file_id}/edit\",\n \"application/vnd.google-apps.spreadsheet\": f\"https://docs.google.com/spreadsheets/d/{file_id}/edit\",\n \"application/vnd.google-apps.presentation\": f\"https://docs.google.com/presentation/d/{file_id}/edit\",\n \"application/vnd.google-apps.drawing\": f\"https://docs.google.com/drawings/d/{file_id}/edit\",\n \"application/pdf\": f\"https://drive.google.com/file/d/{file_id}/view?usp=drivesdk\",\n }.get(mime_type, f\"https://drive.google.com/file/d/{file_id}/view?usp=drivesdk\")\n\n def search_files(self) -> dict:\n # Load the token information from the JSON string\n token_info = json.loads(self.token_string)\n creds = Credentials.from_authorized_user_info(token_info)\n\n # Use the query string from the input (which might have been edited by the user)\n query = self.query_string or self.generate_query_string()\n\n # Initialize the Google Drive API service\n service = build(\"drive\", \"v3\", credentials=creds)\n\n # Perform the search\n results = service.files().list(q=query, pageSize=5, fields=\"nextPageToken, files(id, name, mimeType)\").execute()\n items = results.get(\"files\", [])\n\n doc_urls = []\n doc_ids = []\n doc_titles_urls = []\n doc_titles = []\n\n if items:\n for item in items:\n # Directly use the file ID, title, and MIME type to generate the URL\n file_id = item[\"id\"]\n file_title = item[\"name\"]\n mime_type = item[\"mimeType\"]\n file_url = self.generate_file_url(file_id, mime_type)\n\n # Store the URL, ID, and title+URL in their respective lists\n doc_urls.append(file_url)\n doc_ids.append(file_id)\n doc_titles.append(file_title)\n doc_titles_urls.append({\"title\": file_title, \"url\": file_url})\n\n return {\"doc_urls\": doc_urls, \"doc_ids\": doc_ids, \"doc_titles_urls\": doc_titles_urls, \"doc_titles\": doc_titles}\n\n def search_doc_ids(self) -> list[str]:\n return self.search_files()[\"doc_ids\"]\n\n def search_doc_urls(self) -> list[str]:\n return self.search_files()[\"doc_urls\"]\n\n def search_doc_titles(self) -> list[str]:\n return self.search_files()[\"doc_titles\"]\n\n def search_data(self) -> Data:\n return Data(data={\"text\": self.search_files()[\"doc_titles_urls\"]})\n" + "value": "import json\n\nfrom google.oauth2.credentials import Credentials\nfrom googleapiclient.discovery import build\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DropdownInput, MessageTextInput\nfrom lfx.io import SecretStrInput\nfrom lfx.schema.data import Data\nfrom lfx.template.field.base import Output\n\n\nclass GoogleDriveSearchComponent(Component):\n display_name = \"Google Drive Search\"\n description = \"Searches Google Drive files using provided credentials and query parameters.\"\n icon = \"Google\"\n legacy: bool = True\n\n inputs = [\n SecretStrInput(\n name=\"token_string\",\n display_name=\"Token String\",\n info=\"JSON string containing OAuth 2.0 access token information for service account access\",\n required=True,\n ),\n DropdownInput(\n name=\"query_item\",\n display_name=\"Query Item\",\n options=[\n \"name\",\n \"fullText\",\n \"mimeType\",\n \"modifiedTime\",\n \"viewedByMeTime\",\n \"trashed\",\n \"starred\",\n \"parents\",\n \"owners\",\n \"writers\",\n \"readers\",\n \"sharedWithMe\",\n \"createdTime\",\n \"properties\",\n \"appProperties\",\n \"visibility\",\n \"shortcutDetails.targetId\",\n ],\n info=\"The field to query.\",\n required=True,\n ),\n DropdownInput(\n name=\"valid_operator\",\n display_name=\"Valid Operator\",\n options=[\"contains\", \"=\", \"!=\", \"<=\", \"<\", \">\", \">=\", \"in\", \"has\"],\n info=\"Operator to use in the query.\",\n required=True,\n ),\n MessageTextInput(\n name=\"search_term\",\n display_name=\"Search Term\",\n info=\"The value to search for in the specified query item.\",\n required=True,\n ),\n MessageTextInput(\n name=\"query_string\",\n display_name=\"Query String\",\n info=\"The query string used for searching. You can edit this manually.\",\n value=\"\", # This will be updated with the generated query string\n ),\n ]\n\n outputs = [\n Output(display_name=\"Document URLs\", name=\"doc_urls\", method=\"search_doc_urls\"),\n Output(display_name=\"Document IDs\", name=\"doc_ids\", method=\"search_doc_ids\"),\n Output(display_name=\"Document Titles\", name=\"doc_titles\", method=\"search_doc_titles\"),\n Output(display_name=\"JSON\", name=\"Data\", method=\"search_data\"),\n ]\n\n def generate_query_string(self) -> str:\n query_item = self.query_item\n valid_operator = self.valid_operator\n search_term = self.search_term\n\n # Construct the query string\n query = f\"{query_item} {valid_operator} '{search_term}'\"\n\n # Update the editable query string input with the generated query\n self.query_string = query\n\n return query\n\n def on_inputs_changed(self) -> None:\n # Automatically regenerate the query string when inputs change\n self.generate_query_string()\n\n def generate_file_url(self, file_id: str, mime_type: str) -> str:\n \"\"\"Generates the appropriate Google Drive URL for a file based on its MIME type.\"\"\"\n return {\n \"application/vnd.google-apps.document\": f\"https://docs.google.com/document/d/{file_id}/edit\",\n \"application/vnd.google-apps.spreadsheet\": f\"https://docs.google.com/spreadsheets/d/{file_id}/edit\",\n \"application/vnd.google-apps.presentation\": f\"https://docs.google.com/presentation/d/{file_id}/edit\",\n \"application/vnd.google-apps.drawing\": f\"https://docs.google.com/drawings/d/{file_id}/edit\",\n \"application/pdf\": f\"https://drive.google.com/file/d/{file_id}/view?usp=drivesdk\",\n }.get(mime_type, f\"https://drive.google.com/file/d/{file_id}/view?usp=drivesdk\")\n\n def search_files(self) -> dict:\n # Load the token information from the JSON string\n token_info = json.loads(self.token_string)\n creds = Credentials.from_authorized_user_info(token_info)\n\n # Use the query string from the input (which might have been edited by the user)\n query = self.query_string or self.generate_query_string()\n\n # Initialize the Google Drive API service\n service = build(\"drive\", \"v3\", credentials=creds)\n\n # Perform the search\n results = service.files().list(q=query, pageSize=5, fields=\"nextPageToken, files(id, name, mimeType)\").execute()\n items = results.get(\"files\", [])\n\n doc_urls = []\n doc_ids = []\n doc_titles_urls = []\n doc_titles = []\n\n if items:\n for item in items:\n # Directly use the file ID, title, and MIME type to generate the URL\n file_id = item[\"id\"]\n file_title = item[\"name\"]\n mime_type = item[\"mimeType\"]\n file_url = self.generate_file_url(file_id, mime_type)\n\n # Store the URL, ID, and title+URL in their respective lists\n doc_urls.append(file_url)\n doc_ids.append(file_id)\n doc_titles.append(file_title)\n doc_titles_urls.append({\"title\": file_title, \"url\": file_url})\n\n return {\"doc_urls\": doc_urls, \"doc_ids\": doc_ids, \"doc_titles_urls\": doc_titles_urls, \"doc_titles\": doc_titles}\n\n def search_doc_ids(self) -> list[str]:\n return self.search_files()[\"doc_ids\"]\n\n def search_doc_urls(self) -> list[str]:\n return self.search_files()[\"doc_urls\"]\n\n def search_doc_titles(self) -> list[str]:\n return self.search_files()[\"doc_titles\"]\n\n def search_data(self) -> Data:\n return Data(data={\"text\": self.search_files()[\"doc_titles_urls\"]})\n" }, "query_item": { "_input_type": "DropdownInput", @@ -74569,7 +74626,7 @@ }, "GoogleOAuthToken": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -74616,10 +74673,10 @@ "group_outputs": false, "method": "build_output", "name": "output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -74704,7 +74761,7 @@ }, "GoogleSearchAPICore": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -74749,10 +74806,10 @@ "group_outputs": false, "method": "search_google", "name": "results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -74870,7 +74927,7 @@ }, "GoogleSerperAPICore": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -74914,10 +74971,10 @@ "group_outputs": false, "method": "search_serper", "name": "results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -75375,7 +75432,7 @@ { "HomeAssistantControl": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -75425,14 +75482,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -75559,7 +75616,7 @@ }, "ListHomeAssistantStates": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -75608,14 +75665,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -77190,7 +77247,7 @@ { "Combinatorial Reasoner": { "base_classes": [ - "Data", + "JSON", "Message" ], "beta": false, @@ -77251,10 +77308,10 @@ "group_outputs": false, "method": "build_reasons", "name": "reasons", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -77706,7 +77763,7 @@ "icon": "MessagesSquare", "legacy": false, "metadata": { - "code_hash": "8c87e536cca4", + "code_hash": "c312c84b1777", "dependencies": { "dependencies": [ { @@ -77783,7 +77840,7 @@ "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\", \"DataFrame\", \"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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" + "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 self.status = message\n return message\n\n def _serialize_data(self, data: Data) -> str:\n \"\"\"Serialize Data object to JSON string.\"\"\"\n # Convert data.data to JSON-serializable format\n serializable_data = jsonable_encoder(data.data)\n # Serialize with orjson, enabling pretty printing with indentation\n json_bytes = orjson.dumps(serializable_data, option=orjson.OPT_INDENT_2)\n # Convert bytes to string and wrap in Markdown code blocks\n return \"```json\\n\" + json_bytes.decode(\"utf-8\") + \"\\n```\"\n\n def _validate_input(self) -> None:\n \"\"\"Validate the input data and raise ValueError if invalid.\"\"\"\n if self.input_value is None:\n msg = \"Input data cannot be None\"\n raise ValueError(msg)\n if isinstance(self.input_value, list) and not all(\n isinstance(item, Message | Data | DataFrame | str) for item in self.input_value\n ):\n invalid_types = [\n type(item).__name__\n for item in self.input_value\n if not isinstance(item, Message | Data | DataFrame | str)\n ]\n msg = f\"Expected Data or DataFrame or Message or str, got {invalid_types}\"\n raise TypeError(msg)\n if not isinstance(\n self.input_value,\n Message | Data | DataFrame | str | list | Generator | type(None),\n ):\n type_name = type(self.input_value).__name__\n msg = f\"Expected Data or DataFrame or Message or str, Generator or None, got {type_name}\"\n raise TypeError(msg)\n\n def convert_to_string(self) -> str | Generator[Any, None, None]:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n self._validate_input()\n if isinstance(self.input_value, list):\n clean_data: bool = getattr(self, \"clean_data\", False)\n return \"\\n\".join([safe_convert(item, clean_data=clean_data) for item in self.input_value])\n if isinstance(self.input_value, Generator):\n return self.input_value\n return safe_convert(self.input_value)\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -77843,7 +77900,9 @@ "info": "Message to be passed as output.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -78185,7 +78244,7 @@ }, "Webhook": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -78202,7 +78261,7 @@ "icon": "webhook", "legacy": false, "metadata": { - "code_hash": "eb561ef21f3d", + "code_hash": "e99e2452d56e", "dependencies": { "dependencies": [ { @@ -78220,14 +78279,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "build_data", "name": "output_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -78251,7 +78310,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import MultilineInput, Output\nfrom lfx.schema.data import Data\n\n\nclass WebhookComponent(Component):\n display_name = \"Webhook\"\n documentation: str = \"https://docs.langflow.org/component-webhook\"\n name = \"Webhook\"\n icon = \"webhook\"\n\n inputs = [\n MultilineInput(\n name=\"data\",\n display_name=\"Payload\",\n info=\"Receives a payload from external systems via HTTP POST.\",\n advanced=True,\n ),\n MultilineInput(\n name=\"curl\",\n display_name=\"cURL\",\n value=\"CURL_WEBHOOK\",\n advanced=True,\n input_types=[],\n ),\n MultilineInput(\n name=\"endpoint\",\n display_name=\"Endpoint\",\n value=\"BACKEND_URL\",\n advanced=False,\n copy_field=True,\n input_types=[],\n ),\n ]\n outputs = [\n Output(display_name=\"Data\", name=\"output_data\", method=\"build_data\"),\n ]\n\n def build_data(self) -> Data:\n message: str | Data = \"\"\n if not self.data:\n self.status = \"No data provided.\"\n return Data(data={})\n try:\n my_data = self.data.replace('\"\\n\"', '\"\\\\n\"')\n body = json.loads(my_data or \"{}\")\n except json.JSONDecodeError:\n body = {\"payload\": self.data}\n message = f\"Invalid JSON payload. Please check the format.\\n\\n{self.data}\"\n data = Data(data=body)\n if not message:\n message = data\n self.status = message\n return data\n" + "value": "import json\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import MultilineInput, Output\nfrom lfx.schema.data import Data\n\n\nclass WebhookComponent(Component):\n display_name = \"Webhook\"\n documentation: str = \"https://docs.langflow.org/component-webhook\"\n name = \"Webhook\"\n icon = \"webhook\"\n\n inputs = [\n MultilineInput(\n name=\"data\",\n display_name=\"Payload\",\n info=\"Receives a payload from external systems via HTTP POST.\",\n advanced=True,\n ),\n MultilineInput(\n name=\"curl\",\n display_name=\"cURL\",\n value=\"CURL_WEBHOOK\",\n advanced=True,\n input_types=[],\n ),\n MultilineInput(\n name=\"endpoint\",\n display_name=\"Endpoint\",\n value=\"BACKEND_URL\",\n advanced=False,\n copy_field=True,\n input_types=[],\n ),\n ]\n outputs = [\n Output(display_name=\"JSON\", name=\"output_data\", method=\"build_data\"),\n ]\n\n def build_data(self) -> Data:\n message: str | Data = \"\"\n if not self.data:\n self.status = \"No data provided.\"\n return Data(data={})\n try:\n my_data = self.data.replace('\"\\n\"', '\"\\\\n\"')\n body = json.loads(my_data or \"{}\")\n except json.JSONDecodeError:\n body = {\"payload\": self.data}\n message = f\"Invalid JSON payload. Please check the format.\\n\\n{self.data}\"\n data = Data(data=body)\n if not message:\n message = data\n self.status = message\n return data\n" }, "curl": { "_input_type": "MultilineInput", @@ -78346,7 +78405,7 @@ { "JigsawStackAIScraper": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -78392,10 +78451,10 @@ "group_outputs": false, "method": "scrape", "name": "scrape_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -78545,7 +78604,7 @@ }, "JigsawStackAISearch": { "base_classes": [ - "Data", + "JSON", "Message" ], "beta": false, @@ -78592,10 +78651,10 @@ "group_outputs": false, "method": "search", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -78752,7 +78811,7 @@ }, "JigsawStackFileRead": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -78795,10 +78854,10 @@ "group_outputs": false, "method": "read_and_save_file", "name": "file_path", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -78869,7 +78928,7 @@ }, "JigsawStackFileUpload": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -78915,10 +78974,10 @@ "group_outputs": false, "method": "upload_file", "name": "file_upload_result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -79062,7 +79121,7 @@ }, "JigsawStackImageGeneration": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -79115,10 +79174,10 @@ "group_outputs": false, "method": "generate_image", "name": "image_generation_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -79420,7 +79479,7 @@ }, "JigsawStackNSFW": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -79463,10 +79522,10 @@ "group_outputs": false, "method": "detect_nsfw", "name": "nsfw_result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -79537,7 +79596,7 @@ }, "JigsawStackObjectDetection": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -79585,10 +79644,10 @@ "group_outputs": false, "method": "detect_objects", "name": "object_detection_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -79790,7 +79849,7 @@ }, "JigsawStackSentiment": { "base_classes": [ - "Data", + "JSON", "Message" ], "beta": false, @@ -79834,10 +79893,10 @@ "group_outputs": false, "method": "analyze_sentiment", "name": "sentiment_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -79926,7 +79985,7 @@ }, "JigsawStackTextToSQL": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -79971,10 +80030,10 @@ "group_outputs": false, "method": "generate_sql", "name": "sql_query", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -80095,7 +80154,7 @@ }, "JigsawStackTextTranslate": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -80139,10 +80198,10 @@ "group_outputs": false, "method": "translation", "name": "translation_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -80238,7 +80297,7 @@ }, "JigsawStackVOCR": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -80285,10 +80344,10 @@ "group_outputs": false, "method": "vocr", "name": "vocr_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -80876,7 +80935,7 @@ }, "CharacterTextSplitter": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -80895,7 +80954,7 @@ "icon": "LangChain", "legacy": false, "metadata": { - "code_hash": "995b35c5296c", + "code_hash": "ea7c81772b05", "dependencies": { "dependencies": [ { @@ -80917,14 +80976,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "transform_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -80988,17 +81047,18 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_text_splitters import CharacterTextSplitter, TextSplitter\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.inputs.inputs import DataInput, IntInput, MessageTextInput\nfrom lfx.utils.util import unescape_string\n\n\nclass CharacterTextSplitterComponent(LCTextSplitterComponent):\n display_name = \"Character Text Splitter\"\n description = \"Split text by number of characters.\"\n documentation = \"https://docs.langflow.org/bundles-langchain\"\n name = \"CharacterTextSplitter\"\n icon = \"LangChain\"\n\n inputs = [\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=\"The maximum length of each chunk.\",\n value=1000,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"The amount of overlap between chunks.\",\n value=200,\n ),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The texts to split.\",\n input_types=[\"Document\", \"Data\"],\n required=True,\n ),\n MessageTextInput(\n name=\"separator\",\n display_name=\"Separator\",\n info='The characters to split on.\\nIf left empty defaults to \"\\\\n\\\\n\".',\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_text_splitter(self) -> TextSplitter:\n separator = unescape_string(self.separator) if self.separator else \"\\n\\n\"\n return CharacterTextSplitter(\n chunk_overlap=self.chunk_overlap,\n chunk_size=self.chunk_size,\n separator=separator,\n )\n" + "value": "from typing import Any\n\nfrom langchain_text_splitters import CharacterTextSplitter, TextSplitter\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.inputs.inputs import DataInput, IntInput, MessageTextInput\nfrom lfx.utils.util import unescape_string\n\n\nclass CharacterTextSplitterComponent(LCTextSplitterComponent):\n display_name = \"Character Text Splitter\"\n description = \"Split text by number of characters.\"\n documentation = \"https://docs.langflow.org/bundles-langchain\"\n name = \"CharacterTextSplitter\"\n icon = \"LangChain\"\n\n inputs = [\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=\"The maximum length of each chunk.\",\n value=1000,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"The amount of overlap between chunks.\",\n value=200,\n ),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The texts to split.\",\n input_types=[\"Document\", \"Data\", \"JSON\"],\n required=True,\n ),\n MessageTextInput(\n name=\"separator\",\n display_name=\"Separator\",\n info='The characters to split on.\\nIf left empty defaults to \"\\\\n\\\\n\".',\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_text_splitter(self) -> TextSplitter:\n separator = unescape_string(self.separator) if self.separator else \"\\n\\n\"\n return CharacterTextSplitter(\n chunk_overlap=self.chunk_overlap,\n chunk_size=self.chunk_size,\n separator=separator,\n )\n" }, "data_input": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Input", "dynamic": false, "info": "The texts to split.", "input_types": [ "Document", - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -81196,7 +81256,7 @@ }, "HtmlLinkExtractor": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -81214,7 +81274,7 @@ "icon": "LangChain", "legacy": false, "metadata": { - "code_hash": "13cc6457f84c", + "code_hash": "7acefd9ece13", "dependencies": { "dependencies": [ { @@ -81240,14 +81300,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "transform_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -81271,17 +81331,18 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_community.graph_vectorstores.extractors import HtmlLinkExtractor, LinkExtractorTransformer\nfrom langchain_core.documents import BaseDocumentTransformer\n\nfrom lfx.base.document_transformers.model import LCDocumentTransformerComponent\nfrom lfx.inputs.inputs import BoolInput, DataInput, StrInput\n\n\nclass HtmlLinkExtractorComponent(LCDocumentTransformerComponent):\n display_name = \"HTML Link Extractor\"\n description = \"Extract hyperlinks from HTML content.\"\n documentation = \"https://python.langchain.com/v0.2/api_reference/community/graph_vectorstores/langchain_community.graph_vectorstores.extractors.html_link_extractor.HtmlLinkExtractor.html\"\n name = \"HtmlLinkExtractor\"\n icon = \"LangChain\"\n\n inputs = [\n StrInput(name=\"kind\", display_name=\"Kind of edge\", value=\"hyperlink\", required=False),\n BoolInput(name=\"drop_fragments\", display_name=\"Drop URL fragments\", value=True, required=False),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The texts from which to extract links.\",\n input_types=[\"Document\", \"Data\"],\n required=True,\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_document_transformer(self) -> BaseDocumentTransformer:\n return LinkExtractorTransformer(\n [HtmlLinkExtractor(kind=self.kind, drop_fragments=self.drop_fragments).as_document_extractor()]\n )\n" + "value": "from typing import Any\n\nfrom langchain_community.graph_vectorstores.extractors import HtmlLinkExtractor, LinkExtractorTransformer\nfrom langchain_core.documents import BaseDocumentTransformer\n\nfrom lfx.base.document_transformers.model import LCDocumentTransformerComponent\nfrom lfx.inputs.inputs import BoolInput, DataInput, StrInput\n\n\nclass HtmlLinkExtractorComponent(LCDocumentTransformerComponent):\n display_name = \"HTML Link Extractor\"\n description = \"Extract hyperlinks from HTML content.\"\n documentation = \"https://python.langchain.com/v0.2/api_reference/community/graph_vectorstores/langchain_community.graph_vectorstores.extractors.html_link_extractor.HtmlLinkExtractor.html\"\n name = \"HtmlLinkExtractor\"\n icon = \"LangChain\"\n\n inputs = [\n StrInput(name=\"kind\", display_name=\"Kind of edge\", value=\"hyperlink\", required=False),\n BoolInput(name=\"drop_fragments\", display_name=\"Drop URL fragments\", value=True, required=False),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The texts from which to extract links.\",\n input_types=[\"Document\", \"Data\", \"JSON\"],\n required=True,\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_document_transformer(self) -> BaseDocumentTransformer:\n return LinkExtractorTransformer(\n [HtmlLinkExtractor(kind=self.kind, drop_fragments=self.drop_fragments).as_document_extractor()]\n )\n" }, "data_input": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Input", "dynamic": false, "info": "The texts from which to extract links.", "input_types": [ "Document", - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -82085,7 +82146,7 @@ }, "LanguageRecursiveTextSplitter": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -82104,7 +82165,7 @@ "icon": "LangChain", "legacy": false, "metadata": { - "code_hash": "207a88b20a7e", + "code_hash": "ee28cc4c2001", "dependencies": { "dependencies": [ { @@ -82126,14 +82187,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "transform_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -82197,7 +82258,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_text_splitters import Language, RecursiveCharacterTextSplitter, TextSplitter\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.inputs.inputs import DataInput, DropdownInput, IntInput\n\n\nclass LanguageRecursiveTextSplitterComponent(LCTextSplitterComponent):\n display_name: str = \"Language Recursive Text Splitter\"\n description: str = \"Split text into chunks of a specified length based on language.\"\n documentation: str = \"https://docs.langflow.org/bundles-langchain\"\n name = \"LanguageRecursiveTextSplitter\"\n icon = \"LangChain\"\n\n inputs = [\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=\"The maximum length of each chunk.\",\n value=1000,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"The amount of overlap between chunks.\",\n value=200,\n ),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The texts to split.\",\n input_types=[\"Document\", \"Data\"],\n required=True,\n ),\n DropdownInput(\n name=\"code_language\", display_name=\"Code Language\", options=[x.value for x in Language], value=\"python\"\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_text_splitter(self) -> TextSplitter:\n return RecursiveCharacterTextSplitter.from_language(\n language=Language(self.code_language),\n chunk_size=self.chunk_size,\n chunk_overlap=self.chunk_overlap,\n )\n" + "value": "from typing import Any\n\nfrom langchain_text_splitters import Language, RecursiveCharacterTextSplitter, TextSplitter\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.inputs.inputs import DataInput, DropdownInput, IntInput\n\n\nclass LanguageRecursiveTextSplitterComponent(LCTextSplitterComponent):\n display_name: str = \"Language Recursive Text Splitter\"\n description: str = \"Split text into chunks of a specified length based on language.\"\n documentation: str = \"https://docs.langflow.org/bundles-langchain\"\n name = \"LanguageRecursiveTextSplitter\"\n icon = \"LangChain\"\n\n inputs = [\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=\"The maximum length of each chunk.\",\n value=1000,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"The amount of overlap between chunks.\",\n value=200,\n ),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The texts to split.\",\n input_types=[\"Document\", \"Data\", \"JSON\"],\n required=True,\n ),\n DropdownInput(\n name=\"code_language\", display_name=\"Code Language\", options=[x.value for x in Language], value=\"python\"\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_text_splitter(self) -> TextSplitter:\n return RecursiveCharacterTextSplitter.from_language(\n language=Language(self.code_language),\n chunk_size=self.chunk_size,\n chunk_overlap=self.chunk_overlap,\n )\n" }, "code_language": { "_input_type": "DropdownInput", @@ -82252,14 +82313,15 @@ "value": "python" }, "data_input": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Input", "dynamic": false, "info": "The texts to split.", "input_types": [ "Document", - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -82281,7 +82343,7 @@ }, "NaturalLanguageTextSplitter": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -82301,7 +82363,7 @@ "icon": "LangChain", "legacy": false, "metadata": { - "code_hash": "aed1e0bb411e", + "code_hash": "6483da1155b8", "dependencies": { "dependencies": [ { @@ -82323,14 +82385,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "transform_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -82394,17 +82456,18 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_text_splitters import NLTKTextSplitter, TextSplitter\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.inputs.inputs import DataInput, IntInput, MessageTextInput\nfrom lfx.utils.util import unescape_string\n\n\nclass NaturalLanguageTextSplitterComponent(LCTextSplitterComponent):\n display_name = \"Natural Language Text Splitter\"\n description = \"Split text based on natural language boundaries, optimized for a specified language.\"\n documentation = (\n \"https://python.langchain.com/v0.1/docs/modules/data_connection/document_transformers/split_by_token/#nltk\"\n )\n name = \"NaturalLanguageTextSplitter\"\n icon = \"LangChain\"\n inputs = [\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=\"The maximum number of characters in each chunk after splitting.\",\n value=1000,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"The number of characters that overlap between consecutive chunks.\",\n value=200,\n ),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The text data to be split.\",\n input_types=[\"Document\", \"Data\"],\n required=True,\n ),\n MessageTextInput(\n name=\"separator\",\n display_name=\"Separator\",\n info='The character(s) to use as a delimiter when splitting text.\\nDefaults to \"\\\\n\\\\n\" if left empty.',\n ),\n MessageTextInput(\n name=\"language\",\n display_name=\"Language\",\n info='The language of the text. Default is \"English\". '\n \"Supports multiple languages for better text boundary recognition.\",\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_text_splitter(self) -> TextSplitter:\n separator = unescape_string(self.separator) if self.separator else \"\\n\\n\"\n return NLTKTextSplitter(\n language=self.language.lower() if self.language else \"english\",\n separator=separator,\n chunk_size=self.chunk_size,\n chunk_overlap=self.chunk_overlap,\n )\n" + "value": "from typing import Any\n\nfrom langchain_text_splitters import NLTKTextSplitter, TextSplitter\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.inputs.inputs import DataInput, IntInput, MessageTextInput\nfrom lfx.utils.util import unescape_string\n\n\nclass NaturalLanguageTextSplitterComponent(LCTextSplitterComponent):\n display_name = \"Natural Language Text Splitter\"\n description = \"Split text based on natural language boundaries, optimized for a specified language.\"\n documentation = (\n \"https://python.langchain.com/v0.1/docs/modules/data_connection/document_transformers/split_by_token/#nltk\"\n )\n name = \"NaturalLanguageTextSplitter\"\n icon = \"LangChain\"\n inputs = [\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=\"The maximum number of characters in each chunk after splitting.\",\n value=1000,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"The number of characters that overlap between consecutive chunks.\",\n value=200,\n ),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The text data to be split.\",\n input_types=[\"Document\", \"Data\", \"JSON\"],\n required=True,\n ),\n MessageTextInput(\n name=\"separator\",\n display_name=\"Separator\",\n info='The character(s) to use as a delimiter when splitting text.\\nDefaults to \"\\\\n\\\\n\" if left empty.',\n ),\n MessageTextInput(\n name=\"language\",\n display_name=\"Language\",\n info='The language of the text. Default is \"English\". '\n \"Supports multiple languages for better text boundary recognition.\",\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_text_splitter(self) -> TextSplitter:\n separator = unescape_string(self.separator) if self.separator else \"\\n\\n\"\n return NLTKTextSplitter(\n language=self.language.lower() if self.language else \"english\",\n separator=separator,\n chunk_size=self.chunk_size,\n chunk_overlap=self.chunk_overlap,\n )\n" }, "data_input": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Input", "dynamic": false, "info": "The text data to be split.", "input_types": [ "Document", - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -82642,13 +82705,14 @@ "value": "https://us-south.ml.cloud.ibm.com" }, "chat_history": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": true, "display_name": "Chat History", "dynamic": false, "info": "", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -83291,7 +83355,7 @@ }, "RecursiveCharacterTextSplitter": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -83310,7 +83374,7 @@ "icon": "LangChain", "legacy": false, "metadata": { - "code_hash": "9ed58a212804", + "code_hash": "1cad6dd9957a", "dependencies": { "dependencies": [ { @@ -83332,14 +83396,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "transform_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -83403,17 +83467,18 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter, TextSplitter\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.inputs.inputs import DataInput, IntInput, MessageTextInput\nfrom lfx.utils.util import unescape_string\n\n\nclass RecursiveCharacterTextSplitterComponent(LCTextSplitterComponent):\n display_name: str = \"Recursive Character Text Splitter\"\n description: str = \"Split text trying to keep all related text together.\"\n documentation: str = \"https://docs.langflow.org/components-processing\"\n name = \"RecursiveCharacterTextSplitter\"\n icon = \"LangChain\"\n\n inputs = [\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=\"The maximum length of each chunk.\",\n value=1000,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"The amount of overlap between chunks.\",\n value=200,\n ),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The texts to split.\",\n input_types=[\"Document\", \"Data\"],\n required=True,\n ),\n MessageTextInput(\n name=\"separators\",\n display_name=\"Separators\",\n info='The characters to split on.\\nIf left empty defaults to [\"\\\\n\\\\n\", \"\\\\n\", \" \", \"\"].',\n is_list=True,\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_text_splitter(self) -> TextSplitter:\n if not self.separators:\n separators: list[str] | None = None\n else:\n # check if the separators list has escaped characters\n # if there are escaped characters, unescape them\n separators = [unescape_string(x) for x in self.separators]\n\n return RecursiveCharacterTextSplitter(\n separators=separators,\n chunk_size=self.chunk_size,\n chunk_overlap=self.chunk_overlap,\n )\n" + "value": "from typing import Any\n\nfrom langchain_text_splitters import RecursiveCharacterTextSplitter, TextSplitter\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.inputs.inputs import DataInput, IntInput, MessageTextInput\nfrom lfx.utils.util import unescape_string\n\n\nclass RecursiveCharacterTextSplitterComponent(LCTextSplitterComponent):\n display_name: str = \"Recursive Character Text Splitter\"\n description: str = \"Split text trying to keep all related text together.\"\n documentation: str = \"https://docs.langflow.org/components-processing\"\n name = \"RecursiveCharacterTextSplitter\"\n icon = \"LangChain\"\n\n inputs = [\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=\"The maximum length of each chunk.\",\n value=1000,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"The amount of overlap between chunks.\",\n value=200,\n ),\n DataInput(\n name=\"data_input\",\n display_name=\"Input\",\n info=\"The texts to split.\",\n input_types=[\"Document\", \"Data\", \"JSON\"],\n required=True,\n ),\n MessageTextInput(\n name=\"separators\",\n display_name=\"Separators\",\n info='The characters to split on.\\nIf left empty defaults to [\"\\\\n\\\\n\", \"\\\\n\", \" \", \"\"].',\n is_list=True,\n ),\n ]\n\n def get_data_input(self) -> Any:\n return self.data_input\n\n def build_text_splitter(self) -> TextSplitter:\n if not self.separators:\n separators: list[str] | None = None\n else:\n # check if the separators list has escaped characters\n # if there are escaped characters, unescape them\n separators = [unescape_string(x) for x in self.separators]\n\n return RecursiveCharacterTextSplitter(\n separators=separators,\n chunk_size=self.chunk_size,\n chunk_overlap=self.chunk_overlap,\n )\n" }, "data_input": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Input", "dynamic": false, "info": "The texts to split.", "input_types": [ "Document", - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -84568,7 +84633,7 @@ }, "SelfQueryRetriever": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -84588,7 +84653,7 @@ "icon": "LangChain", "legacy": true, "metadata": { - "code_hash": "a18169f36371", + "code_hash": "3b647e2416be", "dependencies": { "dependencies": [ { @@ -84614,10 +84679,10 @@ "group_outputs": false, "method": "retrieve_documents", "name": "documents", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -84632,7 +84697,8 @@ "dynamic": false, "info": "Metadata Field Info to be passed as input.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -84663,7 +84729,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from langchain.chains.query_constructor.base import AttributeInfo\nfrom langchain.retrievers.self_query.base import SelfQueryRetriever\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass SelfQueryRetrieverComponent(Component):\n display_name = \"Self Query Retriever\"\n description = \"Retriever that uses a vector store and an LLM to generate the vector store queries.\"\n name = \"SelfQueryRetriever\"\n icon = \"LangChain\"\n legacy: bool = True\n\n inputs = [\n HandleInput(\n name=\"query\",\n display_name=\"Query\",\n info=\"Query to be passed as input.\",\n input_types=[\"Message\"],\n ),\n HandleInput(\n name=\"vectorstore\",\n display_name=\"Vector Store\",\n info=\"Vector Store to be passed as input.\",\n input_types=[\"VectorStore\"],\n ),\n HandleInput(\n name=\"attribute_infos\",\n display_name=\"Metadata Field Info\",\n info=\"Metadata Field Info to be passed as input.\",\n input_types=[\"Data\"],\n is_list=True,\n ),\n MessageTextInput(\n name=\"document_content_description\",\n display_name=\"Document Content Description\",\n info=\"Document Content Description to be passed as input.\",\n ),\n HandleInput(\n name=\"llm\",\n display_name=\"LLM\",\n info=\"LLM to be passed as input.\",\n input_types=[\"LanguageModel\"],\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Retrieved Documents\",\n name=\"documents\",\n method=\"retrieve_documents\",\n ),\n ]\n\n def retrieve_documents(self) -> list[Data]:\n metadata_field_infos = [AttributeInfo(**value.data) for value in self.attribute_infos]\n self_query_retriever = SelfQueryRetriever.from_llm(\n llm=self.llm,\n vectorstore=self.vectorstore,\n document_contents=self.document_content_description,\n metadata_field_info=metadata_field_infos,\n enable_limit=True,\n )\n\n if isinstance(self.query, Message):\n input_text = self.query.text\n elif isinstance(self.query, str):\n input_text = self.query\n else:\n msg = f\"Query type {type(self.query)} not supported.\"\n raise TypeError(msg)\n\n documents = self_query_retriever.invoke(input=input_text, config={\"callbacks\": self.get_langchain_callbacks()})\n data = [Data.from_document(document) for document in documents]\n self.status = data\n return data\n" + "value": "from langchain.chains.query_constructor.base import AttributeInfo\nfrom langchain.retrievers.self_query.base import SelfQueryRetriever\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass SelfQueryRetrieverComponent(Component):\n display_name = \"Self Query Retriever\"\n description = \"Retriever that uses a vector store and an LLM to generate the vector store queries.\"\n name = \"SelfQueryRetriever\"\n icon = \"LangChain\"\n legacy: bool = True\n\n inputs = [\n HandleInput(\n name=\"query\",\n display_name=\"Query\",\n info=\"Query to be passed as input.\",\n input_types=[\"Message\"],\n ),\n HandleInput(\n name=\"vectorstore\",\n display_name=\"Vector Store\",\n info=\"Vector Store to be passed as input.\",\n input_types=[\"VectorStore\"],\n ),\n HandleInput(\n name=\"attribute_infos\",\n display_name=\"Metadata Field Info\",\n info=\"Metadata Field Info to be passed as input.\",\n input_types=[\"Data\", \"JSON\"],\n is_list=True,\n ),\n MessageTextInput(\n name=\"document_content_description\",\n display_name=\"Document Content Description\",\n info=\"Document Content Description to be passed as input.\",\n ),\n HandleInput(\n name=\"llm\",\n display_name=\"LLM\",\n info=\"LLM to be passed as input.\",\n input_types=[\"LanguageModel\"],\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Retrieved Documents\",\n name=\"documents\",\n method=\"retrieve_documents\",\n ),\n ]\n\n def retrieve_documents(self) -> list[Data]:\n metadata_field_infos = [AttributeInfo(**value.data) for value in self.attribute_infos]\n self_query_retriever = SelfQueryRetriever.from_llm(\n llm=self.llm,\n vectorstore=self.vectorstore,\n document_contents=self.document_content_description,\n metadata_field_info=metadata_field_infos,\n enable_limit=True,\n )\n\n if isinstance(self.query, Message):\n input_text = self.query.text\n elif isinstance(self.query, str):\n input_text = self.query\n else:\n msg = f\"Query type {type(self.query)} not supported.\"\n raise TypeError(msg)\n\n documents = self_query_retriever.invoke(input=input_text, config={\"callbacks\": self.get_langchain_callbacks()})\n data = [Data.from_document(document) for document in documents]\n self.status = data\n return data\n" }, "document_content_description": { "_input_type": "MessageTextInput", @@ -84761,7 +84827,7 @@ }, "SemanticTextSplitter": { "base_classes": [ - "Data" + "JSON" ], "beta": true, "conditional_paths": [], @@ -84783,7 +84849,7 @@ "icon": "LangChain", "legacy": false, "metadata": { - "code_hash": "e9178757dea0", + "code_hash": "8a7e7a5a39ed", "dependencies": { "dependencies": [ { @@ -84813,10 +84879,10 @@ "group_outputs": false, "method": "split_text", "name": "chunks", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -84908,7 +84974,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from langchain.docstore.document import Document\nfrom langchain_experimental.text_splitter import SemanticChunker\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.io import (\n DropdownInput,\n FloatInput,\n HandleInput,\n IntInput,\n MessageTextInput,\n Output,\n)\nfrom lfx.schema.data import Data\n\n\nclass SemanticTextSplitterComponent(LCTextSplitterComponent):\n \"\"\"Split text into semantically meaningful chunks using semantic similarity.\"\"\"\n\n display_name: str = \"Semantic Text Splitter\"\n name: str = \"SemanticTextSplitter\"\n description: str = \"Split text into semantically meaningful chunks using semantic similarity.\"\n documentation = \"https://python.langchain.com/docs/how_to/semantic-chunker/\"\n beta = True # this component is beta because it is imported from langchain_experimental\n icon = \"LangChain\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Data Inputs\",\n info=\"List of Data objects containing text and metadata to split.\",\n input_types=[\"Data\"],\n is_list=True,\n required=True,\n ),\n HandleInput(\n name=\"embeddings\",\n display_name=\"Embeddings\",\n info=\"Embeddings model to use for semantic similarity. Required.\",\n input_types=[\"Embeddings\"],\n is_list=False,\n required=True,\n ),\n DropdownInput(\n name=\"breakpoint_threshold_type\",\n display_name=\"Breakpoint Threshold Type\",\n info=(\n \"Method to determine breakpoints. Options: 'percentile', \"\n \"'standard_deviation', 'interquartile'. Defaults to 'percentile'.\"\n ),\n value=\"percentile\",\n options=[\"percentile\", \"standard_deviation\", \"interquartile\"],\n ),\n FloatInput(\n name=\"breakpoint_threshold_amount\",\n display_name=\"Breakpoint Threshold Amount\",\n info=\"Numerical amount for the breakpoint threshold.\",\n value=0.5,\n ),\n IntInput(\n name=\"number_of_chunks\",\n display_name=\"Number of Chunks\",\n info=\"Number of chunks to split the text into.\",\n value=5,\n ),\n MessageTextInput(\n name=\"sentence_split_regex\",\n display_name=\"Sentence Split Regex\",\n info=\"Regular expression to split sentences. Optional.\",\n value=\"\",\n advanced=True,\n ),\n IntInput(\n name=\"buffer_size\",\n display_name=\"Buffer Size\",\n info=\"Size of the buffer.\",\n value=0,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Chunks\", name=\"chunks\", method=\"split_text\"),\n ]\n\n def _docs_to_data(self, docs: list[Document]) -> list[Data]:\n \"\"\"Convert a list of Document objects to Data objects.\"\"\"\n return [Data(text=doc.page_content, data=doc.metadata) for doc in docs]\n\n def split_text(self) -> list[Data]:\n \"\"\"Split the input data into semantically meaningful chunks.\"\"\"\n try:\n embeddings = getattr(self, \"embeddings\", None)\n if embeddings is None:\n error_msg = \"An embeddings model is required for SemanticTextSplitter.\"\n raise ValueError(error_msg)\n\n if not self.data_inputs:\n error_msg = \"Data inputs cannot be empty.\"\n raise ValueError(error_msg)\n\n documents = []\n for _input in self.data_inputs:\n if isinstance(_input, Data):\n documents.append(_input.to_lc_document())\n else:\n error_msg = f\"Invalid data input type: {_input}\"\n raise TypeError(error_msg)\n\n if not documents:\n error_msg = \"No valid Data objects found in data_inputs.\"\n raise ValueError(error_msg)\n\n texts = [doc.page_content for doc in documents]\n metadatas = [doc.metadata for doc in documents]\n\n splitter_params = {\n \"embeddings\": embeddings,\n \"breakpoint_threshold_type\": self.breakpoint_threshold_type or \"percentile\",\n \"breakpoint_threshold_amount\": self.breakpoint_threshold_amount,\n \"number_of_chunks\": self.number_of_chunks,\n \"buffer_size\": self.buffer_size,\n }\n\n if self.sentence_split_regex:\n splitter_params[\"sentence_split_regex\"] = self.sentence_split_regex\n\n splitter = SemanticChunker(**splitter_params)\n docs = splitter.create_documents(texts, metadatas=metadatas)\n\n data = self._docs_to_data(docs)\n self.status = data\n\n except Exception as e:\n error_msg = f\"An error occurred during semantic splitting: {e}\"\n raise RuntimeError(error_msg) from e\n\n else:\n return data\n" + "value": "from langchain.docstore.document import Document\nfrom langchain_experimental.text_splitter import SemanticChunker\n\nfrom lfx.base.textsplitters.model import LCTextSplitterComponent\nfrom lfx.io import (\n DropdownInput,\n FloatInput,\n HandleInput,\n IntInput,\n MessageTextInput,\n Output,\n)\nfrom lfx.schema.data import Data\n\n\nclass SemanticTextSplitterComponent(LCTextSplitterComponent):\n \"\"\"Split text into semantically meaningful chunks using semantic similarity.\"\"\"\n\n display_name: str = \"Semantic Text Splitter\"\n name: str = \"SemanticTextSplitter\"\n description: str = \"Split text into semantically meaningful chunks using semantic similarity.\"\n documentation = \"https://python.langchain.com/docs/how_to/semantic-chunker/\"\n beta = True # this component is beta because it is imported from langchain_experimental\n icon = \"LangChain\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Data Inputs\",\n info=\"List of Data objects containing text and metadata to split.\",\n input_types=[\"Data\", \"JSON\"],\n is_list=True,\n required=True,\n ),\n HandleInput(\n name=\"embeddings\",\n display_name=\"Embeddings\",\n info=\"Embeddings model to use for semantic similarity. Required.\",\n input_types=[\"Embeddings\"],\n is_list=False,\n required=True,\n ),\n DropdownInput(\n name=\"breakpoint_threshold_type\",\n display_name=\"Breakpoint Threshold Type\",\n info=(\n \"Method to determine breakpoints. Options: 'percentile', \"\n \"'standard_deviation', 'interquartile'. Defaults to 'percentile'.\"\n ),\n value=\"percentile\",\n options=[\"percentile\", \"standard_deviation\", \"interquartile\"],\n ),\n FloatInput(\n name=\"breakpoint_threshold_amount\",\n display_name=\"Breakpoint Threshold Amount\",\n info=\"Numerical amount for the breakpoint threshold.\",\n value=0.5,\n ),\n IntInput(\n name=\"number_of_chunks\",\n display_name=\"Number of Chunks\",\n info=\"Number of chunks to split the text into.\",\n value=5,\n ),\n MessageTextInput(\n name=\"sentence_split_regex\",\n display_name=\"Sentence Split Regex\",\n info=\"Regular expression to split sentences. Optional.\",\n value=\"\",\n advanced=True,\n ),\n IntInput(\n name=\"buffer_size\",\n display_name=\"Buffer Size\",\n info=\"Size of the buffer.\",\n value=0,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Chunks\", name=\"chunks\", method=\"split_text\"),\n ]\n\n def _docs_to_data(self, docs: list[Document]) -> list[Data]:\n \"\"\"Convert a list of Document objects to Data objects.\"\"\"\n return [Data(text=doc.page_content, data=doc.metadata) for doc in docs]\n\n def split_text(self) -> list[Data]:\n \"\"\"Split the input data into semantically meaningful chunks.\"\"\"\n try:\n embeddings = getattr(self, \"embeddings\", None)\n if embeddings is None:\n error_msg = \"An embeddings model is required for SemanticTextSplitter.\"\n raise ValueError(error_msg)\n\n if not self.data_inputs:\n error_msg = \"Data inputs cannot be empty.\"\n raise ValueError(error_msg)\n\n documents = []\n for _input in self.data_inputs:\n if isinstance(_input, Data):\n documents.append(_input.to_lc_document())\n else:\n error_msg = f\"Invalid data input type: {_input}\"\n raise TypeError(error_msg)\n\n if not documents:\n error_msg = \"No valid Data objects found in data_inputs.\"\n raise ValueError(error_msg)\n\n texts = [doc.page_content for doc in documents]\n metadatas = [doc.metadata for doc in documents]\n\n splitter_params = {\n \"embeddings\": embeddings,\n \"breakpoint_threshold_type\": self.breakpoint_threshold_type or \"percentile\",\n \"breakpoint_threshold_amount\": self.breakpoint_threshold_amount,\n \"number_of_chunks\": self.number_of_chunks,\n \"buffer_size\": self.buffer_size,\n }\n\n if self.sentence_split_regex:\n splitter_params[\"sentence_split_regex\"] = self.sentence_split_regex\n\n splitter = SemanticChunker(**splitter_params)\n docs = splitter.create_documents(texts, metadatas=metadatas)\n\n data = self._docs_to_data(docs)\n self.status = data\n\n except Exception as e:\n error_msg = f\"An error occurred during semantic splitting: {e}\"\n raise RuntimeError(error_msg) from e\n\n else:\n return data\n" }, "data_inputs": { "_input_type": "HandleInput", @@ -84917,7 +84983,8 @@ "dynamic": false, "info": "List of Data objects containing text and metadata to split.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -85004,7 +85071,7 @@ }, "SpiderTool": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -85055,10 +85122,10 @@ "group_outputs": false, "method": "crawl", "name": "content", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -85483,13 +85550,14 @@ "value": "https://us-south.ml.cloud.ibm.com" }, "chat_history": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": true, "display_name": "Chat Memory", "dynamic": false, "info": "This input stores the chat history, allowing the agent to remember previous conversations.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -86293,13 +86361,14 @@ "value": "https://us-south.ml.cloud.ibm.com" }, "chat_history": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": true, "display_name": "Chat History", "dynamic": false, "info": "", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -86567,7 +86636,7 @@ { "LangWatchEvaluator": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -86615,10 +86684,10 @@ "group_outputs": false, "method": "evaluate", "name": "evaluation_result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -87173,7 +87242,7 @@ { "BatchRunComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -87195,7 +87264,7 @@ "icon": "List", "legacy": false, "metadata": { - "code_hash": "8b1ec3b03475", + "code_hash": "f20d52a329ad", "dependencies": { "dependencies": [ { @@ -87225,10 +87294,10 @@ "group_outputs": false, "method": "run_batch", "name": "batch_results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -87272,7 +87341,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport toml # type: ignore[import-untyped]\n\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_model_class,\n update_model_options_in_build_config,\n)\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DataFrameInput, MessageTextInput, ModelInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\nif TYPE_CHECKING:\n from langchain_core.runnables import Runnable\n\n\nclass BatchRunComponent(Component):\n display_name = \"Batch Run\"\n description = \"Runs an LLM on each row of a DataFrame column. If no column is specified, all columns are used.\"\n documentation: str = \"https://docs.langflow.org/batch-run\"\n icon = \"List\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"Instructions\",\n info=\"Multi-line system instruction for all rows in the DataFrame.\",\n required=False,\n ),\n DataFrameInput(\n name=\"df\",\n display_name=\"DataFrame\",\n info=\"The DataFrame whose column (specified by 'column_name') we'll treat as text messages.\",\n required=True,\n ),\n MessageTextInput(\n name=\"column_name\",\n display_name=\"Column Name\",\n info=(\n \"The name of the DataFrame column to treat as text messages. \"\n \"If empty, all columns will be formatted in TOML.\"\n ),\n required=False,\n advanced=False,\n ),\n MessageTextInput(\n name=\"output_column_name\",\n display_name=\"Output Column Name\",\n info=\"Name of the column where the model's response will be stored.\",\n value=\"model_response\",\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"enable_metadata\",\n display_name=\"Enable Metadata\",\n info=\"If True, add metadata to the output DataFrame.\",\n value=False,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"LLM Results\",\n name=\"batch_results\",\n method=\"run_batch\",\n info=\"A DataFrame with all original columns plus the model's response column.\",\n ),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n def _format_row_as_toml(self, row: dict[str, Any]) -> str:\n \"\"\"Convert a dictionary (row) into a TOML-formatted string.\"\"\"\n formatted_dict = {str(col): {\"value\": str(val)} for col, val in row.items()}\n return toml.dumps(formatted_dict)\n\n def _create_base_row(\n self, original_row: dict[str, Any], model_response: str = \"\", batch_index: int = -1\n ) -> dict[str, Any]:\n \"\"\"Create a base row with original columns and additional metadata.\"\"\"\n row = original_row.copy()\n row[self.output_column_name] = model_response\n row[\"batch_index\"] = batch_index\n return row\n\n def _add_metadata(\n self, row: dict[str, Any], *, success: bool = True, system_msg: str = \"\", error: str | None = None\n ) -> None:\n \"\"\"Add metadata to a row if enabled.\"\"\"\n if not self.enable_metadata:\n return\n\n if success:\n row[\"metadata\"] = {\n \"has_system_message\": bool(system_msg),\n \"input_length\": len(row.get(\"text_input\", \"\")),\n \"response_length\": len(row[self.output_column_name]),\n \"processing_status\": \"success\",\n }\n else:\n row[\"metadata\"] = {\n \"error\": error,\n \"processing_status\": \"failed\",\n }\n\n async def run_batch(self) -> DataFrame:\n \"\"\"Process each row in df[column_name] with the language model asynchronously.\"\"\"\n # Check if model is already an instance (for testing) or needs to be instantiated\n if isinstance(self.model, list):\n # Extract model configuration\n model_selection = self.model[0]\n model_name = model_selection.get(\"name\")\n provider = model_selection.get(\"provider\")\n metadata = model_selection.get(\"metadata\", {})\n\n # Get model class and parameters from metadata\n model_class_name = metadata.get(\"model_class\")\n if not model_class_name:\n msg = f\"No model class defined for {model_name}\"\n raise ValueError(msg)\n model_class = get_model_class(model_class_name)\n\n api_key_param = metadata.get(\"api_key_param\", \"api_key\")\n model_name_param = metadata.get(\"model_name_param\", \"model\")\n\n # Get API key from global variables\n from lfx.base.models.unified_models import get_api_key_for_provider\n\n api_key = get_api_key_for_provider(self.user_id, provider, self.api_key)\n\n if not api_key and provider != \"Ollama\":\n msg = f\"{provider} API key is required. Please configure it globally.\"\n raise ValueError(msg)\n\n # Instantiate the model\n kwargs = {\n model_name_param: model_name,\n api_key_param: api_key,\n }\n model: Runnable = model_class(**kwargs)\n else:\n # Model is already an instance (typically in tests)\n model = self.model\n\n system_msg = self.system_message or \"\"\n df: DataFrame = self.df\n col_name = self.column_name or \"\"\n\n # Validate inputs first\n if not isinstance(df, DataFrame):\n msg = f\"Expected DataFrame input, got {type(df)}\"\n raise TypeError(msg)\n\n if col_name and col_name not in df.columns:\n msg = f\"Column '{col_name}' not found in the DataFrame. Available columns: {', '.join(df.columns)}\"\n raise ValueError(msg)\n\n try:\n # Determine text input for each row\n if col_name:\n user_texts = df[col_name].astype(str).tolist()\n else:\n user_texts = [\n self._format_row_as_toml(cast(\"dict[str, Any]\", row)) for row in df.to_dict(orient=\"records\")\n ]\n\n total_rows = len(user_texts)\n await logger.ainfo(f\"Processing {total_rows} rows with batch run\")\n\n # Prepare the batch of conversations\n conversations = [\n [{\"role\": \"system\", \"content\": system_msg}, {\"role\": \"user\", \"content\": text}]\n if system_msg\n else [{\"role\": \"user\", \"content\": text}]\n for text in user_texts\n ]\n\n # Configure the model with project info and callbacks\n # Some models (e.g., ChatWatsonx) may have serialization issues with with_config()\n # due to SecretStr or other non-serializable attributes\n try:\n model = model.with_config(\n {\n \"run_name\": self.display_name,\n \"project_name\": self.get_project_name(),\n \"callbacks\": self.get_langchain_callbacks(),\n }\n )\n except (TypeError, ValueError, AttributeError) as e:\n # Log warning and continue without configuration\n await logger.awarning(\n f\"Could not configure model with callbacks and project info: {e!s}. \"\n \"Proceeding with batch processing without configuration.\"\n )\n # Process batches and track progress\n responses_with_idx = list(\n zip(\n range(len(conversations)),\n await model.abatch(list(conversations)),\n strict=True,\n )\n )\n\n # Sort by index to maintain order\n responses_with_idx.sort(key=lambda x: x[0])\n\n # Build the final data with enhanced metadata\n rows: list[dict[str, Any]] = []\n for idx, (original_row, response) in enumerate(\n zip(df.to_dict(orient=\"records\"), responses_with_idx, strict=False)\n ):\n response_text = response[1].content if hasattr(response[1], \"content\") else str(response[1])\n row = self._create_base_row(\n cast(\"dict[str, Any]\", original_row), model_response=response_text, batch_index=idx\n )\n self._add_metadata(row, success=True, system_msg=system_msg)\n rows.append(row)\n\n # Log progress\n if (idx + 1) % max(1, total_rows // 10) == 0:\n await logger.ainfo(f\"Processed {idx + 1}/{total_rows} rows\")\n\n await logger.ainfo(\"Batch processing completed successfully\")\n return DataFrame(rows)\n\n except (KeyError, AttributeError) as e:\n # Handle data structure and attribute access errors\n await logger.aerror(f\"Data processing error: {e!s}\")\n error_row = self._create_base_row(dict.fromkeys(df.columns, \"\"), model_response=\"\", batch_index=-1)\n self._add_metadata(error_row, success=False, error=str(e))\n return DataFrame([error_row])\n" + "value": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, cast\n\nimport toml # type: ignore[import-untyped]\n\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_model_class,\n update_model_options_in_build_config,\n)\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DataFrameInput, MessageTextInput, ModelInput, MultilineInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\nif TYPE_CHECKING:\n from langchain_core.runnables import Runnable\n\n\nclass BatchRunComponent(Component):\n display_name = \"Batch Run\"\n description = \"Runs an LLM on each row of a DataFrame column. If no column is specified, all columns are used.\"\n documentation: str = \"https://docs.langflow.org/batch-run\"\n icon = \"List\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"Instructions\",\n info=\"Multi-line system instruction for all rows in the DataFrame.\",\n required=False,\n ),\n DataFrameInput(\n name=\"df\",\n display_name=\"Table\",\n info=\"The DataFrame whose column (specified by 'column_name') we'll treat as text messages.\",\n required=True,\n ),\n MessageTextInput(\n name=\"column_name\",\n display_name=\"Column Name\",\n info=(\n \"The name of the DataFrame column to treat as text messages. \"\n \"If empty, all columns will be formatted in TOML.\"\n ),\n required=False,\n advanced=False,\n ),\n MessageTextInput(\n name=\"output_column_name\",\n display_name=\"Output Column Name\",\n info=\"Name of the column where the model's response will be stored.\",\n value=\"model_response\",\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"enable_metadata\",\n display_name=\"Enable Metadata\",\n info=\"If True, add metadata to the output DataFrame.\",\n value=False,\n required=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"LLM Results\",\n name=\"batch_results\",\n method=\"run_batch\",\n info=\"A DataFrame with all original columns plus the model's response column.\",\n ),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n def _format_row_as_toml(self, row: dict[str, Any]) -> str:\n \"\"\"Convert a dictionary (row) into a TOML-formatted string.\"\"\"\n formatted_dict = {str(col): {\"value\": str(val)} for col, val in row.items()}\n return toml.dumps(formatted_dict)\n\n def _create_base_row(\n self, original_row: dict[str, Any], model_response: str = \"\", batch_index: int = -1\n ) -> dict[str, Any]:\n \"\"\"Create a base row with original columns and additional metadata.\"\"\"\n row = original_row.copy()\n row[self.output_column_name] = model_response\n row[\"batch_index\"] = batch_index\n return row\n\n def _add_metadata(\n self, row: dict[str, Any], *, success: bool = True, system_msg: str = \"\", error: str | None = None\n ) -> None:\n \"\"\"Add metadata to a row if enabled.\"\"\"\n if not self.enable_metadata:\n return\n\n if success:\n row[\"metadata\"] = {\n \"has_system_message\": bool(system_msg),\n \"input_length\": len(row.get(\"text_input\", \"\")),\n \"response_length\": len(row[self.output_column_name]),\n \"processing_status\": \"success\",\n }\n else:\n row[\"metadata\"] = {\n \"error\": error,\n \"processing_status\": \"failed\",\n }\n\n async def run_batch(self) -> DataFrame:\n \"\"\"Process each row in df[column_name] with the language model asynchronously.\"\"\"\n # Check if model is already an instance (for testing) or needs to be instantiated\n if isinstance(self.model, list):\n # Extract model configuration\n model_selection = self.model[0]\n model_name = model_selection.get(\"name\")\n provider = model_selection.get(\"provider\")\n metadata = model_selection.get(\"metadata\", {})\n\n # Get model class and parameters from metadata\n model_class_name = metadata.get(\"model_class\")\n if not model_class_name:\n msg = f\"No model class defined for {model_name}\"\n raise ValueError(msg)\n model_class = get_model_class(model_class_name)\n\n api_key_param = metadata.get(\"api_key_param\", \"api_key\")\n model_name_param = metadata.get(\"model_name_param\", \"model\")\n\n # Get API key from global variables\n from lfx.base.models.unified_models import get_api_key_for_provider\n\n api_key = get_api_key_for_provider(self.user_id, provider, self.api_key)\n\n if not api_key and provider != \"Ollama\":\n msg = f\"{provider} API key is required. Please configure it globally.\"\n raise ValueError(msg)\n\n # Instantiate the model\n kwargs = {\n model_name_param: model_name,\n api_key_param: api_key,\n }\n model: Runnable = model_class(**kwargs)\n else:\n # Model is already an instance (typically in tests)\n model = self.model\n\n system_msg = self.system_message or \"\"\n df: DataFrame = self.df\n col_name = self.column_name or \"\"\n\n # Validate inputs first\n if not isinstance(df, DataFrame):\n msg = f\"Expected DataFrame input, got {type(df)}\"\n raise TypeError(msg)\n\n if col_name and col_name not in df.columns:\n msg = f\"Column '{col_name}' not found in the DataFrame. Available columns: {', '.join(df.columns)}\"\n raise ValueError(msg)\n\n try:\n # Determine text input for each row\n if col_name:\n user_texts = df[col_name].astype(str).tolist()\n else:\n user_texts = [\n self._format_row_as_toml(cast(\"dict[str, Any]\", row)) for row in df.to_dict(orient=\"records\")\n ]\n\n total_rows = len(user_texts)\n await logger.ainfo(f\"Processing {total_rows} rows with batch run\")\n\n # Prepare the batch of conversations\n conversations = [\n [{\"role\": \"system\", \"content\": system_msg}, {\"role\": \"user\", \"content\": text}]\n if system_msg\n else [{\"role\": \"user\", \"content\": text}]\n for text in user_texts\n ]\n\n # Configure the model with project info and callbacks\n # Some models (e.g., ChatWatsonx) may have serialization issues with with_config()\n # due to SecretStr or other non-serializable attributes\n try:\n model = model.with_config(\n {\n \"run_name\": self.display_name,\n \"project_name\": self.get_project_name(),\n \"callbacks\": self.get_langchain_callbacks(),\n }\n )\n except (TypeError, ValueError, AttributeError) as e:\n # Log warning and continue without configuration\n await logger.awarning(\n f\"Could not configure model with callbacks and project info: {e!s}. \"\n \"Proceeding with batch processing without configuration.\"\n )\n # Process batches and track progress\n responses_with_idx = list(\n zip(\n range(len(conversations)),\n await model.abatch(list(conversations)),\n strict=True,\n )\n )\n\n # Sort by index to maintain order\n responses_with_idx.sort(key=lambda x: x[0])\n\n # Build the final data with enhanced metadata\n rows: list[dict[str, Any]] = []\n for idx, (original_row, response) in enumerate(\n zip(df.to_dict(orient=\"records\"), responses_with_idx, strict=False)\n ):\n response_text = response[1].content if hasattr(response[1], \"content\") else str(response[1])\n row = self._create_base_row(\n cast(\"dict[str, Any]\", original_row), model_response=response_text, batch_index=idx\n )\n self._add_metadata(row, success=True, system_msg=system_msg)\n rows.append(row)\n\n # Log progress\n if (idx + 1) % max(1, total_rows // 10) == 0:\n await logger.ainfo(f\"Processed {idx + 1}/{total_rows} rows\")\n\n await logger.ainfo(\"Batch processing completed successfully\")\n return DataFrame(rows)\n\n except (KeyError, AttributeError) as e:\n # Handle data structure and attribute access errors\n await logger.aerror(f\"Data processing error: {e!s}\")\n error_row = self._create_base_row(dict.fromkeys(df.columns, \"\"), model_response=\"\", batch_index=-1)\n self._add_metadata(error_row, success=False, error=str(e))\n return DataFrame([error_row])\n" }, "column_name": { "_input_type": "MessageTextInput", @@ -87302,11 +87371,12 @@ "df": { "_input_type": "DataFrameInput", "advanced": false, - "display_name": "DataFrame", + "display_name": "Table", "dynamic": false, "info": "The DataFrame whose column (specified by 'column_name') we'll treat as text messages.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -87439,7 +87509,7 @@ }, "GuardrailValidator": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -87483,10 +87553,10 @@ "group_outputs": true, "method": "process_check", "name": "pass_result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -87497,10 +87567,10 @@ "group_outputs": true, "method": "process_check", "name": "failed_result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -87731,6 +87801,7 @@ "LLMSelectorComponent": { "base_classes": [ "Data", + "JSON", "Message" ], "beta": false, @@ -87796,7 +87867,8 @@ "selected": "Data", "tool_mode": true, "types": [ - "Data" + "Data", + "JSON" ], "value": "__UNDEFINED__" }, @@ -88003,9 +88075,9 @@ }, "Smart Transform": { "base_classes": [ - "Data", - "DataFrame", - "Message" + "JSON", + "Message", + "Table" ], "beta": false, "conditional_paths": [], @@ -88026,7 +88098,7 @@ "icon": "square-function", "legacy": false, "metadata": { - "code_hash": "9912fe8c7f1b", + "code_hash": "4fb127dc371c", "dependencies": { "dependencies": [ { @@ -88048,10 +88120,10 @@ "group_outputs": false, "method": "process_as_data", "name": "data_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -88062,10 +88134,10 @@ "group_outputs": false, "method": "process_as_dataframe", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -88123,17 +88195,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom collections.abc import Callable # noqa: TC003 - required at runtime for dynamic exec()\nfrom typing import Any\n\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, IntInput, ModelInput, MultilineInput, Output, SecretStrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.constants import MESSAGE_SENDER_AI\n\nTEXT_TRANSFORM_PROMPT = (\n \"Given this text, create a Python lambda function that transforms it \"\n \"according to the instruction.\\n\"\n \"The lambda should take a string parameter and return the transformed string.\\n\\n\"\n \"Text Preview:\\n{text_preview}\\n\\n\"\n \"Instruction: {instruction}\\n\\n\"\n \"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\\n\"\n \"Just a string starting with lambda.\\n\"\n \"Example: lambda text: text.upper()\"\n)\n\nDATA_TRANSFORM_PROMPT = (\n \"Given this data structure and examples, create a Python lambda function \"\n \"that implements the following instruction:\\n\\n\"\n \"Data Structure:\\n{dump_structure}\\n\\n\"\n \"Example Items:\\n{data_sample}\\n\\n\"\n \"Instruction: {instruction}\\n\\n\"\n \"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\\n\"\n \"Just a string starting with lambda.\"\n)\n\n\nclass LambdaFilterComponent(Component):\n display_name = \"Smart Transform\"\n description = \"Uses an LLM to generate a function for filtering or transforming structured data and messages.\"\n documentation: str = \"https://docs.langflow.org/smart-transform\"\n icon = \"square-function\"\n name = \"Smart Transform\"\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The structured data or text messages to filter or transform using a lambda function.\",\n input_types=[\"Data\", \"DataFrame\", \"Message\"],\n is_list=True,\n required=True,\n ),\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MultilineInput(\n name=\"filter_instruction\",\n display_name=\"Instructions\",\n info=(\n \"Natural language instructions for how to filter or transform the data using a lambda function. \"\n \"Examples: 'Filter the data to only include items where status is active', \"\n \"'Convert the text to uppercase', 'Keep only first 100 characters'\"\n ),\n value=\"Transform the data to...\",\n required=True,\n ),\n IntInput(\n name=\"sample_size\",\n display_name=\"Sample Size\",\n info=\"For large datasets, number of items to sample from head/tail.\",\n value=1000,\n advanced=True,\n ),\n IntInput(\n name=\"max_size\",\n display_name=\"Max Size\",\n info=\"Number of characters for the data to be considered large.\",\n value=30000,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Output\",\n name=\"data_output\",\n method=\"process_as_data\",\n ),\n Output(\n display_name=\"Output\",\n name=\"dataframe_output\",\n method=\"process_as_dataframe\",\n ),\n Output(\n display_name=\"Output\",\n name=\"message_output\",\n method=\"process_as_message\",\n ),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n def get_data_structure(self, data):\n \"\"\"Extract the structure of data, replacing values with their types.\"\"\"\n if isinstance(data, list):\n # For lists, get structure of first item if available\n if data:\n return [self.get_data_structure(data[0])]\n return []\n if isinstance(data, dict):\n return {k: self.get_data_structure(v) for k, v in data.items()}\n # For primitive types, return the type name\n return type(data).__name__\n\n def _validate_lambda(self, lambda_text: str) -> bool:\n \"\"\"Validate the provided lambda function text.\"\"\"\n # Return False if the lambda function does not start with 'lambda' or does not contain a colon\n return lambda_text.strip().startswith(\"lambda\") and \":\" in lambda_text\n\n def _get_input_type_name(self) -> str:\n \"\"\"Detect and return the input type name for error messages.\"\"\"\n if isinstance(self.data, Message):\n return \"Message\"\n if isinstance(self.data, DataFrame):\n return \"DataFrame\"\n if isinstance(self.data, Data):\n return \"Data\"\n if isinstance(self.data, list) and len(self.data) > 0:\n first = self.data[0]\n if isinstance(first, Message):\n return \"Message\"\n if isinstance(first, DataFrame):\n return \"DataFrame\"\n if isinstance(first, Data):\n return \"Data\"\n return \"unknown\"\n\n def _extract_message_text(self) -> str:\n \"\"\"Extract text content from Message input(s).\"\"\"\n if isinstance(self.data, Message):\n return self.data.text or \"\"\n\n texts = [msg.text or \"\" for msg in self.data if isinstance(msg, Message)]\n return \"\\n\\n\".join(texts) if len(texts) > 1 else (texts[0] if texts else \"\")\n\n def _extract_structured_data(self) -> dict | list:\n \"\"\"Extract structured data from Data or DataFrame input(s).\"\"\"\n if isinstance(self.data, DataFrame):\n return self.data.to_dict(orient=\"records\")\n\n if hasattr(self.data, \"data\"):\n return self.data.data\n\n if not isinstance(self.data, list):\n return self.data\n\n combined_data: list[dict] = []\n for item in self.data:\n if isinstance(item, DataFrame):\n combined_data.extend(item.to_dict(orient=\"records\"))\n elif hasattr(item, \"data\"):\n if isinstance(item.data, dict):\n combined_data.append(item.data)\n elif isinstance(item.data, list):\n combined_data.extend(item.data)\n\n if len(combined_data) == 1 and isinstance(combined_data[0], dict):\n return combined_data[0]\n if len(combined_data) == 0:\n return {}\n return combined_data\n\n def _is_message_input(self) -> bool:\n \"\"\"Check if input is Message type.\"\"\"\n if isinstance(self.data, Message):\n return True\n return isinstance(self.data, list) and len(self.data) > 0 and isinstance(self.data[0], Message)\n\n def _build_text_prompt(self, text: str) -> str:\n \"\"\"Build prompt for text/Message transformation.\"\"\"\n text_length = len(text)\n if text_length > self.max_size:\n text_preview = (\n f\"Text length: {text_length} characters\\n\\n\"\n f\"First {self.sample_size} characters:\\n{text[: self.sample_size]}\\n\\n\"\n f\"Last {self.sample_size} characters:\\n{text[-self.sample_size :]}\"\n )\n else:\n text_preview = text\n\n return TEXT_TRANSFORM_PROMPT.format(text_preview=text_preview, instruction=self.filter_instruction)\n\n def _build_data_prompt(self, data: dict | list) -> str:\n \"\"\"Build prompt for structured data transformation.\"\"\"\n dump = json.dumps(data)\n dump_structure = json.dumps(self.get_data_structure(data))\n\n if len(dump) > self.max_size:\n data_sample = (\n f\"Data is too long to display...\\n\\nFirst lines (head): {dump[: self.sample_size]}\\n\\n\"\n f\"Last lines (tail): {dump[-self.sample_size :]}\"\n )\n else:\n data_sample = dump\n\n return DATA_TRANSFORM_PROMPT.format(\n dump_structure=dump_structure, data_sample=data_sample, instruction=self.filter_instruction\n )\n\n def _parse_lambda_from_response(self, response_text: str) -> Callable[[Any], Any]:\n \"\"\"Extract and validate lambda function from LLM response.\"\"\"\n lambda_match = re.search(r\"lambda\\s+\\w+\\s*:.*?(?=\\n|$)\", response_text)\n if not lambda_match:\n msg = f\"Could not find lambda in response: {response_text}\"\n raise ValueError(msg)\n\n lambda_text = lambda_match.group().strip()\n self.log(f\"Generated lambda: {lambda_text}\")\n\n if not self._validate_lambda(lambda_text):\n msg = f\"Invalid lambda format: {lambda_text}\"\n raise ValueError(msg)\n\n return eval(lambda_text) # noqa: S307\n\n async def _execute_lambda(self) -> Any:\n \"\"\"Generate and execute a lambda function based on input type.\"\"\"\n if self._is_message_input():\n data: Any = self._extract_message_text()\n prompt = self._build_text_prompt(data)\n else:\n data = self._extract_structured_data()\n prompt = self._build_data_prompt(data)\n\n llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)\n response = await llm.ainvoke(prompt)\n response_text = response.content if hasattr(response, \"content\") else str(response)\n\n fn = self._parse_lambda_from_response(response_text)\n return fn(data)\n\n def _handle_process_error(self, error: Exception, output_type: str) -> None:\n \"\"\"Handle errors from process methods with context-aware messages.\"\"\"\n input_type = self._get_input_type_name()\n error_msg = (\n f\"Failed to convert result to {output_type} output. \"\n f\"Error: {error}. \"\n f\"Input type was {input_type}. \"\n f\"Try using the same output type as the input.\"\n )\n raise ValueError(error_msg) from error\n\n def _convert_result_to_data(self, result: Any) -> Data:\n \"\"\"Convert lambda result to Data object.\"\"\"\n if isinstance(result, dict):\n return Data(data=result)\n if isinstance(result, list):\n return Data(data={\"_results\": result})\n return Data(data={\"text\": str(result)})\n\n def _convert_result_to_dataframe(self, result: Any) -> DataFrame:\n \"\"\"Convert lambda result to DataFrame object.\"\"\"\n if isinstance(result, list):\n if all(isinstance(item, dict) for item in result):\n return DataFrame(result)\n return DataFrame([{\"value\": item} for item in result])\n if isinstance(result, dict):\n return DataFrame([result])\n return DataFrame([{\"value\": str(result)}])\n\n def _convert_result_to_message(self, result: Any) -> Message:\n \"\"\"Convert lambda result to Message object.\"\"\"\n if isinstance(result, str):\n return Message(text=result, sender=MESSAGE_SENDER_AI)\n if isinstance(result, list):\n text = \"\\n\".join(str(item) for item in result)\n return Message(text=text, sender=MESSAGE_SENDER_AI)\n if isinstance(result, dict):\n text = json.dumps(result, indent=2)\n return Message(text=text, sender=MESSAGE_SENDER_AI)\n return Message(text=str(result), sender=MESSAGE_SENDER_AI)\n\n async def process_as_data(self) -> Data:\n \"\"\"Process the data and return as a Data object.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_data(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"Data\")\n\n async def process_as_dataframe(self) -> DataFrame:\n \"\"\"Process the data and return as a DataFrame.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_dataframe(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"DataFrame\")\n\n async def process_as_message(self) -> Message:\n \"\"\"Process the data and return as a Message.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_message(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"Message\")\n" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom collections.abc import Callable # noqa: TC003 - required at runtime for dynamic exec()\nfrom typing import Any\n\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, IntInput, ModelInput, MultilineInput, Output, SecretStrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.constants import MESSAGE_SENDER_AI\n\nTEXT_TRANSFORM_PROMPT = (\n \"Given this text, create a Python lambda function that transforms it \"\n \"according to the instruction.\\n\"\n \"The lambda should take a string parameter and return the transformed string.\\n\\n\"\n \"Text Preview:\\n{text_preview}\\n\\n\"\n \"Instruction: {instruction}\\n\\n\"\n \"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\\n\"\n \"Just a string starting with lambda.\\n\"\n \"Example: lambda text: text.upper()\"\n)\n\nDATA_TRANSFORM_PROMPT = (\n \"Given this data structure and examples, create a Python lambda function \"\n \"that implements the following instruction:\\n\\n\"\n \"Data Structure:\\n{dump_structure}\\n\\n\"\n \"Example Items:\\n{data_sample}\\n\\n\"\n \"Instruction: {instruction}\\n\\n\"\n \"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\\n\"\n \"Just a string starting with lambda.\"\n)\n\n\nclass LambdaFilterComponent(Component):\n display_name = \"Smart Transform\"\n description = \"Uses an LLM to generate a function for filtering or transforming structured data and messages.\"\n documentation: str = \"https://docs.langflow.org/smart-transform\"\n icon = \"square-function\"\n name = \"Smart Transform\"\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"JSON\",\n info=\"The structured data or text messages to filter or transform using a lambda function.\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\", \"Message\"],\n is_list=True,\n required=True,\n ),\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MultilineInput(\n name=\"filter_instruction\",\n display_name=\"Instructions\",\n info=(\n \"Natural language instructions for how to filter or transform the data using a lambda function. \"\n \"Examples: 'Filter the data to only include items where status is active', \"\n \"'Convert the text to uppercase', 'Keep only first 100 characters'\"\n ),\n value=\"Transform the data to...\",\n required=True,\n ),\n IntInput(\n name=\"sample_size\",\n display_name=\"Sample Size\",\n info=\"For large datasets, number of items to sample from head/tail.\",\n value=1000,\n advanced=True,\n ),\n IntInput(\n name=\"max_size\",\n display_name=\"Max Size\",\n info=\"Number of characters for the data to be considered large.\",\n value=30000,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Output\",\n name=\"data_output\",\n method=\"process_as_data\",\n ),\n Output(\n display_name=\"Output\",\n name=\"dataframe_output\",\n method=\"process_as_dataframe\",\n ),\n Output(\n display_name=\"Output\",\n name=\"message_output\",\n method=\"process_as_message\",\n ),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n def get_data_structure(self, data):\n \"\"\"Extract the structure of data, replacing values with their types.\"\"\"\n if isinstance(data, list):\n # For lists, get structure of first item if available\n if data:\n return [self.get_data_structure(data[0])]\n return []\n if isinstance(data, dict):\n return {k: self.get_data_structure(v) for k, v in data.items()}\n # For primitive types, return the type name\n return type(data).__name__\n\n def _validate_lambda(self, lambda_text: str) -> bool:\n \"\"\"Validate the provided lambda function text.\"\"\"\n # Return False if the lambda function does not start with 'lambda' or does not contain a colon\n return lambda_text.strip().startswith(\"lambda\") and \":\" in lambda_text\n\n def _get_input_type_name(self) -> str:\n \"\"\"Detect and return the input type name for error messages.\"\"\"\n if isinstance(self.data, Message):\n return \"Message\"\n if isinstance(self.data, DataFrame):\n return \"DataFrame\"\n if isinstance(self.data, Data):\n return \"Data\"\n if isinstance(self.data, list) and len(self.data) > 0:\n first = self.data[0]\n if isinstance(first, Message):\n return \"Message\"\n if isinstance(first, DataFrame):\n return \"DataFrame\"\n if isinstance(first, Data):\n return \"Data\"\n return \"unknown\"\n\n def _extract_message_text(self) -> str:\n \"\"\"Extract text content from Message input(s).\"\"\"\n if isinstance(self.data, Message):\n return self.data.text or \"\"\n\n texts = [msg.text or \"\" for msg in self.data if isinstance(msg, Message)]\n return \"\\n\\n\".join(texts) if len(texts) > 1 else (texts[0] if texts else \"\")\n\n def _extract_structured_data(self) -> dict | list:\n \"\"\"Extract structured data from Data or DataFrame input(s).\"\"\"\n if isinstance(self.data, DataFrame):\n return self.data.to_dict(orient=\"records\")\n\n if hasattr(self.data, \"data\"):\n return self.data.data\n\n if not isinstance(self.data, list):\n return self.data\n\n combined_data: list[dict] = []\n for item in self.data:\n if isinstance(item, DataFrame):\n combined_data.extend(item.to_dict(orient=\"records\"))\n elif hasattr(item, \"data\"):\n if isinstance(item.data, dict):\n combined_data.append(item.data)\n elif isinstance(item.data, list):\n combined_data.extend(item.data)\n\n if len(combined_data) == 1 and isinstance(combined_data[0], dict):\n return combined_data[0]\n if len(combined_data) == 0:\n return {}\n return combined_data\n\n def _is_message_input(self) -> bool:\n \"\"\"Check if input is Message type.\"\"\"\n if isinstance(self.data, Message):\n return True\n return isinstance(self.data, list) and len(self.data) > 0 and isinstance(self.data[0], Message)\n\n def _build_text_prompt(self, text: str) -> str:\n \"\"\"Build prompt for text/Message transformation.\"\"\"\n text_length = len(text)\n if text_length > self.max_size:\n text_preview = (\n f\"Text length: {text_length} characters\\n\\n\"\n f\"First {self.sample_size} characters:\\n{text[: self.sample_size]}\\n\\n\"\n f\"Last {self.sample_size} characters:\\n{text[-self.sample_size :]}\"\n )\n else:\n text_preview = text\n\n return TEXT_TRANSFORM_PROMPT.format(text_preview=text_preview, instruction=self.filter_instruction)\n\n def _build_data_prompt(self, data: dict | list) -> str:\n \"\"\"Build prompt for structured data transformation.\"\"\"\n dump = json.dumps(data)\n dump_structure = json.dumps(self.get_data_structure(data))\n\n if len(dump) > self.max_size:\n data_sample = (\n f\"Data is too long to display...\\n\\nFirst lines (head): {dump[: self.sample_size]}\\n\\n\"\n f\"Last lines (tail): {dump[-self.sample_size :]}\"\n )\n else:\n data_sample = dump\n\n return DATA_TRANSFORM_PROMPT.format(\n dump_structure=dump_structure, data_sample=data_sample, instruction=self.filter_instruction\n )\n\n def _parse_lambda_from_response(self, response_text: str) -> Callable[[Any], Any]:\n \"\"\"Extract and validate lambda function from LLM response.\"\"\"\n lambda_match = re.search(r\"lambda\\s+\\w+\\s*:.*?(?=\\n|$)\", response_text)\n if not lambda_match:\n msg = f\"Could not find lambda in response: {response_text}\"\n raise ValueError(msg)\n\n lambda_text = lambda_match.group().strip()\n self.log(f\"Generated lambda: {lambda_text}\")\n\n if not self._validate_lambda(lambda_text):\n msg = f\"Invalid lambda format: {lambda_text}\"\n raise ValueError(msg)\n\n return eval(lambda_text) # noqa: S307\n\n async def _execute_lambda(self) -> Any:\n \"\"\"Generate and execute a lambda function based on input type.\"\"\"\n if self._is_message_input():\n data: Any = self._extract_message_text()\n prompt = self._build_text_prompt(data)\n else:\n data = self._extract_structured_data()\n prompt = self._build_data_prompt(data)\n\n llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)\n response = await llm.ainvoke(prompt)\n response_text = response.content if hasattr(response, \"content\") else str(response)\n\n fn = self._parse_lambda_from_response(response_text)\n return fn(data)\n\n def _handle_process_error(self, error: Exception, output_type: str) -> None:\n \"\"\"Handle errors from process methods with context-aware messages.\"\"\"\n input_type = self._get_input_type_name()\n error_msg = (\n f\"Failed to convert result to {output_type} output. \"\n f\"Error: {error}. \"\n f\"Input type was {input_type}. \"\n f\"Try using the same output type as the input.\"\n )\n raise ValueError(error_msg) from error\n\n def _convert_result_to_data(self, result: Any) -> Data:\n \"\"\"Convert lambda result to Data object.\"\"\"\n if isinstance(result, dict):\n return Data(data=result)\n if isinstance(result, list):\n return Data(data={\"_results\": result})\n return Data(data={\"text\": str(result)})\n\n def _convert_result_to_dataframe(self, result: Any) -> DataFrame:\n \"\"\"Convert lambda result to DataFrame object.\"\"\"\n if isinstance(result, list):\n if all(isinstance(item, dict) for item in result):\n return DataFrame(result)\n return DataFrame([{\"value\": item} for item in result])\n if isinstance(result, dict):\n return DataFrame([result])\n return DataFrame([{\"value\": str(result)}])\n\n def _convert_result_to_message(self, result: Any) -> Message:\n \"\"\"Convert lambda result to Message object.\"\"\"\n if isinstance(result, str):\n return Message(text=result, sender=MESSAGE_SENDER_AI)\n if isinstance(result, list):\n text = \"\\n\".join(str(item) for item in result)\n return Message(text=text, sender=MESSAGE_SENDER_AI)\n if isinstance(result, dict):\n text = json.dumps(result, indent=2)\n return Message(text=text, sender=MESSAGE_SENDER_AI)\n return Message(text=str(result), sender=MESSAGE_SENDER_AI)\n\n async def process_as_data(self) -> Data:\n \"\"\"Process the data and return as a Data object.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_data(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"Data\")\n\n async def process_as_dataframe(self) -> DataFrame:\n \"\"\"Process the data and return as a DataFrame.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_dataframe(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"DataFrame\")\n\n async def process_as_message(self) -> Message:\n \"\"\"Process the data and return as a Message.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_message(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"Message\")\n" }, "data": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, - "display_name": "Data", + "display_name": "JSON", "dynamic": false, "info": "The structured data or text messages to filter or transform using a lambda function.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": true, @@ -88481,6 +88555,10 @@ "display_name": "Routes", "dynamic": false, "info": "Define the categories for routing. Each row should have a route/category name and optionally a custom output value.", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "routes", @@ -88540,8 +88618,8 @@ }, "StructuredOutput": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -88592,10 +88670,10 @@ "group_outputs": false, "method": "build_structured_output", "name": "structured_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -88606,10 +88684,10 @@ "group_outputs": false, "method": "build_structured_dataframe", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -88727,6 +88805,10 @@ "display_name": "Output Schema", "dynamic": false, "info": "Define the structure and data types for the model's output.", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "output_schema", @@ -89649,7 +89731,7 @@ { "mem0_chat_memory": { "base_classes": [ - "Data", + "JSON", "Memory" ], "beta": false, @@ -89673,7 +89755,7 @@ "icon": "Mem0", "legacy": false, "metadata": { - "code_hash": "b6addbcccf9a", + "code_hash": "309abc9375f4", "dependencies": { "dependencies": [ { @@ -89713,10 +89795,10 @@ "group_outputs": false, "method": "build_search_results", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -89740,7 +89822,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import os\n\nfrom mem0 import Memory, MemoryClient\n\nfrom lfx.base.memory.model import LCChatMemoryComponent\nfrom lfx.inputs.inputs import DictInput, HandleInput, MessageTextInput, NestedDictInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.utils.validate_cloud import raise_error_if_astra_cloud_disable_component\n\ndisable_component_in_astra_cloud_msg = (\n \"Mem0 chat memory is not supported in Astra cloud environment. Please use local storage mode or mem0 cloud.\"\n)\n\n\nclass Mem0MemoryComponent(LCChatMemoryComponent):\n display_name = \"Mem0 Chat Memory\"\n description = \"Retrieves and stores chat messages using Mem0 memory storage.\"\n name = \"mem0_chat_memory\"\n icon: str = \"Mem0\"\n inputs = [\n NestedDictInput(\n name=\"mem0_config\",\n display_name=\"Mem0 Configuration\",\n info=\"\"\"Configuration dictionary for initializing Mem0 memory instance.\n Example:\n {\n \"graph_store\": {\n \"provider\": \"neo4j\",\n \"config\": {\n \"url\": \"neo4j+s://your-neo4j-url\",\n \"username\": \"neo4j\",\n \"password\": \"your-password\"\n }\n },\n \"version\": \"v1.1\"\n }\"\"\",\n input_types=[\"Data\"],\n ),\n MessageTextInput(\n name=\"ingest_message\",\n display_name=\"Message to Ingest\",\n info=\"The message content to be ingested into Mem0 memory.\",\n ),\n HandleInput(\n name=\"existing_memory\",\n display_name=\"Existing Memory Instance\",\n input_types=[\"Memory\"],\n info=\"Optional existing Mem0 memory instance. If not provided, a new instance will be created.\",\n ),\n MessageTextInput(\n name=\"user_id\", display_name=\"User ID\", info=\"Identifier for the user associated with the messages.\"\n ),\n MessageTextInput(\n name=\"search_query\", display_name=\"Search Query\", info=\"Input text for searching related memories in Mem0.\"\n ),\n SecretStrInput(\n name=\"mem0_api_key\",\n display_name=\"Mem0 API Key\",\n info=\"API key for Mem0 platform. Leave empty to use the local version.\",\n ),\n DictInput(\n name=\"metadata\",\n display_name=\"Metadata\",\n info=\"Additional metadata to associate with the ingested message.\",\n advanced=True,\n ),\n SecretStrInput(\n name=\"openai_api_key\",\n display_name=\"OpenAI API Key\",\n required=False,\n info=\"API key for OpenAI. Required if using OpenAI Embeddings without a provided configuration.\",\n ),\n ]\n\n outputs = [\n Output(name=\"memory\", display_name=\"Mem0 Memory\", method=\"ingest_data\"),\n Output(\n name=\"search_results\",\n display_name=\"Search Results\",\n method=\"build_search_results\",\n ),\n ]\n\n def build_mem0(self) -> Memory:\n \"\"\"Initializes a Mem0 memory instance based on provided configuration and API keys.\"\"\"\n # Check if we're in Astra cloud environment and raise an error if we are.\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n if self.openai_api_key:\n os.environ[\"OPENAI_API_KEY\"] = self.openai_api_key\n\n try:\n if not self.mem0_api_key:\n return Memory.from_config(config_dict=dict(self.mem0_config)) if self.mem0_config else Memory()\n if self.mem0_config:\n return MemoryClient.from_config(api_key=self.mem0_api_key, config_dict=dict(self.mem0_config))\n return MemoryClient(api_key=self.mem0_api_key)\n except ImportError as e:\n msg = \"Mem0 is not properly installed. Please install it with 'pip install -U mem0ai'.\"\n raise ImportError(msg) from e\n\n def ingest_data(self) -> Memory:\n \"\"\"Ingests a new message into Mem0 memory and returns the updated memory instance.\"\"\"\n # Check if we're in Astra cloud environment and raise an error if we are.\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n mem0_memory = self.existing_memory or self.build_mem0()\n\n if not self.ingest_message or not self.user_id:\n logger.warning(\"Missing 'ingest_message' or 'user_id'; cannot ingest data.\")\n return mem0_memory\n\n metadata = self.metadata or {}\n\n logger.info(\"Ingesting message for user_id: %s\", self.user_id)\n\n try:\n mem0_memory.add(self.ingest_message, user_id=self.user_id, metadata=metadata)\n except Exception:\n logger.exception(\"Failed to add message to Mem0 memory.\")\n raise\n\n return mem0_memory\n\n def build_search_results(self) -> Data:\n \"\"\"Searches the Mem0 memory for related messages based on the search query and returns the results.\"\"\"\n # Check if we're in Astra cloud environment and raise an error if we are.\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n mem0_memory = self.ingest_data()\n search_query = self.search_query\n user_id = self.user_id\n\n logger.info(\"Search query: %s\", search_query)\n\n try:\n if search_query:\n logger.info(\"Performing search with query.\")\n related_memories = mem0_memory.search(query=search_query, user_id=user_id)\n else:\n logger.info(\"Retrieving all memories for user_id: %s\", user_id)\n related_memories = mem0_memory.get_all(user_id=user_id)\n except Exception:\n logger.exception(\"Failed to retrieve related memories from Mem0.\")\n raise\n\n logger.info(\"Related memories retrieved: %s\", related_memories)\n return related_memories\n" + "value": "import os\n\nfrom mem0 import Memory, MemoryClient\n\nfrom lfx.base.memory.model import LCChatMemoryComponent\nfrom lfx.inputs.inputs import DictInput, HandleInput, MessageTextInput, NestedDictInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.utils.validate_cloud import raise_error_if_astra_cloud_disable_component\n\ndisable_component_in_astra_cloud_msg = (\n \"Mem0 chat memory is not supported in Astra cloud environment. Please use local storage mode or mem0 cloud.\"\n)\n\n\nclass Mem0MemoryComponent(LCChatMemoryComponent):\n display_name = \"Mem0 Chat Memory\"\n description = \"Retrieves and stores chat messages using Mem0 memory storage.\"\n name = \"mem0_chat_memory\"\n icon: str = \"Mem0\"\n inputs = [\n NestedDictInput(\n name=\"mem0_config\",\n display_name=\"Mem0 Configuration\",\n info=\"\"\"Configuration dictionary for initializing Mem0 memory instance.\n Example:\n {\n \"graph_store\": {\n \"provider\": \"neo4j\",\n \"config\": {\n \"url\": \"neo4j+s://your-neo4j-url\",\n \"username\": \"neo4j\",\n \"password\": \"your-password\"\n }\n },\n \"version\": \"v1.1\"\n }\"\"\",\n input_types=[\"Data\", \"JSON\"],\n ),\n MessageTextInput(\n name=\"ingest_message\",\n display_name=\"Message to Ingest\",\n info=\"The message content to be ingested into Mem0 memory.\",\n ),\n HandleInput(\n name=\"existing_memory\",\n display_name=\"Existing Memory Instance\",\n input_types=[\"Memory\"],\n info=\"Optional existing Mem0 memory instance. If not provided, a new instance will be created.\",\n ),\n MessageTextInput(\n name=\"user_id\", display_name=\"User ID\", info=\"Identifier for the user associated with the messages.\"\n ),\n MessageTextInput(\n name=\"search_query\", display_name=\"Search Query\", info=\"Input text for searching related memories in Mem0.\"\n ),\n SecretStrInput(\n name=\"mem0_api_key\",\n display_name=\"Mem0 API Key\",\n info=\"API key for Mem0 platform. Leave empty to use the local version.\",\n ),\n DictInput(\n name=\"metadata\",\n display_name=\"Metadata\",\n info=\"Additional metadata to associate with the ingested message.\",\n advanced=True,\n ),\n SecretStrInput(\n name=\"openai_api_key\",\n display_name=\"OpenAI API Key\",\n required=False,\n info=\"API key for OpenAI. Required if using OpenAI Embeddings without a provided configuration.\",\n ),\n ]\n\n outputs = [\n Output(name=\"memory\", display_name=\"Mem0 Memory\", method=\"ingest_data\"),\n Output(\n name=\"search_results\",\n display_name=\"Search Results\",\n method=\"build_search_results\",\n ),\n ]\n\n def build_mem0(self) -> Memory:\n \"\"\"Initializes a Mem0 memory instance based on provided configuration and API keys.\"\"\"\n # Check if we're in Astra cloud environment and raise an error if we are.\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n if self.openai_api_key:\n os.environ[\"OPENAI_API_KEY\"] = self.openai_api_key\n\n try:\n if not self.mem0_api_key:\n return Memory.from_config(config_dict=dict(self.mem0_config)) if self.mem0_config else Memory()\n if self.mem0_config:\n return MemoryClient.from_config(api_key=self.mem0_api_key, config_dict=dict(self.mem0_config))\n return MemoryClient(api_key=self.mem0_api_key)\n except ImportError as e:\n msg = \"Mem0 is not properly installed. Please install it with 'pip install -U mem0ai'.\"\n raise ImportError(msg) from e\n\n def ingest_data(self) -> Memory:\n \"\"\"Ingests a new message into Mem0 memory and returns the updated memory instance.\"\"\"\n # Check if we're in Astra cloud environment and raise an error if we are.\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n mem0_memory = self.existing_memory or self.build_mem0()\n\n if not self.ingest_message or not self.user_id:\n logger.warning(\"Missing 'ingest_message' or 'user_id'; cannot ingest data.\")\n return mem0_memory\n\n metadata = self.metadata or {}\n\n logger.info(\"Ingesting message for user_id: %s\", self.user_id)\n\n try:\n mem0_memory.add(self.ingest_message, user_id=self.user_id, metadata=metadata)\n except Exception:\n logger.exception(\"Failed to add message to Mem0 memory.\")\n raise\n\n return mem0_memory\n\n def build_search_results(self) -> Data:\n \"\"\"Searches the Mem0 memory for related messages based on the search query and returns the results.\"\"\"\n # Check if we're in Astra cloud environment and raise an error if we are.\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n mem0_memory = self.ingest_data()\n search_query = self.search_query\n user_id = self.user_id\n\n logger.info(\"Search query: %s\", search_query)\n\n try:\n if search_query:\n logger.info(\"Performing search with query.\")\n related_memories = mem0_memory.search(query=search_query, user_id=user_id)\n else:\n logger.info(\"Retrieving all memories for user_id: %s\", user_id)\n related_memories = mem0_memory.get_all(user_id=user_id)\n except Exception:\n logger.exception(\"Failed to retrieve related memories from Mem0.\")\n raise\n\n logger.info(\"Related memories retrieved: %s\", related_memories)\n return related_memories\n" }, "existing_memory": { "_input_type": "HandleInput", @@ -89815,7 +89897,8 @@ "dynamic": false, "info": "Configuration dictionary for initializing Mem0 memory instance.\n Example:\n {\n \"graph_store\": {\n \"provider\": \"neo4j\",\n \"config\": {\n \"url\": \"neo4j+s://your-neo4j-url\",\n \"username\": \"neo4j\",\n \"password\": \"your-password\"\n }\n },\n \"version\": \"v1.1\"\n }", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -89931,8 +90014,8 @@ { "Milvus": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -89991,24 +90074,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -91473,6 +91556,10 @@ "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", @@ -92379,7 +92466,7 @@ }, "MCPTools": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -92430,10 +92517,10 @@ "group_outputs": false, "method": "build_output", "name": "response", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -92592,8 +92679,8 @@ }, "Memory": { "base_classes": [ - "DataFrame", - "Message" + "Message", + "Table" ], "beta": false, "conditional_paths": [], @@ -92619,7 +92706,7 @@ "icon": "message-square-more", "legacy": false, "metadata": { - "code_hash": "efd064ef48ff", + "code_hash": "460243b16a3a", "dependencies": { "dependencies": [ { @@ -92651,14 +92738,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Dataframe", + "display_name": "Table", "group_outputs": false, "method": "retrieve_messages_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -92682,7 +92769,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any, cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text\nfrom lfx.inputs.inputs import DropdownInput, HandleInput, IntInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.memory import aget_messages, astore_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\nfrom lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass MemoryComponent(Component):\n display_name = \"Message History\"\n description = \"Stores or retrieves stored chat messages from Langflow tables or an external memory.\"\n documentation: str = \"https://docs.langflow.org/message-history\"\n icon = \"message-square-more\"\n name = \"Memory\"\n default_keys = [\"mode\", \"memory\", \"session_id\", \"context_id\"]\n mode_config = {\n \"Store\": [\"message\", \"memory\", \"sender\", \"sender_name\", \"session_id\", \"context_id\"],\n \"Retrieve\": [\"n_messages\", \"order\", \"template\", \"memory\", \"session_id\", \"context_id\"],\n }\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Retrieve\", \"Store\"],\n value=\"Retrieve\",\n info=\"Operation mode: Store messages or Retrieve messages.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The chat message to be stored.\",\n tool_mode=True,\n dynamic=True,\n show=False,\n ),\n HandleInput(\n name=\"memory\",\n display_name=\"External Memory\",\n input_types=[\"Memory\"],\n info=\"Retrieve messages from an external memory. If empty, it will use the Langflow tables.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"sender_type\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER, \"Machine and User\"],\n value=\"Machine and User\",\n info=\"Filter by sender type.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender\",\n display_name=\"Sender\",\n info=\"The sender of the message. Might be Machine or User. \"\n \"If empty, the current sender parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Filter by sender name.\",\n advanced=True,\n show=False,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Messages\",\n value=100,\n info=\"Number of messages to retrieve.\",\n advanced=True,\n show=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 value=\"\",\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 DropdownInput(\n name=\"order\",\n display_name=\"Order\",\n options=[\"Ascending\", \"Descending\"],\n value=\"Ascending\",\n info=\"Order of the messages.\",\n advanced=True,\n tool_mode=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {sender} or any other key in the message data.\",\n value=\"{sender_name}: {text}\",\n advanced=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Message\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True),\n Output(display_name=\"Dataframe\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"mode\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n if field_value == \"Store\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Stored Messages\",\n name=\"stored_messages\",\n method=\"store_message\",\n hidden=True,\n dynamic=True,\n )\n ]\n if field_value == \"Retrieve\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Messages\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True\n ),\n Output(\n display_name=\"Dataframe\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True\n ),\n ]\n return frontend_node\n\n async def store_message(self) -> Message:\n message = Message(text=self.message) if isinstance(self.message, str) else self.message\n\n message.context_id = self.context_id or message.context_id\n message.session_id = self.session_id or message.session_id\n message.sender = self.sender or message.sender or MESSAGE_SENDER_AI\n message.sender_name = self.sender_name or message.sender_name or MESSAGE_SENDER_NAME_AI\n\n stored_messages: list[Message] = []\n\n if self.memory:\n self.memory.context_id = message.context_id\n self.memory.session_id = message.session_id\n lc_message = message.to_lc_message()\n await self.memory.aadd_messages([lc_message])\n\n stored_messages = await self.memory.aget_messages() or []\n\n stored_messages = [Message.from_lc_message(m) for m in stored_messages] if stored_messages else []\n\n if message.sender:\n stored_messages = [m for m in stored_messages if m.sender == message.sender]\n else:\n await astore_message(message, flow_id=self.graph.flow_id)\n stored_messages = (\n await aget_messages(\n session_id=message.session_id,\n context_id=message.context_id,\n sender_name=message.sender_name,\n sender=message.sender,\n )\n or []\n )\n\n if not stored_messages:\n msg = \"No messages were stored. Please ensure that the session ID and sender are properly set.\"\n raise ValueError(msg)\n\n stored_message = stored_messages[0]\n self.status = stored_message\n return stored_message\n\n async def retrieve_messages(self) -> Data:\n sender_type = self.sender_type\n sender_name = self.sender_name\n session_id = self.session_id\n context_id = self.context_id\n n_messages = self.n_messages\n order = \"DESC\" if self.order == \"Descending\" else \"ASC\"\n\n if sender_type == \"Machine and User\":\n sender_type = None\n\n if self.memory and not hasattr(self.memory, \"aget_messages\"):\n memory_name = type(self.memory).__name__\n err_msg = f\"External Memory object ({memory_name}) must have 'aget_messages' method.\"\n raise AttributeError(err_msg)\n # Check if n_messages is None or 0\n if n_messages == 0:\n stored = []\n elif self.memory:\n # override session_id\n self.memory.session_id = session_id\n self.memory.context_id = context_id\n\n stored = await self.memory.aget_messages()\n # langchain memories are supposed to return messages in ascending order\n\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages first\n\n if order == \"DESC\":\n stored = stored[::-1] # Then reverse if needed\n\n stored = [Message.from_lc_message(m) for m in stored]\n if sender_type:\n expected_type = MESSAGE_SENDER_AI if sender_type == MESSAGE_SENDER_AI else MESSAGE_SENDER_USER\n stored = [m for m in stored if m.type == expected_type]\n else:\n # For internal memory, we always fetch the last N messages by ordering by DESC\n stored = await aget_messages(\n sender=sender_type,\n sender_name=sender_name,\n session_id=session_id,\n context_id=context_id,\n limit=10000,\n order=order,\n )\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages\n\n # self.status = stored\n return cast(\"Data\", stored)\n\n async def retrieve_messages_as_text(self) -> Message:\n stored_text = data_to_text(self.template, await self.retrieve_messages())\n # self.status = stored_text\n return Message(text=stored_text)\n\n async def retrieve_messages_dataframe(self) -> DataFrame:\n \"\"\"Convert the retrieved messages into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the message data.\n \"\"\"\n messages = await self.retrieve_messages()\n return DataFrame(messages)\n\n def update_build_config(\n self,\n build_config: dotdict,\n field_value: Any, # noqa: ARG002\n field_name: str | None = None, # noqa: ARG002\n ) -> dotdict:\n return set_current_fields(\n build_config=build_config,\n action_fields=self.mode_config,\n selected_action=build_config[\"mode\"][\"value\"],\n default_fields=self.default_keys,\n func=set_field_display,\n )\n" + "value": "from typing import Any, cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text\nfrom lfx.inputs.inputs import DropdownInput, HandleInput, IntInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.memory import aget_messages, astore_message\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\nfrom lfx.utils.constants import MESSAGE_SENDER_AI, MESSAGE_SENDER_NAME_AI, MESSAGE_SENDER_USER\n\n\nclass MemoryComponent(Component):\n display_name = \"Message History\"\n description = \"Stores or retrieves stored chat messages from Langflow tables or an external memory.\"\n documentation: str = \"https://docs.langflow.org/message-history\"\n icon = \"message-square-more\"\n name = \"Memory\"\n default_keys = [\"mode\", \"memory\", \"session_id\", \"context_id\"]\n mode_config = {\n \"Store\": [\"message\", \"memory\", \"sender\", \"sender_name\", \"session_id\", \"context_id\"],\n \"Retrieve\": [\"n_messages\", \"order\", \"template\", \"memory\", \"session_id\", \"context_id\"],\n }\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Retrieve\", \"Store\"],\n value=\"Retrieve\",\n info=\"Operation mode: Store messages or Retrieve messages.\",\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The chat message to be stored.\",\n tool_mode=True,\n dynamic=True,\n show=False,\n ),\n HandleInput(\n name=\"memory\",\n display_name=\"External Memory\",\n input_types=[\"Memory\"],\n info=\"Retrieve messages from an external memory. If empty, it will use the Langflow tables.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"sender_type\",\n display_name=\"Sender Type\",\n options=[MESSAGE_SENDER_AI, MESSAGE_SENDER_USER, \"Machine and User\"],\n value=\"Machine and User\",\n info=\"Filter by sender type.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender\",\n display_name=\"Sender\",\n info=\"The sender of the message. Might be Machine or User. \"\n \"If empty, the current sender parameter will be used.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"sender_name\",\n display_name=\"Sender Name\",\n info=\"Filter by sender name.\",\n advanced=True,\n show=False,\n ),\n IntInput(\n name=\"n_messages\",\n display_name=\"Number of Messages\",\n value=100,\n info=\"Number of messages to retrieve.\",\n advanced=True,\n show=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 value=\"\",\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 DropdownInput(\n name=\"order\",\n display_name=\"Order\",\n options=[\"Ascending\", \"Descending\"],\n value=\"Ascending\",\n info=\"Order of the messages.\",\n advanced=True,\n tool_mode=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {sender} or any other key in the message data.\",\n value=\"{sender_name}: {text}\",\n advanced=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Message\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"mode\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n if field_value == \"Store\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Stored Messages\",\n name=\"stored_messages\",\n method=\"store_message\",\n hidden=True,\n dynamic=True,\n )\n ]\n if field_value == \"Retrieve\":\n frontend_node[\"outputs\"] = [\n Output(\n display_name=\"Messages\", name=\"messages_text\", method=\"retrieve_messages_as_text\", dynamic=True\n ),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"retrieve_messages_dataframe\", dynamic=True),\n ]\n return frontend_node\n\n async def store_message(self) -> Message:\n message = Message(text=self.message) if isinstance(self.message, str) else self.message\n\n message.context_id = self.context_id or message.context_id\n message.session_id = self.session_id or message.session_id\n message.sender = self.sender or message.sender or MESSAGE_SENDER_AI\n message.sender_name = self.sender_name or message.sender_name or MESSAGE_SENDER_NAME_AI\n\n stored_messages: list[Message] = []\n\n if self.memory:\n self.memory.context_id = message.context_id\n self.memory.session_id = message.session_id\n lc_message = message.to_lc_message()\n await self.memory.aadd_messages([lc_message])\n\n stored_messages = await self.memory.aget_messages() or []\n\n stored_messages = [Message.from_lc_message(m) for m in stored_messages] if stored_messages else []\n\n if message.sender:\n stored_messages = [m for m in stored_messages if m.sender == message.sender]\n else:\n await astore_message(message, flow_id=self.graph.flow_id)\n stored_messages = (\n await aget_messages(\n session_id=message.session_id,\n context_id=message.context_id,\n sender_name=message.sender_name,\n sender=message.sender,\n )\n or []\n )\n\n if not stored_messages:\n msg = \"No messages were stored. Please ensure that the session ID and sender are properly set.\"\n raise ValueError(msg)\n\n stored_message = stored_messages[0]\n self.status = stored_message\n return stored_message\n\n async def retrieve_messages(self) -> Data:\n sender_type = self.sender_type\n sender_name = self.sender_name\n session_id = self.session_id\n context_id = self.context_id\n n_messages = self.n_messages\n order = \"DESC\" if self.order == \"Descending\" else \"ASC\"\n\n if sender_type == \"Machine and User\":\n sender_type = None\n\n if self.memory and not hasattr(self.memory, \"aget_messages\"):\n memory_name = type(self.memory).__name__\n err_msg = f\"External Memory object ({memory_name}) must have 'aget_messages' method.\"\n raise AttributeError(err_msg)\n # Check if n_messages is None or 0\n if n_messages == 0:\n stored = []\n elif self.memory:\n # override session_id\n self.memory.session_id = session_id\n self.memory.context_id = context_id\n\n stored = await self.memory.aget_messages()\n # langchain memories are supposed to return messages in ascending order\n\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages first\n\n if order == \"DESC\":\n stored = stored[::-1] # Then reverse if needed\n\n stored = [Message.from_lc_message(m) for m in stored]\n if sender_type:\n expected_type = MESSAGE_SENDER_AI if sender_type == MESSAGE_SENDER_AI else MESSAGE_SENDER_USER\n stored = [m for m in stored if m.type == expected_type]\n else:\n # For internal memory, we always fetch the last N messages by ordering by DESC\n stored = await aget_messages(\n sender=sender_type,\n sender_name=sender_name,\n session_id=session_id,\n context_id=context_id,\n limit=10000,\n order=order,\n )\n if n_messages:\n stored = stored[-n_messages:] # Get last N messages\n\n # self.status = stored\n return cast(\"Data\", stored)\n\n async def retrieve_messages_as_text(self) -> Message:\n stored_text = data_to_text(self.template, await self.retrieve_messages())\n # self.status = stored_text\n return Message(text=stored_text)\n\n async def retrieve_messages_dataframe(self) -> DataFrame:\n \"\"\"Convert the retrieved messages into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the message data.\n \"\"\"\n messages = await self.retrieve_messages()\n return DataFrame(messages)\n\n def update_build_config(\n self,\n build_config: dotdict,\n field_value: Any, # noqa: ARG002\n field_name: str | None = None, # noqa: ARG002\n ) -> dotdict:\n return set_current_fields(\n build_config=build_config,\n action_fields=self.mode_config,\n selected_action=build_config[\"mode\"][\"value\"],\n default_fields=self.default_keys,\n func=set_field_display,\n )\n" }, "context_id": { "_input_type": "MessageTextInput", @@ -93108,8 +93195,8 @@ { "MongoDBAtlasVector": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -93179,24 +93266,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -95085,7 +95172,7 @@ }, "NvidiaIngestComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": true, "conditional_paths": [], @@ -95155,10 +95242,10 @@ "group_outputs": false, "method": "load_files", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -95436,6 +95523,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, @@ -95717,7 +95805,7 @@ }, "NvidiaRerankComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -95764,10 +95852,10 @@ "group_outputs": false, "method": "compress_documents", "name": "reranked_documents", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -95890,13 +95978,14 @@ "value": "" }, "search_results": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Search Results", "dynamic": false, "info": "Search Results from a Vector Store.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -96044,7 +96133,7 @@ { "OlivyaComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -96091,10 +96180,10 @@ "group_outputs": false, "method": "build_output", "name": "output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -96435,10 +96524,10 @@ }, "OllamaModel": { "base_classes": [ - "Data", - "DataFrame", + "JSON", "LanguageModel", - "Message" + "Message", + "Table" ], "beta": false, "conditional_paths": [], @@ -96481,7 +96570,7 @@ "icon": "Ollama", "legacy": false, "metadata": { - "code_hash": "cd3dc38272a7", + "code_hash": "2aa7e6ecf48c", "dependencies": { "dependencies": [ { @@ -96541,28 +96630,28 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "build_data_output", "name": "data_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "build_dataframe_output", "name": "dataframe_output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -96627,7 +96716,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import asyncio\nimport json\nfrom contextlib import suppress\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nfrom langchain_ollama import ChatOllama\n\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.base_model import build_model_from_schema\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n Output,\n SecretStrInput,\n SliderInput,\n StrInput,\n TableInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.table import EditMode\nfrom lfx.utils.util import transform_localhost_url\n\nHTTP_STATUS_OK = 200\nTABLE_ROW_PLACEHOLDER = {\"name\": \"field\", \"description\": \"description of field\", \"type\": \"str\", \"multiple\": \"False\"}\n\n\nclass ChatOllamaComponent(LCModelComponent):\n display_name = \"Ollama\"\n description = \"Generate text using Ollama Local LLMs.\"\n icon = \"Ollama\"\n name = \"OllamaModel\"\n\n # Define constants for JSON keys\n JSON_MODELS_KEY = \"models\"\n JSON_NAME_KEY = \"name\"\n JSON_CAPABILITIES_KEY = \"capabilities\"\n DESIRED_CAPABILITY = \"completion\"\n TOOL_CALLING_CAPABILITY = \"tools\"\n\n # Define the table schema for the format input\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 \"edit_mode\": EditMode.INLINE,\n \"options\": [\"True\", \"False\"],\n \"default\": \"False\",\n },\n ]\n default_table_row = {row[\"name\"]: row.get(\"default\", None) for row in TABLE_SCHEMA}\n default_table_row_schema = build_model_from_schema([default_table_row]).model_json_schema()\n\n inputs = [\n StrInput(\n name=\"base_url\",\n display_name=\"Ollama API URL\",\n info=\"Endpoint of the Ollama API. Defaults to http://localhost:11434.\",\n value=\"http://localhost:11434\",\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n options=[],\n info=\"Refer to https://ollama.com/library for more models.\",\n refresh_button=True,\n real_time_refresh=True,\n required=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Ollama API Key\",\n info=\"Your Ollama API key.\",\n value=None,\n required=False,\n real_time_refresh=True,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n TableInput(\n name=\"format\",\n display_name=\"Format\",\n info=\"Specify the format of the output.\",\n table_schema=TABLE_SCHEMA,\n value=default_table_row,\n show=False,\n ),\n DictInput(name=\"metadata\", display_name=\"Metadata\", info=\"Metadata to add to the run trace.\", advanced=True),\n DropdownInput(\n name=\"mirostat\",\n display_name=\"Mirostat\",\n options=[\"Disabled\", \"Mirostat\", \"Mirostat 2.0\"],\n info=\"Enable/disable Mirostat sampling for controlling perplexity.\",\n value=\"Disabled\",\n advanced=True,\n real_time_refresh=True,\n ),\n FloatInput(\n name=\"mirostat_eta\",\n display_name=\"Mirostat Eta\",\n info=\"Learning rate for Mirostat algorithm. (Default: 0.1)\",\n advanced=True,\n ),\n FloatInput(\n name=\"mirostat_tau\",\n display_name=\"Mirostat Tau\",\n info=\"Controls the balance between coherence and diversity of the output. (Default: 5.0)\",\n advanced=True,\n ),\n IntInput(\n name=\"num_ctx\",\n display_name=\"Context Window Size\",\n info=\"Size of the context window for generating tokens. (Default: 2048)\",\n advanced=True,\n ),\n IntInput(\n name=\"num_gpu\",\n display_name=\"Number of GPUs\",\n info=\"Number of GPUs to use for computation. (Default: 1 on macOS, 0 to disable)\",\n advanced=True,\n ),\n IntInput(\n name=\"num_thread\",\n display_name=\"Number of Threads\",\n info=\"Number of threads to use during computation. (Default: detected for optimal performance)\",\n advanced=True,\n ),\n IntInput(\n name=\"repeat_last_n\",\n display_name=\"Repeat Last N\",\n info=\"How far back the model looks to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx)\",\n advanced=True,\n ),\n FloatInput(\n name=\"repeat_penalty\",\n display_name=\"Repeat Penalty\",\n info=\"Penalty for repetitions in generated text. (Default: 1.1)\",\n advanced=True,\n ),\n FloatInput(name=\"tfs_z\", display_name=\"TFS Z\", info=\"Tail free sampling value. (Default: 1)\", advanced=True),\n IntInput(name=\"timeout\", display_name=\"Timeout\", info=\"Timeout for the request stream.\", advanced=True),\n IntInput(\n name=\"top_k\", display_name=\"Top K\", info=\"Limits token selection to top K. (Default: 40)\", advanced=True\n ),\n FloatInput(name=\"top_p\", display_name=\"Top P\", info=\"Works together with top-k. (Default: 0.9)\", advanced=True),\n BoolInput(\n name=\"enable_verbose_output\",\n display_name=\"Ollama Verbose Output\",\n info=\"Whether to print out response text.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"tags\",\n display_name=\"Tags\",\n info=\"Comma-separated list of tags to add to the run trace.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"stop_tokens\",\n display_name=\"Stop Tokens\",\n info=\"Comma-separated list of tokens to signal the model to stop generating text.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"system\", display_name=\"System\", info=\"System to use for generating text.\", advanced=True\n ),\n BoolInput(\n name=\"tool_model_enabled\",\n display_name=\"Tool Model Enabled\",\n info=\"Whether to enable tool calling in the model.\",\n value=True,\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"template\", display_name=\"Template\", info=\"Template to use for generating text.\", advanced=True\n ),\n BoolInput(\n name=\"enable_structured_output\",\n display_name=\"Enable Structured Output\",\n info=\"Whether to enable structured output in the model.\",\n value=False,\n advanced=False,\n real_time_refresh=True,\n ),\n *LCModelComponent.get_base_inputs(),\n ]\n\n outputs = [\n Output(display_name=\"Text\", name=\"text_output\", method=\"text_response\"),\n Output(display_name=\"Language Model\", name=\"model_output\", method=\"build_model\"),\n Output(display_name=\"Data\", name=\"data_output\", method=\"build_data_output\"),\n Output(display_name=\"DataFrame\", name=\"dataframe_output\", method=\"build_dataframe_output\"),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n # Mapping mirostat settings to their corresponding values\n mirostat_options = {\"Mirostat\": 1, \"Mirostat 2.0\": 2}\n\n # Default to None for 'Disabled'\n mirostat_value = mirostat_options.get(self.mirostat, None)\n\n # Set mirostat_eta and mirostat_tau to None if mirostat is disabled\n if mirostat_value is None:\n mirostat_eta = None\n mirostat_tau = None\n else:\n mirostat_eta = self.mirostat_eta\n mirostat_tau = self.mirostat_tau\n\n transformed_base_url = transform_localhost_url(self.base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n try:\n output_format = self._parse_format_field(self.format) if self.enable_structured_output else None\n except Exception as e:\n msg = f\"Failed to parse the format field: {e}\"\n raise ValueError(msg) from e\n\n # Mapping system settings to their corresponding values\n llm_params = {\n \"base_url\": transformed_base_url,\n \"model\": self.model_name,\n \"mirostat\": mirostat_value,\n \"format\": output_format or None,\n \"metadata\": self.metadata,\n \"tags\": self.tags.split(\",\") if self.tags else None,\n \"mirostat_eta\": mirostat_eta,\n \"mirostat_tau\": mirostat_tau,\n \"num_ctx\": self.num_ctx or None,\n \"num_gpu\": self.num_gpu or None,\n \"num_thread\": self.num_thread or None,\n \"repeat_last_n\": self.repeat_last_n or None,\n \"repeat_penalty\": self.repeat_penalty or None,\n \"temperature\": self.temperature or None,\n \"stop\": self.stop_tokens.split(\",\") if self.stop_tokens else None,\n \"system\": self.system,\n \"tfs_z\": self.tfs_z or None,\n \"timeout\": self.timeout or None,\n \"top_k\": self.top_k or None,\n \"top_p\": self.top_p or None,\n \"verbose\": self.enable_verbose_output or False,\n \"template\": self.template,\n }\n headers = self.headers\n if headers is not None:\n llm_params[\"client_kwargs\"] = {\"headers\": headers}\n\n # Remove parameters with None values\n llm_params = {k: v for k, v in llm_params.items() if v is not None}\n\n try:\n output = ChatOllama(**llm_params)\n except Exception as e:\n msg = (\n \"Unable to connect to the Ollama API. \"\n \"Please verify the base URL, ensure the relevant Ollama model is pulled, and try again.\"\n )\n raise ValueError(msg) from e\n\n return output\n\n async def is_valid_ollama_url(self, url: str) -> bool:\n try:\n async with httpx.AsyncClient() as client:\n url = transform_localhost_url(url)\n if not url:\n return False\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n url = url.rstrip(\"/\").removesuffix(\"/v1\")\n if not url.endswith(\"/\"):\n url = url + \"/\"\n return (\n await client.get(url=urljoin(url, \"api/tags\"), headers=self.headers)\n ).status_code == HTTP_STATUS_OK\n except httpx.RequestError:\n return False\n\n async def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None):\n if field_name == \"enable_structured_output\": # bind enable_structured_output boolean to format show value\n build_config[\"format\"][\"show\"] = field_value\n\n if field_name == \"mirostat\":\n if field_value == \"Disabled\":\n build_config[\"mirostat_eta\"][\"advanced\"] = True\n build_config[\"mirostat_tau\"][\"advanced\"] = True\n build_config[\"mirostat_eta\"][\"value\"] = None\n build_config[\"mirostat_tau\"][\"value\"] = None\n\n else:\n build_config[\"mirostat_eta\"][\"advanced\"] = False\n build_config[\"mirostat_tau\"][\"advanced\"] = False\n\n if field_value == \"Mirostat 2.0\":\n build_config[\"mirostat_eta\"][\"value\"] = 0.2\n build_config[\"mirostat_tau\"][\"value\"] = 10\n else:\n build_config[\"mirostat_eta\"][\"value\"] = 0.1\n build_config[\"mirostat_tau\"][\"value\"] = 5\n\n if field_name in {\"model_name\", \"base_url\", \"tool_model_enabled\"}:\n # Use field_value if base_url is being updated, otherwise use self.base_url\n base_url_to_check = field_value if field_name == \"base_url\" else self.base_url\n # Fallback to self.base_url if field_value is None or empty\n if not base_url_to_check and field_name == \"base_url\":\n base_url_to_check = self.base_url\n logger.warning(f\"Fetching Ollama models from updated URL: {base_url_to_check}\")\n\n if base_url_to_check and await self.is_valid_ollama_url(base_url_to_check):\n tool_model_enabled = build_config[\"tool_model_enabled\"].get(\"value\", False) or self.tool_model_enabled\n build_config[\"model_name\"][\"options\"] = await self.get_models(\n base_url_to_check, tool_model_enabled=tool_model_enabled\n )\n else:\n build_config[\"model_name\"][\"options\"] = []\n if field_name == \"keep_alive_flag\":\n if field_value == \"Keep\":\n build_config[\"keep_alive\"][\"value\"] = \"-1\"\n build_config[\"keep_alive\"][\"advanced\"] = True\n elif field_value == \"Immediately\":\n build_config[\"keep_alive\"][\"value\"] = \"0\"\n build_config[\"keep_alive\"][\"advanced\"] = True\n else:\n build_config[\"keep_alive\"][\"advanced\"] = False\n\n return build_config\n\n async def get_models(self, base_url_value: str, *, tool_model_enabled: bool | None = None) -> list[str]:\n \"\"\"Fetches a list of models from the Ollama API suitable for text generation.\n\n Args:\n base_url_value (str): The base URL of the Ollama API.\n tool_model_enabled (bool | None, optional): If True, filters the models further to include\n only those that support tool calling. Defaults to None.\n\n Returns:\n list[str]: A list of model names suitable for text generation. Models are included if:\n - They have the \"completion\" capability, OR\n - The capabilities field is not returned (backwards compatibility with older Ollama versions)\n If `tool_model_enabled` is True, only models with verified \"tools\" capability are included\n (models without capabilities info are excluded in this case).\n\n Raises:\n ValueError: If there is an issue with the API request or response, or if the model\n names cannot be retrieved.\n \"\"\"\n try:\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n base_url = base_url_value.rstrip(\"/\").removesuffix(\"/v1\")\n if not base_url.endswith(\"/\"):\n base_url = base_url + \"/\"\n base_url = transform_localhost_url(base_url)\n\n # Ollama REST API to return models\n tags_url = urljoin(base_url, \"api/tags\")\n\n # Ollama REST API to return model capabilities\n show_url = urljoin(base_url, \"api/show\")\n\n async with httpx.AsyncClient() as client:\n headers = self.headers\n # Fetch available models\n tags_response = await client.get(url=tags_url, headers=headers)\n tags_response.raise_for_status()\n models = tags_response.json()\n if asyncio.iscoroutine(models):\n models = await models\n await logger.adebug(f\"Available models: {models}\")\n\n # Filter models that are NOT embedding models\n model_ids = []\n for model in models[self.JSON_MODELS_KEY]:\n model_name = model[self.JSON_NAME_KEY]\n await logger.adebug(f\"Checking model: {model_name}\")\n\n payload = {\"model\": model_name}\n show_response = await client.post(url=show_url, json=payload, headers=headers)\n show_response.raise_for_status()\n json_data = show_response.json()\n if asyncio.iscoroutine(json_data):\n json_data = await json_data\n\n capabilities = json_data.get(self.JSON_CAPABILITIES_KEY)\n await logger.adebug(f\"Model: {model_name}, Capabilities: {capabilities}\")\n\n # If capabilities not provided, assume it's a completion model (backwards compatibility\n # with older Ollama versions that don't return capabilities from /api/show)\n if capabilities is None:\n if not tool_model_enabled:\n model_ids.append(model_name)\n # If tool_model_enabled is True but no capabilities info, skip the model\n # since we can't verify tool support\n elif self.DESIRED_CAPABILITY in capabilities and (\n not tool_model_enabled or self.TOOL_CALLING_CAPABILITY in capabilities\n ):\n model_ids.append(model_name)\n\n except (httpx.RequestError, ValueError) as e:\n msg = \"Could not get model names from Ollama.\"\n raise ValueError(msg) from e\n\n return model_ids\n\n def _parse_format_field(self, format_value: Any) -> Any:\n \"\"\"Parse the format field to handle both string and dict inputs.\n\n The format field can be:\n - A simple string like \"json\" (backward compatibility)\n - A JSON string from NestedDictInput that needs parsing\n - A dict/JSON schema (already parsed)\n - None or empty\n\n Args:\n format_value: The raw format value from the input field\n\n Returns:\n Parsed format value as string, dict, or None\n \"\"\"\n if not format_value:\n return None\n\n schema = format_value\n if isinstance(format_value, list):\n schema = build_model_from_schema(format_value).model_json_schema()\n if schema == self.default_table_row_schema:\n return None # the rows are generic placeholder rows\n elif isinstance(format_value, str): # parse as json if string\n with suppress(json.JSONDecodeError): # e.g., literal \"json\" is valid for format field\n schema = json.loads(format_value)\n\n return schema or None\n\n async def _parse_json_response(self) -> Any:\n \"\"\"Parse the JSON response from the model.\n\n This method gets the text response and attempts to parse it as JSON.\n Works with models that have format='json' or a JSON schema set.\n\n Returns:\n Parsed JSON (dict, list, or primitive type)\n\n Raises:\n ValueError: If the response is not valid JSON\n \"\"\"\n message = await self.text_response()\n text = message.text if hasattr(message, \"text\") else str(message)\n\n if not text:\n msg = \"No response from model\"\n raise ValueError(msg)\n\n try:\n return json.loads(text)\n except json.JSONDecodeError as e:\n msg = f\"Invalid JSON response. Ensure model supports JSON output. Error: {e}\"\n raise ValueError(msg) from e\n\n async def build_data_output(self) -> Data:\n \"\"\"Build a Data output from the model's JSON response.\n\n Returns:\n Data: A Data object containing the parsed JSON response\n \"\"\"\n parsed = await self._parse_json_response()\n\n # If the response is already a dict, wrap it in Data\n if isinstance(parsed, dict):\n return Data(data=parsed)\n\n # If it's a list, wrap in a results container\n if isinstance(parsed, list):\n if len(parsed) == 1:\n return Data(data=parsed[0])\n return Data(data={\"results\": parsed})\n\n # For primitive types, wrap in a value container\n return Data(data={\"value\": parsed})\n\n async def build_dataframe_output(self) -> DataFrame:\n \"\"\"Build a DataFrame output from the model's JSON response.\n\n Returns:\n DataFrame: A DataFrame containing the parsed JSON response\n\n Raises:\n ValueError: If the response cannot be converted to a DataFrame\n \"\"\"\n parsed = await self._parse_json_response()\n\n # If it's a list of dicts, convert directly to DataFrame\n if isinstance(parsed, list):\n if not parsed:\n return DataFrame()\n # Ensure all items are dicts for proper DataFrame conversion\n if all(isinstance(item, dict) for item in parsed):\n return DataFrame(parsed)\n msg = \"List items must be dictionaries to convert to DataFrame\"\n raise ValueError(msg)\n\n # If it's a single dict, wrap in a list to create a single-row DataFrame\n if isinstance(parsed, dict):\n return DataFrame([parsed])\n\n # For primitive types, create a single-column DataFrame\n return DataFrame([{\"value\": parsed}])\n\n @property\n def headers(self) -> dict[str, str] | None:\n \"\"\"Get the headers for the Ollama API.\"\"\"\n if self.api_key and self.api_key.strip():\n return {\"Authorization\": f\"Bearer {self.api_key}\"}\n return None\n" + "value": "import asyncio\nimport json\nfrom contextlib import suppress\nfrom typing import Any\nfrom urllib.parse import urljoin\n\nimport httpx\nfrom langchain_ollama import ChatOllama\n\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.helpers.base_model import build_model_from_schema\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n Output,\n SecretStrInput,\n SliderInput,\n StrInput,\n TableInput,\n)\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.table import EditMode\nfrom lfx.utils.util import transform_localhost_url\n\nHTTP_STATUS_OK = 200\nTABLE_ROW_PLACEHOLDER = {\"name\": \"field\", \"description\": \"description of field\", \"type\": \"str\", \"multiple\": \"False\"}\n\n\nclass ChatOllamaComponent(LCModelComponent):\n display_name = \"Ollama\"\n description = \"Generate text using Ollama Local LLMs.\"\n icon = \"Ollama\"\n name = \"OllamaModel\"\n\n # Define constants for JSON keys\n JSON_MODELS_KEY = \"models\"\n JSON_NAME_KEY = \"name\"\n JSON_CAPABILITIES_KEY = \"capabilities\"\n DESIRED_CAPABILITY = \"completion\"\n TOOL_CALLING_CAPABILITY = \"tools\"\n\n # Define the table schema for the format input\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 \"edit_mode\": EditMode.INLINE,\n \"options\": [\"True\", \"False\"],\n \"default\": \"False\",\n },\n ]\n default_table_row = {row[\"name\"]: row.get(\"default\", None) for row in TABLE_SCHEMA}\n default_table_row_schema = build_model_from_schema([default_table_row]).model_json_schema()\n\n inputs = [\n StrInput(\n name=\"base_url\",\n display_name=\"Ollama API URL\",\n info=\"Endpoint of the Ollama API. Defaults to http://localhost:11434.\",\n value=\"http://localhost:11434\",\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model Name\",\n options=[],\n info=\"Refer to https://ollama.com/library for more models.\",\n refresh_button=True,\n real_time_refresh=True,\n required=True,\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Ollama API Key\",\n info=\"Your Ollama API key.\",\n value=None,\n required=False,\n real_time_refresh=True,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n TableInput(\n name=\"format\",\n display_name=\"Format\",\n info=\"Specify the format of the output.\",\n table_schema=TABLE_SCHEMA,\n value=default_table_row,\n show=False,\n ),\n DictInput(name=\"metadata\", display_name=\"Metadata\", info=\"Metadata to add to the run trace.\", advanced=True),\n DropdownInput(\n name=\"mirostat\",\n display_name=\"Mirostat\",\n options=[\"Disabled\", \"Mirostat\", \"Mirostat 2.0\"],\n info=\"Enable/disable Mirostat sampling for controlling perplexity.\",\n value=\"Disabled\",\n advanced=True,\n real_time_refresh=True,\n ),\n FloatInput(\n name=\"mirostat_eta\",\n display_name=\"Mirostat Eta\",\n info=\"Learning rate for Mirostat algorithm. (Default: 0.1)\",\n advanced=True,\n ),\n FloatInput(\n name=\"mirostat_tau\",\n display_name=\"Mirostat Tau\",\n info=\"Controls the balance between coherence and diversity of the output. (Default: 5.0)\",\n advanced=True,\n ),\n IntInput(\n name=\"num_ctx\",\n display_name=\"Context Window Size\",\n info=\"Size of the context window for generating tokens. (Default: 2048)\",\n advanced=True,\n ),\n IntInput(\n name=\"num_gpu\",\n display_name=\"Number of GPUs\",\n info=\"Number of GPUs to use for computation. (Default: 1 on macOS, 0 to disable)\",\n advanced=True,\n ),\n IntInput(\n name=\"num_thread\",\n display_name=\"Number of Threads\",\n info=\"Number of threads to use during computation. (Default: detected for optimal performance)\",\n advanced=True,\n ),\n IntInput(\n name=\"repeat_last_n\",\n display_name=\"Repeat Last N\",\n info=\"How far back the model looks to prevent repetition. (Default: 64, 0 = disabled, -1 = num_ctx)\",\n advanced=True,\n ),\n FloatInput(\n name=\"repeat_penalty\",\n display_name=\"Repeat Penalty\",\n info=\"Penalty for repetitions in generated text. (Default: 1.1)\",\n advanced=True,\n ),\n FloatInput(name=\"tfs_z\", display_name=\"TFS Z\", info=\"Tail free sampling value. (Default: 1)\", advanced=True),\n IntInput(name=\"timeout\", display_name=\"Timeout\", info=\"Timeout for the request stream.\", advanced=True),\n IntInput(\n name=\"top_k\", display_name=\"Top K\", info=\"Limits token selection to top K. (Default: 40)\", advanced=True\n ),\n FloatInput(name=\"top_p\", display_name=\"Top P\", info=\"Works together with top-k. (Default: 0.9)\", advanced=True),\n BoolInput(\n name=\"enable_verbose_output\",\n display_name=\"Ollama Verbose Output\",\n info=\"Whether to print out response text.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"tags\",\n display_name=\"Tags\",\n info=\"Comma-separated list of tags to add to the run trace.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"stop_tokens\",\n display_name=\"Stop Tokens\",\n info=\"Comma-separated list of tokens to signal the model to stop generating text.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"system\", display_name=\"System\", info=\"System to use for generating text.\", advanced=True\n ),\n BoolInput(\n name=\"tool_model_enabled\",\n display_name=\"Tool Model Enabled\",\n info=\"Whether to enable tool calling in the model.\",\n value=True,\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"template\", display_name=\"Template\", info=\"Template to use for generating text.\", advanced=True\n ),\n BoolInput(\n name=\"enable_structured_output\",\n display_name=\"Enable Structured Output\",\n info=\"Whether to enable structured output in the model.\",\n value=False,\n advanced=False,\n real_time_refresh=True,\n ),\n *LCModelComponent.get_base_inputs(),\n ]\n\n outputs = [\n Output(display_name=\"Text\", name=\"text_output\", method=\"text_response\"),\n Output(display_name=\"Language Model\", name=\"model_output\", method=\"build_model\"),\n Output(display_name=\"JSON\", name=\"data_output\", method=\"build_data_output\"),\n Output(display_name=\"Table\", name=\"dataframe_output\", method=\"build_dataframe_output\"),\n ]\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n # Mapping mirostat settings to their corresponding values\n mirostat_options = {\"Mirostat\": 1, \"Mirostat 2.0\": 2}\n\n # Default to None for 'Disabled'\n mirostat_value = mirostat_options.get(self.mirostat, None)\n\n # Set mirostat_eta and mirostat_tau to None if mirostat is disabled\n if mirostat_value is None:\n mirostat_eta = None\n mirostat_tau = None\n else:\n mirostat_eta = self.mirostat_eta\n mirostat_tau = self.mirostat_tau\n\n transformed_base_url = transform_localhost_url(self.base_url)\n\n # Check if URL contains /v1 suffix (OpenAI-compatible mode)\n if transformed_base_url and transformed_base_url.rstrip(\"/\").endswith(\"/v1\"):\n # Strip /v1 suffix and log warning\n transformed_base_url = transformed_base_url.rstrip(\"/\").removesuffix(\"/v1\")\n logger.warning(\n \"Detected '/v1' suffix in base URL. The Ollama component uses the native Ollama API, \"\n \"not the OpenAI-compatible API. The '/v1' suffix has been automatically removed. \"\n \"If you want to use the OpenAI-compatible API, please use the OpenAI component instead. \"\n \"Learn more at https://docs.ollama.com/openai#openai-compatibility\"\n )\n\n try:\n output_format = self._parse_format_field(self.format) if self.enable_structured_output else None\n except Exception as e:\n msg = f\"Failed to parse the format field: {e}\"\n raise ValueError(msg) from e\n\n # Mapping system settings to their corresponding values\n llm_params = {\n \"base_url\": transformed_base_url,\n \"model\": self.model_name,\n \"mirostat\": mirostat_value,\n \"format\": output_format or None,\n \"metadata\": self.metadata,\n \"tags\": self.tags.split(\",\") if self.tags else None,\n \"mirostat_eta\": mirostat_eta,\n \"mirostat_tau\": mirostat_tau,\n \"num_ctx\": self.num_ctx or None,\n \"num_gpu\": self.num_gpu or None,\n \"num_thread\": self.num_thread or None,\n \"repeat_last_n\": self.repeat_last_n or None,\n \"repeat_penalty\": self.repeat_penalty or None,\n \"temperature\": self.temperature or None,\n \"stop\": self.stop_tokens.split(\",\") if self.stop_tokens else None,\n \"system\": self.system,\n \"tfs_z\": self.tfs_z or None,\n \"timeout\": self.timeout or None,\n \"top_k\": self.top_k or None,\n \"top_p\": self.top_p or None,\n \"verbose\": self.enable_verbose_output or False,\n \"template\": self.template,\n }\n headers = self.headers\n if headers is not None:\n llm_params[\"client_kwargs\"] = {\"headers\": headers}\n\n # Remove parameters with None values\n llm_params = {k: v for k, v in llm_params.items() if v is not None}\n\n try:\n output = ChatOllama(**llm_params)\n except Exception as e:\n msg = (\n \"Unable to connect to the Ollama API. \"\n \"Please verify the base URL, ensure the relevant Ollama model is pulled, and try again.\"\n )\n raise ValueError(msg) from e\n\n return output\n\n async def is_valid_ollama_url(self, url: str) -> bool:\n try:\n async with httpx.AsyncClient() as client:\n url = transform_localhost_url(url)\n if not url:\n return False\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n url = url.rstrip(\"/\").removesuffix(\"/v1\")\n if not url.endswith(\"/\"):\n url = url + \"/\"\n return (\n await client.get(url=urljoin(url, \"api/tags\"), headers=self.headers)\n ).status_code == HTTP_STATUS_OK\n except httpx.RequestError:\n return False\n\n async def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None):\n if field_name == \"enable_structured_output\": # bind enable_structured_output boolean to format show value\n build_config[\"format\"][\"show\"] = field_value\n\n if field_name == \"mirostat\":\n if field_value == \"Disabled\":\n build_config[\"mirostat_eta\"][\"advanced\"] = True\n build_config[\"mirostat_tau\"][\"advanced\"] = True\n build_config[\"mirostat_eta\"][\"value\"] = None\n build_config[\"mirostat_tau\"][\"value\"] = None\n\n else:\n build_config[\"mirostat_eta\"][\"advanced\"] = False\n build_config[\"mirostat_tau\"][\"advanced\"] = False\n\n if field_value == \"Mirostat 2.0\":\n build_config[\"mirostat_eta\"][\"value\"] = 0.2\n build_config[\"mirostat_tau\"][\"value\"] = 10\n else:\n build_config[\"mirostat_eta\"][\"value\"] = 0.1\n build_config[\"mirostat_tau\"][\"value\"] = 5\n\n if field_name in {\"model_name\", \"base_url\", \"tool_model_enabled\"}:\n # Use field_value if base_url is being updated, otherwise use self.base_url\n base_url_to_check = field_value if field_name == \"base_url\" else self.base_url\n # Fallback to self.base_url if field_value is None or empty\n if not base_url_to_check and field_name == \"base_url\":\n base_url_to_check = self.base_url\n logger.warning(f\"Fetching Ollama models from updated URL: {base_url_to_check}\")\n\n if base_url_to_check and await self.is_valid_ollama_url(base_url_to_check):\n tool_model_enabled = build_config[\"tool_model_enabled\"].get(\"value\", False) or self.tool_model_enabled\n build_config[\"model_name\"][\"options\"] = await self.get_models(\n base_url_to_check, tool_model_enabled=tool_model_enabled\n )\n else:\n build_config[\"model_name\"][\"options\"] = []\n if field_name == \"keep_alive_flag\":\n if field_value == \"Keep\":\n build_config[\"keep_alive\"][\"value\"] = \"-1\"\n build_config[\"keep_alive\"][\"advanced\"] = True\n elif field_value == \"Immediately\":\n build_config[\"keep_alive\"][\"value\"] = \"0\"\n build_config[\"keep_alive\"][\"advanced\"] = True\n else:\n build_config[\"keep_alive\"][\"advanced\"] = False\n\n return build_config\n\n async def get_models(self, base_url_value: str, *, tool_model_enabled: bool | None = None) -> list[str]:\n \"\"\"Fetches a list of models from the Ollama API suitable for text generation.\n\n Args:\n base_url_value (str): The base URL of the Ollama API.\n tool_model_enabled (bool | None, optional): If True, filters the models further to include\n only those that support tool calling. Defaults to None.\n\n Returns:\n list[str]: A list of model names suitable for text generation. Models are included if:\n - They have the \"completion\" capability, OR\n - The capabilities field is not returned (backwards compatibility with older Ollama versions)\n If `tool_model_enabled` is True, only models with verified \"tools\" capability are included\n (models without capabilities info are excluded in this case).\n\n Raises:\n ValueError: If there is an issue with the API request or response, or if the model\n names cannot be retrieved.\n \"\"\"\n try:\n # Strip /v1 suffix if present, as Ollama API endpoints are at root level\n base_url = base_url_value.rstrip(\"/\").removesuffix(\"/v1\")\n if not base_url.endswith(\"/\"):\n base_url = base_url + \"/\"\n base_url = transform_localhost_url(base_url)\n\n # Ollama REST API to return models\n tags_url = urljoin(base_url, \"api/tags\")\n\n # Ollama REST API to return model capabilities\n show_url = urljoin(base_url, \"api/show\")\n\n async with httpx.AsyncClient() as client:\n headers = self.headers\n # Fetch available models\n tags_response = await client.get(url=tags_url, headers=headers)\n tags_response.raise_for_status()\n models = tags_response.json()\n if asyncio.iscoroutine(models):\n models = await models\n await logger.adebug(f\"Available models: {models}\")\n\n # Filter models that are NOT embedding models\n model_ids = []\n for model in models[self.JSON_MODELS_KEY]:\n model_name = model[self.JSON_NAME_KEY]\n await logger.adebug(f\"Checking model: {model_name}\")\n\n payload = {\"model\": model_name}\n show_response = await client.post(url=show_url, json=payload, headers=headers)\n show_response.raise_for_status()\n json_data = show_response.json()\n if asyncio.iscoroutine(json_data):\n json_data = await json_data\n\n capabilities = json_data.get(self.JSON_CAPABILITIES_KEY)\n await logger.adebug(f\"Model: {model_name}, Capabilities: {capabilities}\")\n\n # If capabilities not provided, assume it's a completion model (backwards compatibility\n # with older Ollama versions that don't return capabilities from /api/show)\n if capabilities is None:\n if not tool_model_enabled:\n model_ids.append(model_name)\n # If tool_model_enabled is True but no capabilities info, skip the model\n # since we can't verify tool support\n elif self.DESIRED_CAPABILITY in capabilities and (\n not tool_model_enabled or self.TOOL_CALLING_CAPABILITY in capabilities\n ):\n model_ids.append(model_name)\n\n except (httpx.RequestError, ValueError) as e:\n msg = \"Could not get model names from Ollama.\"\n raise ValueError(msg) from e\n\n return model_ids\n\n def _parse_format_field(self, format_value: Any) -> Any:\n \"\"\"Parse the format field to handle both string and dict inputs.\n\n The format field can be:\n - A simple string like \"json\" (backward compatibility)\n - A JSON string from NestedDictInput that needs parsing\n - A dict/JSON schema (already parsed)\n - None or empty\n\n Args:\n format_value: The raw format value from the input field\n\n Returns:\n Parsed format value as string, dict, or None\n \"\"\"\n if not format_value:\n return None\n\n schema = format_value\n if isinstance(format_value, list):\n schema = build_model_from_schema(format_value).model_json_schema()\n if schema == self.default_table_row_schema:\n return None # the rows are generic placeholder rows\n elif isinstance(format_value, str): # parse as json if string\n with suppress(json.JSONDecodeError): # e.g., literal \"json\" is valid for format field\n schema = json.loads(format_value)\n\n return schema or None\n\n async def _parse_json_response(self) -> Any:\n \"\"\"Parse the JSON response from the model.\n\n This method gets the text response and attempts to parse it as JSON.\n Works with models that have format='json' or a JSON schema set.\n\n Returns:\n Parsed JSON (dict, list, or primitive type)\n\n Raises:\n ValueError: If the response is not valid JSON\n \"\"\"\n message = await self.text_response()\n text = message.text if hasattr(message, \"text\") else str(message)\n\n if not text:\n msg = \"No response from model\"\n raise ValueError(msg)\n\n try:\n return json.loads(text)\n except json.JSONDecodeError as e:\n msg = f\"Invalid JSON response. Ensure model supports JSON output. Error: {e}\"\n raise ValueError(msg) from e\n\n async def build_data_output(self) -> Data:\n \"\"\"Build a Data output from the model's JSON response.\n\n Returns:\n Data: A Data object containing the parsed JSON response\n \"\"\"\n parsed = await self._parse_json_response()\n\n # If the response is already a dict, wrap it in Data\n if isinstance(parsed, dict):\n return Data(data=parsed)\n\n # If it's a list, wrap in a results container\n if isinstance(parsed, list):\n if len(parsed) == 1:\n return Data(data=parsed[0])\n return Data(data={\"results\": parsed})\n\n # For primitive types, wrap in a value container\n return Data(data={\"value\": parsed})\n\n async def build_dataframe_output(self) -> DataFrame:\n \"\"\"Build a DataFrame output from the model's JSON response.\n\n Returns:\n DataFrame: A DataFrame containing the parsed JSON response\n\n Raises:\n ValueError: If the response cannot be converted to a DataFrame\n \"\"\"\n parsed = await self._parse_json_response()\n\n # If it's a list of dicts, convert directly to DataFrame\n if isinstance(parsed, list):\n if not parsed:\n return DataFrame()\n # Ensure all items are dicts for proper DataFrame conversion\n if all(isinstance(item, dict) for item in parsed):\n return DataFrame(parsed)\n msg = \"List items must be dictionaries to convert to DataFrame\"\n raise ValueError(msg)\n\n # If it's a single dict, wrap in a list to create a single-row DataFrame\n if isinstance(parsed, dict):\n return DataFrame([parsed])\n\n # For primitive types, create a single-column DataFrame\n return DataFrame([{\"value\": parsed}])\n\n @property\n def headers(self) -> dict[str, str] | None:\n \"\"\"Get the headers for the Ollama API.\"\"\"\n if self.api_key and self.api_key.strip():\n return {\"Authorization\": f\"Bearer {self.api_key}\"}\n return None\n" }, "enable_structured_output": { "_input_type": "BoolInput", @@ -96676,6 +96765,10 @@ "display_name": "Format", "dynamic": false, "info": "Specify the format of the output.", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "format", @@ -98931,8 +99024,8 @@ { "pgvector": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -98980,24 +99073,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -99183,8 +99276,8 @@ { "Pinecone": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -99243,24 +99336,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -99516,8 +99609,8 @@ { "AlterMetadata": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -99536,7 +99629,7 @@ "icon": "merge", "legacy": true, "metadata": { - "code_hash": "0b2fe62eaec4", + "code_hash": "a209b85f75c1", "dependencies": { "dependencies": [ { @@ -99554,28 +99647,28 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "process_output", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -99602,7 +99695,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import HandleInput, NestedDictInput, Output, StrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass AlterMetadataComponent(Component):\n display_name = \"Alter Metadata\"\n description = \"Adds/Removes Metadata Dictionary on inputs\"\n icon = \"merge\"\n name = \"AlterMetadata\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n HandleInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"Object(s) to which Metadata should be added\",\n required=False,\n input_types=[\"Message\", \"Data\"],\n is_list=True,\n ),\n StrInput(\n name=\"text_in\",\n display_name=\"User Text\",\n info=\"Text input; value will be in 'text' attribute of Data object. Empty text entries are ignored.\",\n required=False,\n ),\n NestedDictInput(\n name=\"metadata\",\n display_name=\"Metadata\",\n info=\"Metadata to add to each object\",\n input_types=[\"Data\"],\n required=True,\n ),\n MessageTextInput(\n name=\"remove_fields\",\n display_name=\"Fields to Remove\",\n info=\"Metadata Fields to Remove\",\n required=False,\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(\n name=\"data\",\n display_name=\"Data\",\n info=\"List of Input objects each with added Metadata\",\n method=\"process_output\",\n ),\n Output(\n display_name=\"DataFrame\",\n name=\"dataframe\",\n info=\"Data objects as a DataFrame, with metadata as columns\",\n method=\"as_dataframe\",\n ),\n ]\n\n def _as_clean_dict(self, obj):\n \"\"\"Convert a Data object or a standard dictionary to a standard dictionary.\"\"\"\n if isinstance(obj, dict):\n as_dict = obj\n elif isinstance(obj, Data):\n as_dict = obj.data\n else:\n msg = f\"Expected a Data object or a dictionary but got {type(obj)}.\"\n raise TypeError(msg)\n\n return {k: v for k, v in (as_dict or {}).items() if k and k.strip()}\n\n def process_output(self) -> list[Data]:\n # Ensure metadata is a dictionary, filtering out any empty keys\n metadata = self._as_clean_dict(self.metadata)\n\n # Convert text_in to a Data object if it exists, and initialize our list of Data objects\n data_objects = [Data(text=self.text_in)] if self.text_in else []\n\n # Append existing Data objects from input_value, if any\n if self.input_value:\n data_objects.extend(self.input_value)\n\n # Update each Data object with the new metadata, preserving existing fields\n for data in data_objects:\n data.data.update(metadata)\n\n # Handle removal of fields specified in remove_fields\n if self.remove_fields:\n fields_to_remove = {field.strip() for field in self.remove_fields if field.strip()}\n\n # Remove specified fields from each Data object's metadata\n for data in data_objects:\n data.data = {k: v for k, v in data.data.items() if k not in fields_to_remove}\n\n # Set the status for tracking/debugging purposes\n self.status = data_objects\n return data_objects\n\n def as_dataframe(self) -> DataFrame:\n \"\"\"Convert the processed data objects into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame where each row corresponds to a Data object,\n with metadata fields as columns.\n \"\"\"\n data_list = self.process_output()\n return DataFrame(data_list)\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import HandleInput, NestedDictInput, Output, StrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass AlterMetadataComponent(Component):\n display_name = \"Alter Metadata\"\n description = \"Adds/Removes Metadata Dictionary on inputs\"\n icon = \"merge\"\n name = \"AlterMetadata\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n HandleInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"Object(s) to which Metadata should be added\",\n required=False,\n input_types=[\"Message\", \"Data\", \"JSON\"],\n is_list=True,\n ),\n StrInput(\n name=\"text_in\",\n display_name=\"User Text\",\n info=\"Text input; value will be in 'text' attribute of Data object. Empty text entries are ignored.\",\n required=False,\n ),\n NestedDictInput(\n name=\"metadata\",\n display_name=\"Metadata\",\n info=\"Metadata to add to each object\",\n input_types=[\"Data\", \"JSON\"],\n required=True,\n ),\n MessageTextInput(\n name=\"remove_fields\",\n display_name=\"Fields to Remove\",\n info=\"Metadata Fields to Remove\",\n required=False,\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(\n name=\"data\",\n display_name=\"JSON\",\n info=\"List of Input objects each with added Metadata\",\n method=\"process_output\",\n ),\n Output(\n display_name=\"Table\",\n name=\"dataframe\",\n info=\"Data objects as a DataFrame, with metadata as columns\",\n method=\"as_dataframe\",\n ),\n ]\n\n def _as_clean_dict(self, obj):\n \"\"\"Convert a Data object or a standard dictionary to a standard dictionary.\"\"\"\n if isinstance(obj, dict):\n as_dict = obj\n elif isinstance(obj, Data):\n as_dict = obj.data\n else:\n msg = f\"Expected a Data object or a dictionary but got {type(obj)}.\"\n raise TypeError(msg)\n\n return {k: v for k, v in (as_dict or {}).items() if k and k.strip()}\n\n def process_output(self) -> list[Data]:\n # Ensure metadata is a dictionary, filtering out any empty keys\n metadata = self._as_clean_dict(self.metadata)\n\n # Convert text_in to a Data object if it exists, and initialize our list of Data objects\n data_objects = [Data(text=self.text_in)] if self.text_in else []\n\n # Append existing Data objects from input_value, if any\n if self.input_value:\n data_objects.extend(self.input_value)\n\n # Update each Data object with the new metadata, preserving existing fields\n for data in data_objects:\n data.data.update(metadata)\n\n # Handle removal of fields specified in remove_fields\n if self.remove_fields:\n fields_to_remove = {field.strip() for field in self.remove_fields if field.strip()}\n\n # Remove specified fields from each Data object's metadata\n for data in data_objects:\n data.data = {k: v for k, v in data.data.items() if k not in fields_to_remove}\n\n # Set the status for tracking/debugging purposes\n self.status = data_objects\n return data_objects\n\n def as_dataframe(self) -> DataFrame:\n \"\"\"Convert the processed data objects into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame where each row corresponds to a Data object,\n with metadata fields as columns.\n \"\"\"\n data_list = self.process_output()\n return DataFrame(data_list)\n" }, "input_value": { "_input_type": "HandleInput", @@ -99612,7 +99705,8 @@ "info": "Object(s) to which Metadata should be added", "input_types": [ "Message", - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -99634,7 +99728,8 @@ "dynamic": false, "info": "Metadata to add to each object", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -99854,7 +99949,7 @@ }, "CreateData": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -99872,7 +99967,7 @@ "icon": "ListFilter", "legacy": true, "metadata": { - "code_hash": "3e313525090d", + "code_hash": "10b0eae5a063", "dependencies": { "dependencies": [ { @@ -99890,14 +99985,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "build_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -99924,7 +100019,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DictInput, IntInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\n\n\nclass CreateDataComponent(Component):\n display_name: str = \"Create Data\"\n description: str = \"Dynamically create a Data with a specified number of fields.\"\n name: str = \"CreateData\"\n MAX_FIELDS = 15 # Define a constant for maximum number of fields\n legacy = True\n replacement = [\"processing.DataOperations\"]\n icon = \"ListFilter\"\n\n inputs = [\n IntInput(\n name=\"number_of_fields\",\n display_name=\"Number of Fields\",\n info=\"Number of fields to be added to the record.\",\n real_time_refresh=True,\n value=1,\n range_spec=RangeSpec(min=1, max=MAX_FIELDS, step=1, step_type=\"int\"),\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"Key that identifies the field to be used as the text content.\",\n advanced=True,\n ),\n BoolInput(\n name=\"text_key_validator\",\n display_name=\"Text Key Validator\",\n advanced=True,\n info=\"If enabled, checks if the given 'Text Key' is present in the given 'Data'.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"build_data\"),\n ]\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):\n if field_name == \"number_of_fields\":\n default_keys = [\"code\", \"_type\", \"number_of_fields\", \"text_key\", \"text_key_validator\"]\n try:\n field_value_int = int(field_value)\n except ValueError:\n return build_config\n existing_fields = {}\n if field_value_int > self.MAX_FIELDS:\n build_config[\"number_of_fields\"][\"value\"] = self.MAX_FIELDS\n msg = (\n f\"Number of fields cannot exceed {self.MAX_FIELDS}. \"\n \"Please adjust the number of fields to be within the allowed limit.\"\n )\n raise ValueError(msg)\n if len(build_config) > len(default_keys):\n # back up the existing template fields\n for key in build_config.copy():\n if key not in default_keys:\n existing_fields[key] = build_config.pop(key)\n\n for i in range(1, field_value_int + 1):\n key = f\"field_{i}_key\"\n if key in existing_fields:\n field = existing_fields[key]\n build_config[key] = field\n else:\n field = DictInput(\n display_name=f\"Field {i}\",\n name=key,\n info=f\"Key for field {i}.\",\n input_types=[\"Message\", \"Data\"],\n )\n build_config[field.name] = field.to_dict()\n\n build_config[\"number_of_fields\"][\"value\"] = field_value_int\n return build_config\n\n async def build_data(self) -> Data:\n data = self.get_data()\n return_data = Data(data=data, text_key=self.text_key)\n self.status = return_data\n if self.text_key_validator:\n self.validate_text_key()\n return return_data\n\n def get_data(self):\n \"\"\"Function to get the Data from the attributes.\"\"\"\n data = {}\n for value_dict in self._attributes.values():\n if isinstance(value_dict, dict):\n # Check if the value of the value_dict is a Data\n value_dict_ = {\n key: value.get_text() if isinstance(value, Data) else value for key, value in value_dict.items()\n }\n data.update(value_dict_)\n return data\n\n def validate_text_key(self) -> None:\n \"\"\"This function validates that the Text Key is one of the keys in the Data.\"\"\"\n data_keys = self.get_data().keys()\n if self.text_key not in data_keys and self.text_key != \"\":\n formatted_data_keys = \", \".join(data_keys)\n msg = f\"Text Key: '{self.text_key}' not found in the Data keys: '{formatted_data_keys}'\"\n raise ValueError(msg)\n" + "value": "from typing import Any\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DictInput, IntInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\n\n\nclass CreateDataComponent(Component):\n display_name: str = \"Create Data\"\n description: str = \"Dynamically create a Data with a specified number of fields.\"\n name: str = \"CreateData\"\n MAX_FIELDS = 15 # Define a constant for maximum number of fields\n legacy = True\n replacement = [\"processing.DataOperations\"]\n icon = \"ListFilter\"\n\n inputs = [\n IntInput(\n name=\"number_of_fields\",\n display_name=\"Number of Fields\",\n info=\"Number of fields to be added to the record.\",\n real_time_refresh=True,\n value=1,\n range_spec=RangeSpec(min=1, max=MAX_FIELDS, step=1, step_type=\"int\"),\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"Key that identifies the field to be used as the text content.\",\n advanced=True,\n ),\n BoolInput(\n name=\"text_key_validator\",\n display_name=\"Text Key Validator\",\n advanced=True,\n info=\"If enabled, checks if the given 'Text Key' is present in the given 'Data'.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"build_data\"),\n ]\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):\n if field_name == \"number_of_fields\":\n default_keys = [\"code\", \"_type\", \"number_of_fields\", \"text_key\", \"text_key_validator\"]\n try:\n field_value_int = int(field_value)\n except ValueError:\n return build_config\n existing_fields = {}\n if field_value_int > self.MAX_FIELDS:\n build_config[\"number_of_fields\"][\"value\"] = self.MAX_FIELDS\n msg = (\n f\"Number of fields cannot exceed {self.MAX_FIELDS}. \"\n \"Please adjust the number of fields to be within the allowed limit.\"\n )\n raise ValueError(msg)\n if len(build_config) > len(default_keys):\n # back up the existing template fields\n for key in build_config.copy():\n if key not in default_keys:\n existing_fields[key] = build_config.pop(key)\n\n for i in range(1, field_value_int + 1):\n key = f\"field_{i}_key\"\n if key in existing_fields:\n field = existing_fields[key]\n build_config[key] = field\n else:\n field = DictInput(\n display_name=f\"Field {i}\",\n name=key,\n info=f\"Key for field {i}.\",\n input_types=[\"Message\", \"Data\", \"JSON\"],\n )\n build_config[field.name] = field.to_dict()\n\n build_config[\"number_of_fields\"][\"value\"] = field_value_int\n return build_config\n\n async def build_data(self) -> Data:\n data = self.get_data()\n return_data = Data(data=data, text_key=self.text_key)\n self.status = return_data\n if self.text_key_validator:\n self.validate_text_key()\n return return_data\n\n def get_data(self):\n \"\"\"Function to get the Data from the attributes.\"\"\"\n data = {}\n for value_dict in self._attributes.values():\n if isinstance(value_dict, dict):\n # Check if the value of the value_dict is a Data\n value_dict_ = {\n key: value.get_text() if isinstance(value, Data) else value for key, value in value_dict.items()\n }\n data.update(value_dict_)\n return data\n\n def validate_text_key(self) -> None:\n \"\"\"This function validates that the Text Key is one of the keys in the Data.\"\"\"\n data_keys = self.get_data().keys()\n if self.text_key not in data_keys and self.text_key != \"\":\n formatted_data_keys = \", \".join(data_keys)\n msg = f\"Text Key: '{self.text_key}' not found in the Data keys: '{formatted_data_keys}'\"\n raise ValueError(msg)\n" }, "number_of_fields": { "_input_type": "IntInput", @@ -100003,8 +100098,8 @@ }, "CreateList": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -100020,7 +100115,7 @@ "icon": "list", "legacy": true, "metadata": { - "code_hash": "9ec770d03310", + "code_hash": "565738357961", "dependencies": { "dependencies": [ { @@ -100038,28 +100133,28 @@ { "allows_loop": false, "cache": true, - "display_name": "Data List", + "display_name": "JSON List", "group_outputs": false, "method": "create_list", "name": "list", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -100083,7 +100178,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import StrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass CreateListComponent(Component):\n display_name = \"Create List\"\n description = \"Creates a list of texts.\"\n icon = \"list\"\n name = \"CreateList\"\n legacy = True\n\n inputs = [\n StrInput(\n name=\"texts\",\n display_name=\"Texts\",\n info=\"Enter one or more texts.\",\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data List\", name=\"list\", method=\"create_list\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def create_list(self) -> list[Data]:\n data = [Data(text=text) for text in self.texts]\n self.status = data\n return data\n\n def as_dataframe(self) -> DataFrame:\n \"\"\"Convert the list of Data objects into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the list data.\n \"\"\"\n return DataFrame(self.create_list())\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import StrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass CreateListComponent(Component):\n display_name = \"Create List\"\n description = \"Creates a list of texts.\"\n icon = \"list\"\n name = \"CreateList\"\n legacy = True\n\n inputs = [\n StrInput(\n name=\"texts\",\n display_name=\"Texts\",\n info=\"Enter one or more texts.\",\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON List\", name=\"list\", method=\"create_list\"),\n Output(display_name=\"Table\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def create_list(self) -> list[Data]:\n data = [Data(text=text) for text in self.texts]\n self.status = data\n return data\n\n def as_dataframe(self) -> DataFrame:\n \"\"\"Convert the list of Data objects into a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the list data.\n \"\"\"\n return DataFrame(self.create_list())\n" }, "texts": { "_input_type": "StrInput", @@ -100111,13 +100206,13 @@ }, "DataFrameOperations": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], "custom_fields": {}, - "description": "Perform various operations on a DataFrame.", - "display_name": "DataFrame Operations", + "description": "Perform various operations on a Table.", + "display_name": "Table Operations", "documentation": "https://docs.langflow.org/dataframe-operations", "edited": false, "field_order": [ @@ -100140,7 +100235,7 @@ "icon": "table", "legacy": false, "metadata": { - "code_hash": "e2b4323d4ed5", + "code_hash": "3a3aca2d9d1f", "dependencies": { "dependencies": [ { @@ -100154,6 +100249,22 @@ ], "total_dependencies": 2 }, + "keywords": [ + "dataframe", + "dataframe operations", + "table", + "table operations", + "filter", + "sort", + "merge", + "concatenate", + "drop column", + "rename column", + "add column", + "select columns", + "replace value", + "drop duplicates" + ], "module": "lfx.components.processing.dataframe_operations.DataFrameOperationsComponent" }, "minimized": false, @@ -100162,14 +100273,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "perform_operation", "name": "output", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -100213,7 +100324,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import pandas as pd\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs import SortableListInput\nfrom lfx.io import BoolInput, DataFrameInput, DropdownInput, IntInput, MessageTextInput, Output, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass DataFrameOperationsComponent(Component):\n display_name = \"DataFrame Operations\"\n description = \"Perform various operations on a DataFrame.\"\n documentation: str = \"https://docs.langflow.org/dataframe-operations\"\n icon = \"table\"\n name = \"DataFrameOperations\"\n\n OPERATION_CHOICES = [\n \"Add Column\",\n \"Concatenate\",\n \"Drop Column\",\n \"Filter\",\n \"Head\",\n \"Merge\",\n \"Rename Column\",\n \"Replace Value\",\n \"Select Columns\",\n \"Sort\",\n \"Tail\",\n \"Drop Duplicates\",\n ]\n\n inputs = [\n DataFrameInput(\n name=\"df\",\n display_name=\"DataFrame\",\n info=\"The input DataFrame to operate on. Connect multiple DataFrames for merge or concatenate operations.\",\n required=True,\n is_list=True,\n ),\n SortableListInput(\n name=\"operation\",\n display_name=\"Operation\",\n placeholder=\"Select Operation\",\n info=\"Select the DataFrame operation to perform.\",\n options=[\n {\"name\": \"Add Column\", \"icon\": \"plus\"},\n {\"name\": \"Concatenate\", \"icon\": \"combine\"},\n {\"name\": \"Drop Column\", \"icon\": \"minus\"},\n {\"name\": \"Filter\", \"icon\": \"filter\"},\n {\"name\": \"Head\", \"icon\": \"arrow-up\"},\n {\"name\": \"Merge\", \"icon\": \"merge\"},\n {\"name\": \"Rename Column\", \"icon\": \"pencil\"},\n {\"name\": \"Replace Value\", \"icon\": \"replace\"},\n {\"name\": \"Select Columns\", \"icon\": \"columns\"},\n {\"name\": \"Sort\", \"icon\": \"arrow-up-down\"},\n {\"name\": \"Tail\", \"icon\": \"arrow-down\"},\n {\"name\": \"Drop Duplicates\", \"icon\": \"copy-x\"},\n ],\n real_time_refresh=True,\n limit=1,\n ),\n StrInput(\n name=\"column_name\",\n display_name=\"Column Name\",\n info=\"The column name to use for the operation.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"filter_value\",\n display_name=\"Filter Value\",\n info=\"The value to filter rows by.\",\n dynamic=True,\n show=False,\n ),\n DropdownInput(\n name=\"filter_operator\",\n display_name=\"Filter Operator\",\n options=[\n \"equals\",\n \"not equals\",\n \"contains\",\n \"not contains\",\n \"starts with\",\n \"ends with\",\n \"greater than\",\n \"less than\",\n ],\n value=\"equals\",\n info=\"The operator to apply for filtering rows.\",\n advanced=False,\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"ascending\",\n display_name=\"Sort Ascending\",\n info=\"Whether to sort in ascending order.\",\n dynamic=True,\n show=False,\n value=True,\n ),\n StrInput(\n name=\"new_column_name\",\n display_name=\"New Column Name\",\n info=\"The new column name when renaming or adding a column.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"new_column_value\",\n display_name=\"New Column Value\",\n info=\"The value to populate the new column with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"columns_to_select\",\n display_name=\"Columns to Select\",\n dynamic=True,\n is_list=True,\n show=False,\n ),\n IntInput(\n name=\"num_rows\",\n display_name=\"Number of Rows\",\n info=\"Number of rows to return (for head/tail).\",\n dynamic=True,\n show=False,\n value=5,\n ),\n MessageTextInput(\n name=\"replace_value\",\n display_name=\"Value to Replace\",\n info=\"The value to replace in the column.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"replacement_value\",\n display_name=\"Replacement Value\",\n info=\"The value to replace with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"merge_on_column\",\n display_name=\"Merge On Column\",\n info=\"The column name to merge DataFrames on. Must exist in both DataFrames.\",\n dynamic=True,\n show=False,\n ),\n DropdownInput(\n name=\"merge_how\",\n display_name=\"Merge Type\",\n options=[\"inner\", \"outer\", \"left\", \"right\"],\n value=\"inner\",\n info=\"Type of merge: inner (intersection), outer (union), left, or right.\",\n dynamic=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"DataFrame\",\n name=\"output\",\n method=\"perform_operation\",\n info=\"The resulting DataFrame after the operation.\",\n )\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n dynamic_fields = [\n \"column_name\",\n \"filter_value\",\n \"filter_operator\",\n \"ascending\",\n \"new_column_name\",\n \"new_column_value\",\n \"columns_to_select\",\n \"num_rows\",\n \"replace_value\",\n \"replacement_value\",\n \"merge_on_column\",\n \"merge_how\",\n ]\n for field in dynamic_fields:\n build_config[field][\"show\"] = False\n\n if field_name == \"operation\":\n # Handle SortableListInput format\n if isinstance(field_value, list):\n operation_name = field_value[0].get(\"name\", \"\") if field_value else \"\"\n else:\n operation_name = field_value or \"\"\n\n # If no operation selected, all dynamic fields stay hidden (already set to False above)\n if not operation_name:\n return build_config\n\n if operation_name == \"Filter\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"filter_value\"][\"show\"] = True\n build_config[\"filter_operator\"][\"show\"] = True\n elif operation_name == \"Sort\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"ascending\"][\"show\"] = True\n elif operation_name == \"Drop Column\":\n build_config[\"column_name\"][\"show\"] = True\n elif operation_name == \"Rename Column\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"new_column_name\"][\"show\"] = True\n elif operation_name == \"Add Column\":\n build_config[\"new_column_name\"][\"show\"] = True\n build_config[\"new_column_value\"][\"show\"] = True\n elif operation_name == \"Select Columns\":\n build_config[\"columns_to_select\"][\"show\"] = True\n elif operation_name in {\"Head\", \"Tail\"}:\n build_config[\"num_rows\"][\"show\"] = True\n elif operation_name == \"Replace Value\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"replace_value\"][\"show\"] = True\n build_config[\"replacement_value\"][\"show\"] = True\n elif operation_name == \"Drop Duplicates\":\n build_config[\"column_name\"][\"show\"] = True\n elif operation_name == \"Merge\":\n build_config[\"merge_on_column\"][\"show\"] = True\n build_config[\"merge_how\"][\"show\"] = True\n\n return build_config\n\n def _get_primary_dataframe(self) -> DataFrame:\n \"\"\"Get the first DataFrame from input (handles both single and list inputs).\"\"\"\n if isinstance(self.df, list):\n return self.df[0].copy() if self.df else DataFrame()\n return self.df.copy()\n\n def perform_operation(self) -> DataFrame:\n df_copy = self._get_primary_dataframe()\n\n # Handle SortableListInput format for operation (also supports legacy string format)\n operation_input = getattr(self, \"operation\", [])\n if isinstance(operation_input, list):\n op = operation_input[0].get(\"name\", \"\") if operation_input else \"\"\n else:\n op = operation_input or \"\"\n\n # If no operation selected, return original DataFrame\n if not op:\n return df_copy\n\n if op == \"Filter\":\n return self.filter_rows_by_value(df_copy)\n if op == \"Sort\":\n return self.sort_by_column(df_copy)\n if op == \"Drop Column\":\n return self.drop_column(df_copy)\n if op == \"Rename Column\":\n return self.rename_column(df_copy)\n if op == \"Add Column\":\n return self.add_column(df_copy)\n if op == \"Select Columns\":\n return self.select_columns(df_copy)\n if op == \"Head\":\n return self.head(df_copy)\n if op == \"Tail\":\n return self.tail(df_copy)\n if op == \"Replace Value\":\n return self.replace_values(df_copy)\n if op == \"Drop Duplicates\":\n return self.drop_duplicates(df_copy)\n if op == \"Concatenate\":\n return self.concatenate_dataframes()\n if op == \"Merge\":\n return self.merge_dataframes()\n msg = f\"Unsupported operation: {op}\"\n logger.error(msg)\n raise ValueError(msg)\n\n def filter_rows_by_value(self, df: DataFrame) -> DataFrame:\n column = df[self.column_name]\n filter_value = self.filter_value\n\n # Handle regular DropdownInput format (just a string value)\n operator = getattr(self, \"filter_operator\", \"equals\") # Default to equals for backward compatibility\n\n if operator == \"equals\":\n mask = column == filter_value\n elif operator == \"not equals\":\n mask = column != filter_value\n elif operator == \"contains\":\n mask = column.astype(str).str.contains(str(filter_value), na=False)\n elif operator == \"not contains\":\n mask = ~column.astype(str).str.contains(str(filter_value), na=False)\n elif operator == \"starts with\":\n mask = column.astype(str).str.startswith(str(filter_value), na=False)\n elif operator == \"ends with\":\n mask = column.astype(str).str.endswith(str(filter_value), na=False)\n elif operator == \"greater than\":\n try:\n # Try to convert filter_value to numeric for comparison\n numeric_value = pd.to_numeric(filter_value)\n mask = column > numeric_value\n except (ValueError, TypeError):\n # If conversion fails, compare as strings\n mask = column.astype(str) > str(filter_value)\n elif operator == \"less than\":\n try:\n # Try to convert filter_value to numeric for comparison\n numeric_value = pd.to_numeric(filter_value)\n mask = column < numeric_value\n except (ValueError, TypeError):\n # If conversion fails, compare as strings\n mask = column.astype(str) < str(filter_value)\n else:\n mask = column == filter_value # Fallback to equals\n\n return DataFrame(df[mask])\n\n def sort_by_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.sort_values(by=self.column_name, ascending=self.ascending))\n\n def drop_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.drop(columns=[self.column_name]))\n\n def rename_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.rename(columns={self.column_name: self.new_column_name}))\n\n def add_column(self, df: DataFrame) -> DataFrame:\n df[self.new_column_name] = [self.new_column_value] * len(df)\n return DataFrame(df)\n\n def select_columns(self, df: DataFrame) -> DataFrame:\n columns = [col.strip() for col in self.columns_to_select]\n return DataFrame(df[columns])\n\n def head(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.head(self.num_rows))\n\n def tail(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.tail(self.num_rows))\n\n def replace_values(self, df: DataFrame) -> DataFrame:\n df[self.column_name] = df[self.column_name].replace(self.replace_value, self.replacement_value)\n return DataFrame(df)\n\n def drop_duplicates(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.drop_duplicates(subset=self.column_name))\n\n def concatenate_dataframes(self) -> DataFrame:\n \"\"\"Concatenate multiple DataFrames vertically (stack rows).\"\"\"\n if not isinstance(self.df, list) or len(self.df) == 0:\n return self.df.copy() if self.df is not None else DataFrame()\n\n # If only one DataFrame, return it\n if len(self.df) == 1:\n return self.df[0].copy()\n\n # Concatenate all DataFrames vertically\n concatenated = pd.concat(self.df, ignore_index=True)\n return DataFrame(concatenated)\n\n def merge_dataframes(self) -> DataFrame:\n \"\"\"Merge two DataFrames based on a common column (join operation).\"\"\"\n if not isinstance(self.df, list) or len(self.df) == 0:\n return self.df.copy() if self.df is not None else DataFrame()\n\n # If only one DataFrame, return it\n if len(self.df) == 1:\n return self.df[0].copy()\n\n # Merge requires exactly two DataFrames\n max_merge_inputs = 2\n if len(self.df) > max_merge_inputs:\n msg = f\"Merge requires exactly {max_merge_inputs} DataFrames, got {len(self.df)}\"\n raise ValueError(msg)\n\n df1 = self.df[0].copy()\n df2 = self.df[1].copy()\n\n merge_on = getattr(self, \"merge_on_column\", None)\n merge_how = getattr(self, \"merge_how\", \"inner\")\n\n # If merge column specified, validate it exists in both DataFrames\n if merge_on:\n if merge_on not in df1.columns:\n msg = f\"Column '{merge_on}' not found in first DataFrame. Available: {list(df1.columns)}\"\n raise ValueError(msg)\n if merge_on not in df2.columns:\n msg = f\"Column '{merge_on}' not found in second DataFrame. Available: {list(df2.columns)}\"\n raise ValueError(msg)\n\n merged = df1.merge(df2, on=merge_on, how=merge_how, suffixes=(\"\", \"_df2\"))\n else:\n merged = df1.merge(df2, left_index=True, right_index=True, how=merge_how, suffixes=(\"\", \"_df2\"))\n\n # Combine duplicate columns: use df1 value if exists, otherwise df2 value\n cols_to_drop = []\n for col in merged.columns:\n if col.endswith(\"_df2\"):\n original_col = col[:-4] # Remove \"_df2\" suffix\n if original_col in merged.columns:\n # Coalesce: use original if not null, otherwise use _df2\n merged[original_col] = merged[original_col].combine_first(merged[col])\n cols_to_drop.append(col)\n\n if cols_to_drop:\n merged = merged.drop(columns=cols_to_drop)\n\n return DataFrame(merged)\n" + "value": "import pandas as pd\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs import SortableListInput\nfrom lfx.io import BoolInput, DataFrameInput, DropdownInput, IntInput, MessageTextInput, Output, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass DataFrameOperationsComponent(Component):\n display_name = \"Table Operations\"\n description = \"Perform various operations on a Table.\"\n documentation: str = \"https://docs.langflow.org/dataframe-operations\"\n icon = \"table\"\n name = \"DataFrameOperations\"\n metadata = {\n \"keywords\": [\n \"dataframe\",\n \"dataframe operations\",\n \"table\",\n \"table operations\",\n \"filter\",\n \"sort\",\n \"merge\",\n \"concatenate\",\n \"drop column\",\n \"rename column\",\n \"add column\",\n \"select columns\",\n \"replace value\",\n \"drop duplicates\",\n ],\n }\n\n OPERATION_CHOICES = [\n \"Add Column\",\n \"Concatenate\",\n \"Drop Column\",\n \"Filter\",\n \"Head\",\n \"Merge\",\n \"Rename Column\",\n \"Replace Value\",\n \"Select Columns\",\n \"Sort\",\n \"Tail\",\n \"Drop Duplicates\",\n ]\n\n inputs = [\n DataFrameInput(\n name=\"df\",\n display_name=\"Table\",\n info=\"The input DataFrame to operate on. Connect multiple DataFrames for merge or concatenate operations.\",\n required=True,\n is_list=True,\n ),\n SortableListInput(\n name=\"operation\",\n display_name=\"Operation\",\n placeholder=\"Select Operation\",\n info=\"Select the DataFrame operation to perform.\",\n options=[\n {\"name\": \"Add Column\", \"icon\": \"plus\"},\n {\"name\": \"Concatenate\", \"icon\": \"combine\"},\n {\"name\": \"Drop Column\", \"icon\": \"minus\"},\n {\"name\": \"Filter\", \"icon\": \"filter\"},\n {\"name\": \"Head\", \"icon\": \"arrow-up\"},\n {\"name\": \"Merge\", \"icon\": \"merge\"},\n {\"name\": \"Rename Column\", \"icon\": \"pencil\"},\n {\"name\": \"Replace Value\", \"icon\": \"replace\"},\n {\"name\": \"Select Columns\", \"icon\": \"columns\"},\n {\"name\": \"Sort\", \"icon\": \"arrow-up-down\"},\n {\"name\": \"Tail\", \"icon\": \"arrow-down\"},\n {\"name\": \"Drop Duplicates\", \"icon\": \"copy-x\"},\n ],\n real_time_refresh=True,\n limit=1,\n ),\n StrInput(\n name=\"column_name\",\n display_name=\"Column Name\",\n info=\"The column name to use for the operation.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"filter_value\",\n display_name=\"Filter Value\",\n info=\"The value to filter rows by.\",\n dynamic=True,\n show=False,\n ),\n DropdownInput(\n name=\"filter_operator\",\n display_name=\"Filter Operator\",\n options=[\n \"equals\",\n \"not equals\",\n \"contains\",\n \"not contains\",\n \"starts with\",\n \"ends with\",\n \"greater than\",\n \"less than\",\n ],\n value=\"equals\",\n info=\"The operator to apply for filtering rows.\",\n advanced=False,\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"ascending\",\n display_name=\"Sort Ascending\",\n info=\"Whether to sort in ascending order.\",\n dynamic=True,\n show=False,\n value=True,\n ),\n StrInput(\n name=\"new_column_name\",\n display_name=\"New Column Name\",\n info=\"The new column name when renaming or adding a column.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"new_column_value\",\n display_name=\"New Column Value\",\n info=\"The value to populate the new column with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"columns_to_select\",\n display_name=\"Columns to Select\",\n dynamic=True,\n is_list=True,\n show=False,\n ),\n IntInput(\n name=\"num_rows\",\n display_name=\"Number of Rows\",\n info=\"Number of rows to return (for head/tail).\",\n dynamic=True,\n show=False,\n value=5,\n ),\n MessageTextInput(\n name=\"replace_value\",\n display_name=\"Value to Replace\",\n info=\"The value to replace in the column.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"replacement_value\",\n display_name=\"Replacement Value\",\n info=\"The value to replace with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"merge_on_column\",\n display_name=\"Merge On Column\",\n info=\"The column name to merge DataFrames on. Must exist in both DataFrames.\",\n dynamic=True,\n show=False,\n ),\n DropdownInput(\n name=\"merge_how\",\n display_name=\"Merge Type\",\n options=[\"inner\", \"outer\", \"left\", \"right\"],\n value=\"inner\",\n info=\"Type of merge: inner (intersection), outer (union), left, or right.\",\n dynamic=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Table\",\n name=\"output\",\n method=\"perform_operation\",\n info=\"The resulting DataFrame after the operation.\",\n )\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n dynamic_fields = [\n \"column_name\",\n \"filter_value\",\n \"filter_operator\",\n \"ascending\",\n \"new_column_name\",\n \"new_column_value\",\n \"columns_to_select\",\n \"num_rows\",\n \"replace_value\",\n \"replacement_value\",\n \"merge_on_column\",\n \"merge_how\",\n ]\n for field in dynamic_fields:\n build_config[field][\"show\"] = False\n\n if field_name == \"operation\":\n # Handle SortableListInput format\n if isinstance(field_value, list):\n operation_name = field_value[0].get(\"name\", \"\") if field_value else \"\"\n else:\n operation_name = field_value or \"\"\n\n # If no operation selected, all dynamic fields stay hidden (already set to False above)\n if not operation_name:\n return build_config\n\n if operation_name == \"Filter\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"filter_value\"][\"show\"] = True\n build_config[\"filter_operator\"][\"show\"] = True\n elif operation_name == \"Sort\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"ascending\"][\"show\"] = True\n elif operation_name == \"Drop Column\":\n build_config[\"column_name\"][\"show\"] = True\n elif operation_name == \"Rename Column\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"new_column_name\"][\"show\"] = True\n elif operation_name == \"Add Column\":\n build_config[\"new_column_name\"][\"show\"] = True\n build_config[\"new_column_value\"][\"show\"] = True\n elif operation_name == \"Select Columns\":\n build_config[\"columns_to_select\"][\"show\"] = True\n elif operation_name in {\"Head\", \"Tail\"}:\n build_config[\"num_rows\"][\"show\"] = True\n elif operation_name == \"Replace Value\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"replace_value\"][\"show\"] = True\n build_config[\"replacement_value\"][\"show\"] = True\n elif operation_name == \"Drop Duplicates\":\n build_config[\"column_name\"][\"show\"] = True\n elif operation_name == \"Merge\":\n build_config[\"merge_on_column\"][\"show\"] = True\n build_config[\"merge_how\"][\"show\"] = True\n\n return build_config\n\n def _get_primary_dataframe(self) -> DataFrame:\n \"\"\"Get the first DataFrame from input (handles both single and list inputs).\"\"\"\n if isinstance(self.df, list):\n return self.df[0].copy() if self.df else DataFrame()\n return self.df.copy()\n\n def perform_operation(self) -> DataFrame:\n df_copy = self._get_primary_dataframe()\n\n # Handle SortableListInput format for operation (also supports legacy string format)\n operation_input = getattr(self, \"operation\", [])\n if isinstance(operation_input, list):\n op = operation_input[0].get(\"name\", \"\") if operation_input else \"\"\n else:\n op = operation_input or \"\"\n\n # If no operation selected, return original DataFrame\n if not op:\n return df_copy\n\n if op == \"Filter\":\n return self.filter_rows_by_value(df_copy)\n if op == \"Sort\":\n return self.sort_by_column(df_copy)\n if op == \"Drop Column\":\n return self.drop_column(df_copy)\n if op == \"Rename Column\":\n return self.rename_column(df_copy)\n if op == \"Add Column\":\n return self.add_column(df_copy)\n if op == \"Select Columns\":\n return self.select_columns(df_copy)\n if op == \"Head\":\n return self.head(df_copy)\n if op == \"Tail\":\n return self.tail(df_copy)\n if op == \"Replace Value\":\n return self.replace_values(df_copy)\n if op == \"Drop Duplicates\":\n return self.drop_duplicates(df_copy)\n if op == \"Concatenate\":\n return self.concatenate_dataframes()\n if op == \"Merge\":\n return self.merge_dataframes()\n msg = f\"Unsupported operation: {op}\"\n logger.error(msg)\n raise ValueError(msg)\n\n def filter_rows_by_value(self, df: DataFrame) -> DataFrame:\n column = df[self.column_name]\n filter_value = self.filter_value\n\n # Handle regular DropdownInput format (just a string value)\n operator = getattr(self, \"filter_operator\", \"equals\") # Default to equals for backward compatibility\n\n if operator == \"equals\":\n mask = column == filter_value\n elif operator == \"not equals\":\n mask = column != filter_value\n elif operator == \"contains\":\n mask = column.astype(str).str.contains(str(filter_value), na=False)\n elif operator == \"not contains\":\n mask = ~column.astype(str).str.contains(str(filter_value), na=False)\n elif operator == \"starts with\":\n mask = column.astype(str).str.startswith(str(filter_value), na=False)\n elif operator == \"ends with\":\n mask = column.astype(str).str.endswith(str(filter_value), na=False)\n elif operator == \"greater than\":\n try:\n # Try to convert filter_value to numeric for comparison\n numeric_value = pd.to_numeric(filter_value)\n mask = column > numeric_value\n except (ValueError, TypeError):\n # If conversion fails, compare as strings\n mask = column.astype(str) > str(filter_value)\n elif operator == \"less than\":\n try:\n # Try to convert filter_value to numeric for comparison\n numeric_value = pd.to_numeric(filter_value)\n mask = column < numeric_value\n except (ValueError, TypeError):\n # If conversion fails, compare as strings\n mask = column.astype(str) < str(filter_value)\n else:\n mask = column == filter_value # Fallback to equals\n\n return DataFrame(df[mask])\n\n def sort_by_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.sort_values(by=self.column_name, ascending=self.ascending))\n\n def drop_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.drop(columns=[self.column_name]))\n\n def rename_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.rename(columns={self.column_name: self.new_column_name}))\n\n def add_column(self, df: DataFrame) -> DataFrame:\n df[self.new_column_name] = [self.new_column_value] * len(df)\n return DataFrame(df)\n\n def select_columns(self, df: DataFrame) -> DataFrame:\n columns = [col.strip() for col in self.columns_to_select]\n return DataFrame(df[columns])\n\n def head(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.head(self.num_rows))\n\n def tail(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.tail(self.num_rows))\n\n def replace_values(self, df: DataFrame) -> DataFrame:\n df[self.column_name] = df[self.column_name].replace(self.replace_value, self.replacement_value)\n return DataFrame(df)\n\n def drop_duplicates(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.drop_duplicates(subset=self.column_name))\n\n def concatenate_dataframes(self) -> DataFrame:\n \"\"\"Concatenate multiple DataFrames vertically (stack rows).\"\"\"\n if not isinstance(self.df, list) or len(self.df) == 0:\n return self.df.copy() if self.df is not None else DataFrame()\n\n # If only one DataFrame, return it\n if len(self.df) == 1:\n return self.df[0].copy()\n\n # Concatenate all DataFrames vertically\n concatenated = pd.concat(self.df, ignore_index=True)\n return DataFrame(concatenated)\n\n def merge_dataframes(self) -> DataFrame:\n \"\"\"Merge two DataFrames based on a common column (join operation).\"\"\"\n if not isinstance(self.df, list) or len(self.df) == 0:\n return self.df.copy() if self.df is not None else DataFrame()\n\n # If only one DataFrame, return it\n if len(self.df) == 1:\n return self.df[0].copy()\n\n # Merge requires exactly two DataFrames\n max_merge_inputs = 2\n if len(self.df) > max_merge_inputs:\n msg = f\"Merge requires exactly {max_merge_inputs} DataFrames, got {len(self.df)}\"\n raise ValueError(msg)\n\n df1 = self.df[0].copy()\n df2 = self.df[1].copy()\n\n merge_on = getattr(self, \"merge_on_column\", None)\n merge_how = getattr(self, \"merge_how\", \"inner\")\n\n # If merge column specified, validate it exists in both DataFrames\n if merge_on:\n if merge_on not in df1.columns:\n msg = f\"Column '{merge_on}' not found in first DataFrame. Available: {list(df1.columns)}\"\n raise ValueError(msg)\n if merge_on not in df2.columns:\n msg = f\"Column '{merge_on}' not found in second DataFrame. Available: {list(df2.columns)}\"\n raise ValueError(msg)\n\n merged = df1.merge(df2, on=merge_on, how=merge_how, suffixes=(\"\", \"_df2\"))\n else:\n merged = df1.merge(df2, left_index=True, right_index=True, how=merge_how, suffixes=(\"\", \"_df2\"))\n\n # Combine duplicate columns: use df1 value if exists, otherwise df2 value\n cols_to_drop = []\n for col in merged.columns:\n if col.endswith(\"_df2\"):\n original_col = col[:-4] # Remove \"_df2\" suffix\n if original_col in merged.columns:\n # Coalesce: use original if not null, otherwise use _df2\n merged[original_col] = merged[original_col].combine_first(merged[col])\n cols_to_drop.append(col)\n\n if cols_to_drop:\n merged = merged.drop(columns=cols_to_drop)\n\n return DataFrame(merged)\n" }, "column_name": { "_input_type": "StrInput", @@ -100260,11 +100371,12 @@ "df": { "_input_type": "DataFrameInput", "advanced": false, - "display_name": "DataFrame", + "display_name": "Table", "dynamic": false, "info": "The input DataFrame to operate on. Connect multiple DataFrames for merge or concatenate operations.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": true, "list_add_label": "Add More", @@ -100581,13 +100693,13 @@ }, "DataOperations": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], "custom_fields": {}, - "description": "Perform various operations on a Data object.", - "display_name": "Data Operations", + "description": "Perform various operations on a JSON object.", + "display_name": "JSON Operations", "documentation": "", "edited": false, "field_order": [ @@ -100608,7 +100720,7 @@ "icon": "file-json", "legacy": false, "metadata": { - "code_hash": "1e5bfda1706b", + "code_hash": "957fe86b2c4f", "dependencies": { "dependencies": [ { @@ -100628,6 +100740,7 @@ }, "keywords": [ "data", + "json", "operations", "filter values", "Append or Update", @@ -100642,6 +100755,7 @@ "remove", "rename", "data operations", + "json operations", "data manipulation", "data transformation", "data filtering", @@ -100659,14 +100773,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "as_data", "name": "data_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -100712,16 +100826,17 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport json\nfrom typing import TYPE_CHECKING, Any\n\nimport jq\nfrom json_repair import repair_json\n\nfrom lfx.custom import Component\nfrom lfx.inputs import DictInput, DropdownInput, MessageTextInput, SortableListInput\nfrom lfx.io import DataInput, MultilineInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\n\nif TYPE_CHECKING:\n from collections.abc import Callable\n\nACTION_CONFIG = {\n \"Select Keys\": {\"is_list\": False, \"log_msg\": \"setting filter fields\"},\n \"Literal Eval\": {\"is_list\": False, \"log_msg\": \"setting evaluate fields\"},\n \"Combine\": {\"is_list\": True, \"log_msg\": \"setting combine fields\"},\n \"Filter Values\": {\"is_list\": False, \"log_msg\": \"setting filter values fields\"},\n \"Append or Update\": {\"is_list\": False, \"log_msg\": \"setting Append or Update fields\"},\n \"Remove Keys\": {\"is_list\": False, \"log_msg\": \"setting remove keys fields\"},\n \"Rename Keys\": {\"is_list\": False, \"log_msg\": \"setting rename keys fields\"},\n \"Path Selection\": {\"is_list\": False, \"log_msg\": \"setting mapped key extractor fields\"},\n \"JQ Expression\": {\"is_list\": False, \"log_msg\": \"setting parse json fields\"},\n}\nOPERATORS = {\n \"equals\": lambda a, b: str(a) == str(b),\n \"not equals\": lambda a, b: str(a) != str(b),\n \"contains\": lambda a, b: str(b) in str(a),\n \"starts with\": lambda a, b: str(a).startswith(str(b)),\n \"ends with\": lambda a, b: str(a).endswith(str(b)),\n}\n\n\nclass DataOperationsComponent(Component):\n display_name = \"Data Operations\"\n description = \"Perform various operations on a Data object.\"\n icon = \"file-json\"\n name = \"DataOperations\"\n default_keys = [\"operations\", \"data\"]\n metadata = {\n \"keywords\": [\n \"data\",\n \"operations\",\n \"filter values\",\n \"Append or Update\",\n \"remove keys\",\n \"rename keys\",\n \"select keys\",\n \"literal eval\",\n \"combine\",\n \"filter\",\n \"append\",\n \"update\",\n \"remove\",\n \"rename\",\n \"data operations\",\n \"data manipulation\",\n \"data transformation\",\n \"data filtering\",\n \"data selection\",\n \"data combination\",\n \"Parse JSON\",\n \"JSON Query\",\n \"JQ Query\",\n ],\n }\n actions_data = {\n \"Select Keys\": [\"select_keys_input\", \"operations\"],\n \"Literal Eval\": [],\n \"Combine\": [],\n \"Filter Values\": [\"filter_values\", \"operations\", \"operator\", \"filter_key\"],\n \"Append or Update\": [\"append_update_data\", \"operations\"],\n \"Remove Keys\": [\"remove_keys_input\", \"operations\"],\n \"Rename Keys\": [\"rename_keys_input\", \"operations\"],\n \"Path Selection\": [\"mapped_json_display\", \"selected_key\", \"operations\"],\n \"JQ Expression\": [\"query\", \"operations\"],\n }\n\n # All operation-specific input fields (used to hide and reset when no operation selected).\n ALL_OPERATION_FIELDS = [\n \"select_keys_input\",\n \"filter_key\",\n \"operator\",\n \"filter_values\",\n \"append_update_data\",\n \"remove_keys_input\",\n \"rename_keys_input\",\n \"mapped_json_display\",\n \"selected_key\",\n \"query\",\n ]\n\n @staticmethod\n def extract_all_paths(obj, path=\"\"):\n paths = []\n if isinstance(obj, dict):\n for k, v in obj.items():\n new_path = f\"{path}.{k}\" if path else f\".{k}\"\n paths.append(new_path)\n paths.extend(DataOperationsComponent.extract_all_paths(v, new_path))\n elif isinstance(obj, list) and obj:\n new_path = f\"{path}[0]\"\n paths.append(new_path)\n paths.extend(DataOperationsComponent.extract_all_paths(obj[0], new_path))\n return paths\n\n @staticmethod\n def remove_keys_recursive(obj, keys_to_remove):\n if isinstance(obj, dict):\n return {\n k: DataOperationsComponent.remove_keys_recursive(v, keys_to_remove)\n for k, v in obj.items()\n if k not in keys_to_remove\n }\n if isinstance(obj, list):\n return [DataOperationsComponent.remove_keys_recursive(item, keys_to_remove) for item in obj]\n return obj\n\n @staticmethod\n def rename_keys_recursive(obj, rename_map):\n if isinstance(obj, dict):\n return {\n rename_map.get(k, k): DataOperationsComponent.rename_keys_recursive(v, rename_map)\n for k, v in obj.items()\n }\n if isinstance(obj, list):\n return [DataOperationsComponent.rename_keys_recursive(item, rename_map) for item in obj]\n return obj\n\n inputs = [\n DataInput(name=\"data\", display_name=\"Data\", info=\"Data object to filter.\", required=True, is_list=True),\n SortableListInput(\n name=\"operations\",\n display_name=\"Operations\",\n placeholder=\"Select Operation\",\n info=\"List of operations to perform on the data.\",\n options=[\n {\"name\": \"Select Keys\", \"icon\": \"lasso-select\"},\n {\"name\": \"Literal Eval\", \"icon\": \"braces\"},\n {\"name\": \"Combine\", \"icon\": \"merge\"},\n {\"name\": \"Filter Values\", \"icon\": \"filter\"},\n {\"name\": \"Append or Update\", \"icon\": \"circle-plus\"},\n {\"name\": \"Remove Keys\", \"icon\": \"eraser\"},\n {\"name\": \"Rename Keys\", \"icon\": \"pencil-line\"},\n {\"name\": \"Path Selection\", \"icon\": \"mouse-pointer\"},\n {\"name\": \"JQ Expression\", \"icon\": \"terminal\"},\n ],\n real_time_refresh=True,\n limit=1,\n ),\n # select keys inputs\n MessageTextInput(\n name=\"select_keys_input\",\n display_name=\"Select Keys\",\n info=\"List of keys to select from the data. Only top-level keys can be selected.\",\n show=False,\n is_list=True,\n value=[],\n ),\n # filter values inputs\n MessageTextInput(\n name=\"filter_key\",\n display_name=\"Filter Key\",\n info=(\n \"Name of the key containing the list to filter. \"\n \"It must be a top-level key in the JSON and its value must be a list.\"\n ),\n is_list=True,\n show=False,\n value=[],\n ),\n DropdownInput(\n name=\"operator\",\n display_name=\"Comparison Operator\",\n options=[\"equals\", \"not equals\", \"contains\", \"starts with\", \"ends with\"],\n info=\"The operator to apply for comparing the values.\",\n value=\"equals\",\n advanced=False,\n show=False,\n ),\n DictInput(\n name=\"filter_values\",\n display_name=\"Filter Values\",\n info=\"List of values to filter by.\",\n show=False,\n is_list=True,\n value={},\n ),\n # update/ Append data inputs\n DictInput(\n name=\"append_update_data\",\n display_name=\"Append or Update\",\n info=\"Data to append or update the existing data with. Only top-level keys are checked.\",\n show=False,\n value={\"key\": \"value\"},\n is_list=True,\n ),\n # remove keys inputs\n MessageTextInput(\n name=\"remove_keys_input\",\n display_name=\"Remove Keys\",\n info=\"List of keys to remove from the data.\",\n show=False,\n is_list=True,\n value=[],\n ),\n # rename keys inputs\n DictInput(\n name=\"rename_keys_input\",\n display_name=\"Rename Keys\",\n info=\"List of keys to rename in the data.\",\n show=False,\n is_list=True,\n value={\"old_key\": \"new_key\"},\n ),\n MultilineInput(\n name=\"mapped_json_display\",\n display_name=\"JSON to Map\",\n info=\"Paste or preview your JSON here to explore its structure and select a path for extraction.\",\n required=False,\n refresh_button=True,\n real_time_refresh=True,\n placeholder=\"Add a JSON example.\",\n show=False,\n ),\n DropdownInput(\n name=\"selected_key\",\n display_name=\"Select Path\",\n options=[],\n required=False,\n dynamic=True,\n show=False,\n value=None,\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"JQ Expression\",\n info=\"JSON Query to filter the data. Used by Parse JSON operation.\",\n placeholder=\"e.g., .properties.id\",\n show=False,\n ),\n ]\n\n # Default values for operation fields when clearing (match input definitions)\n OPERATION_FIELD_DEFAULTS: dict[str, Any] = {\n \"select_keys_input\": [],\n \"filter_key\": [],\n \"operator\": \"equals\",\n \"filter_values\": {},\n \"append_update_data\": {\"key\": \"value\"},\n \"remove_keys_input\": [],\n \"rename_keys_input\": {\"old_key\": \"new_key\"},\n \"mapped_json_display\": \"\",\n \"selected_key\": None,\n \"query\": \"\",\n }\n\n outputs = [\n Output(display_name=\"Data\", name=\"data_output\", method=\"as_data\"),\n ]\n\n # Helper methods for data operations\n def get_data_dict(self) -> dict:\n \"\"\"Extract data dictionary from Data object.\"\"\"\n data = self.data[0] if isinstance(self.data, list) and len(self.data) == 1 else self.data\n return data.model_dump()\n\n def json_query(self) -> Data:\n import json\n\n import jq\n\n if not self.query or not self.query.strip():\n msg = \"JSON Query is required and cannot be blank.\"\n raise ValueError(msg)\n raw_data = self.get_data_dict()\n try:\n input_str = json.dumps(raw_data)\n repaired = repair_json(input_str)\n data_json = json.loads(repaired)\n jq_input = data_json[\"data\"] if isinstance(data_json, dict) and \"data\" in data_json else data_json\n results = jq.compile(self.query).input(jq_input).all()\n if not results:\n msg = \"No result from JSON query.\"\n raise ValueError(msg)\n result = results[0] if len(results) == 1 else results\n if result is None or result == \"None\":\n msg = \"JSON query returned null/None. Check if the path exists in your data.\"\n raise ValueError(msg)\n if isinstance(result, dict):\n return Data(data=result)\n return Data(data={\"result\": result})\n except (ValueError, TypeError, KeyError, json.JSONDecodeError) as e:\n logger.error(f\"JSON Query failed: {e}\")\n msg = f\"JSON Query error: {e}\"\n raise ValueError(msg) from e\n\n def get_normalized_data(self) -> dict:\n \"\"\"Get normalized data dictionary, handling the 'data' key if present.\"\"\"\n data_dict = self.get_data_dict()\n return data_dict.get(\"data\", data_dict)\n\n def data_is_list(self) -> bool:\n \"\"\"Check if data contains multiple items.\"\"\"\n return isinstance(self.data, list) and len(self.data) > 1\n\n def validate_single_data(self, operation: str) -> None:\n \"\"\"Validate that the operation is being performed on a single data object.\"\"\"\n if self.data_is_list():\n msg = f\"{operation} operation is not supported for multiple data objects.\"\n raise ValueError(msg)\n\n def operation_exception(self, operations: list[str]) -> None:\n \"\"\"Raise exception for incompatible operations.\"\"\"\n msg = f\"{operations} operations are not supported in combination with each other.\"\n raise ValueError(msg)\n\n # Data transformation operations\n def select_keys(self, *, evaluate: bool | None = None) -> Data:\n \"\"\"Select specific keys from the data dictionary.\"\"\"\n self.validate_single_data(\"Select Keys\")\n data_dict = self.get_normalized_data()\n filter_criteria: list[str] = self.select_keys_input\n\n # Filter the data\n if len(filter_criteria) == 1 and filter_criteria[0] == \"data\":\n filtered = data_dict[\"data\"]\n else:\n if not all(key in data_dict for key in filter_criteria):\n msg = f\"Select key not found in data. Available keys: {list(data_dict.keys())}\"\n raise ValueError(msg)\n filtered = {key: value for key, value in data_dict.items() if key in filter_criteria}\n\n # Create a new Data object with the filtered data\n if evaluate:\n filtered = self.recursive_eval(filtered)\n\n # Return a new Data object with the filtered data directly in the data attribute\n return Data(data=filtered)\n\n def remove_keys(self) -> Data:\n \"\"\"Remove specified keys from the data dictionary, recursively.\"\"\"\n self.validate_single_data(\"Remove Keys\")\n data_dict = self.get_normalized_data()\n remove_keys_input: list[str] = self.remove_keys_input\n\n filtered = DataOperationsComponent.remove_keys_recursive(data_dict, set(remove_keys_input))\n return Data(data=filtered)\n\n def rename_keys(self) -> Data:\n \"\"\"Rename keys in the data dictionary, recursively.\"\"\"\n self.validate_single_data(\"Rename Keys\")\n data_dict = self.get_normalized_data()\n rename_keys_input: dict[str, str] = self.rename_keys_input\n\n renamed = DataOperationsComponent.rename_keys_recursive(data_dict, rename_keys_input)\n return Data(data=renamed)\n\n def recursive_eval(self, data: Any) -> Any:\n \"\"\"Recursively evaluate string values in a dictionary or list.\n\n If the value is a string that can be evaluated, it will be evaluated.\n Otherwise, the original value is returned.\n \"\"\"\n if isinstance(data, dict):\n return {k: self.recursive_eval(v) for k, v in data.items()}\n if isinstance(data, list):\n return [self.recursive_eval(item) for item in data]\n if isinstance(data, str):\n try:\n # Only attempt to evaluate strings that look like Python literals\n if (\n data.strip().startswith((\"{\", \"[\", \"(\", \"'\", '\"'))\n or data.strip().lower() in (\"true\", \"false\", \"none\")\n or data.strip().replace(\".\", \"\").isdigit()\n ):\n return ast.literal_eval(data)\n # return data\n except (ValueError, SyntaxError, TypeError, MemoryError):\n # If evaluation fails for any reason, return the original string\n return data\n else:\n return data\n return data\n\n def evaluate_data(self) -> Data:\n \"\"\"Evaluate string values in the data dictionary.\"\"\"\n self.validate_single_data(\"Literal Eval\")\n logger.info(\"evaluating data\")\n return Data(**self.recursive_eval(self.get_data_dict()))\n\n def combine_data(self, *, evaluate: bool | None = None) -> Data:\n \"\"\"Combine multiple data objects into one.\"\"\"\n logger.info(\"combining data\")\n if not self.data_is_list():\n return self.data[0] if self.data else Data(data={})\n\n if len(self.data) == 1:\n msg = \"Combine operation requires multiple data inputs.\"\n raise ValueError(msg)\n\n data_dicts = [data.model_dump().get(\"data\", data.model_dump()) for data in self.data]\n combined_data = {}\n\n for data_dict in data_dicts:\n for key, value in data_dict.items():\n if key not in combined_data:\n combined_data[key] = value\n elif isinstance(combined_data[key], list):\n if isinstance(value, list):\n combined_data[key].extend(value)\n else:\n combined_data[key].append(value)\n else:\n # If current value is not a list, convert it to list and add new value\n combined_data[key] = (\n [combined_data[key], value] if not isinstance(value, list) else [combined_data[key], *value]\n )\n\n if evaluate:\n combined_data = self.recursive_eval(combined_data)\n\n return Data(**combined_data)\n\n def filter_data(self, input_data: list[dict[str, Any]], filter_key: str, filter_value: str, operator: str) -> list:\n \"\"\"Filter list data based on key, value, and operator.\"\"\"\n # Validate inputs\n if not input_data:\n self.status = \"Input data is empty.\"\n return []\n\n if not filter_key or not filter_value:\n self.status = \"Filter key or value is missing.\"\n return input_data\n\n # Filter the data\n filtered_data = []\n for item in input_data:\n if isinstance(item, dict) and filter_key in item:\n if self.compare_values(item[filter_key], filter_value, operator):\n filtered_data.append(item)\n else:\n self.status = f\"Warning: Some items don't have the key '{filter_key}' or are not dictionaries.\"\n\n return filtered_data\n\n def compare_values(self, item_value: Any, filter_value: str, operator: str) -> bool:\n comparison_func = OPERATORS.get(operator)\n if comparison_func:\n return comparison_func(item_value, filter_value)\n return False\n\n def multi_filter_data(self) -> Data:\n \"\"\"Apply multiple filters to the data.\"\"\"\n self.validate_single_data(\"Filter Values\")\n data_filtered = self.get_normalized_data()\n\n for filter_key in self.filter_key:\n if filter_key not in data_filtered:\n msg = f\"Filter key '{filter_key}' not found in data. Available keys: {list(data_filtered.keys())}\"\n raise ValueError(msg)\n\n if isinstance(data_filtered[filter_key], list):\n for filter_data in self.filter_values:\n filter_value = self.filter_values.get(filter_data)\n if filter_value is not None:\n data_filtered[filter_key] = self.filter_data(\n input_data=data_filtered[filter_key],\n filter_key=filter_data,\n filter_value=filter_value,\n operator=self.operator,\n )\n else:\n msg = f\"Filter key '{filter_key}' is not a list.\"\n raise TypeError(msg)\n\n return Data(**data_filtered)\n\n def append_update(self) -> Data:\n \"\"\"Append or Update with new key-value pairs.\"\"\"\n self.validate_single_data(\"Append or Update\")\n data_filtered = self.get_normalized_data()\n\n for key, value in self.append_update_data.items():\n data_filtered[key] = value\n\n return Data(**data_filtered)\n\n # Configuration and execution methods\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name == \"operations\":\n build_config[\"operations\"][\"value\"] = field_value\n # Mirror Text Operations: first hide all operation-specific fields and clear their values\n for field in self.ALL_OPERATION_FIELDS:\n if field in build_config:\n build_config[field][\"show\"] = False\n if field in self.OPERATION_FIELD_DEFAULTS:\n build_config[field][\"value\"] = self.OPERATION_FIELD_DEFAULTS[field]\n\n selected_actions = [\n action[\"name\"] for action in (field_value or []) if isinstance(action, dict) and \"name\" in action\n ]\n if len(selected_actions) == 1 and selected_actions[0] in ACTION_CONFIG:\n action = selected_actions[0]\n config = ACTION_CONFIG[action]\n build_config[\"data\"][\"is_list\"] = config[\"is_list\"]\n logger.info(config[\"log_msg\"])\n return set_current_fields(\n build_config=build_config,\n action_fields=self.actions_data,\n selected_action=action,\n default_fields=[\"operations\", \"data\"],\n func=set_field_display,\n )\n return build_config\n\n if field_name == \"mapped_json_display\":\n try:\n parsed_json = json.loads(field_value)\n keys = DataOperationsComponent.extract_all_paths(parsed_json)\n build_config[\"selected_key\"][\"options\"] = keys\n build_config[\"selected_key\"][\"show\"] = True\n except (json.JSONDecodeError, TypeError, ValueError) as e:\n logger.error(f\"Error parsing mapped JSON: {e}\")\n build_config[\"selected_key\"][\"show\"] = False\n\n return build_config\n\n def json_path(self) -> Data:\n try:\n if not self.data or not self.selected_key:\n msg = \"Missing input data or selected key.\"\n raise ValueError(msg)\n input_payload = self.data[0].data if isinstance(self.data, list) else self.data.data\n compiled = jq.compile(self.selected_key)\n result = compiled.input(input_payload).first()\n if isinstance(result, dict):\n return Data(data=result)\n return Data(data={\"result\": result})\n except (ValueError, TypeError, KeyError) as e:\n self.status = f\"Error: {e!s}\"\n self.log(self.status)\n return Data(data={\"error\": str(e)})\n\n def as_data(self) -> Data:\n if not hasattr(self, \"operations\") or not self.operations:\n return Data(data={})\n\n selected_actions = [action[\"name\"] for action in self.operations]\n logger.info(f\"selected_actions: {selected_actions}\")\n if len(selected_actions) != 1:\n return Data(data={})\n\n action_map: dict[str, Callable[[], Data]] = {\n \"Select Keys\": self.select_keys,\n \"Literal Eval\": self.evaluate_data,\n \"Combine\": self.combine_data,\n \"Filter Values\": self.multi_filter_data,\n \"Append or Update\": self.append_update,\n \"Remove Keys\": self.remove_keys,\n \"Rename Keys\": self.rename_keys,\n \"Path Selection\": self.json_path,\n \"JQ Expression\": self.json_query,\n }\n handler: Callable[[], Data] | None = action_map.get(selected_actions[0])\n if handler:\n try:\n return handler()\n except Exception as e:\n logger.error(f\"Error executing {selected_actions[0]}: {e!s}\")\n raise\n return Data(data={})\n" + "value": "import ast\nimport json\nfrom typing import TYPE_CHECKING, Any\n\nimport jq\nfrom json_repair import repair_json\n\nfrom lfx.custom import Component\nfrom lfx.inputs import DictInput, DropdownInput, MessageTextInput, SortableListInput\nfrom lfx.io import DataInput, MultilineInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.utils.component_utils import set_current_fields, set_field_display\n\nif TYPE_CHECKING:\n from collections.abc import Callable\n\nACTION_CONFIG = {\n \"Select Keys\": {\"is_list\": False, \"log_msg\": \"setting filter fields\"},\n \"Literal Eval\": {\"is_list\": False, \"log_msg\": \"setting evaluate fields\"},\n \"Combine\": {\"is_list\": True, \"log_msg\": \"setting combine fields\"},\n \"Filter Values\": {\"is_list\": False, \"log_msg\": \"setting filter values fields\"},\n \"Append or Update\": {\"is_list\": False, \"log_msg\": \"setting Append or Update fields\"},\n \"Remove Keys\": {\"is_list\": False, \"log_msg\": \"setting remove keys fields\"},\n \"Rename Keys\": {\"is_list\": False, \"log_msg\": \"setting rename keys fields\"},\n \"Path Selection\": {\"is_list\": False, \"log_msg\": \"setting mapped key extractor fields\"},\n \"JQ Expression\": {\"is_list\": False, \"log_msg\": \"setting parse json fields\"},\n}\nOPERATORS = {\n \"equals\": lambda a, b: str(a) == str(b),\n \"not equals\": lambda a, b: str(a) != str(b),\n \"contains\": lambda a, b: str(b) in str(a),\n \"starts with\": lambda a, b: str(a).startswith(str(b)),\n \"ends with\": lambda a, b: str(a).endswith(str(b)),\n}\n\n\nclass DataOperationsComponent(Component):\n display_name = \"JSON Operations\"\n description = \"Perform various operations on a JSON object.\"\n icon = \"file-json\"\n name = \"DataOperations\"\n default_keys = [\"operations\", \"data\"]\n metadata = {\n \"keywords\": [\n \"data\",\n \"json\",\n \"operations\",\n \"filter values\",\n \"Append or Update\",\n \"remove keys\",\n \"rename keys\",\n \"select keys\",\n \"literal eval\",\n \"combine\",\n \"filter\",\n \"append\",\n \"update\",\n \"remove\",\n \"rename\",\n \"data operations\",\n \"json operations\",\n \"data manipulation\",\n \"data transformation\",\n \"data filtering\",\n \"data selection\",\n \"data combination\",\n \"Parse JSON\",\n \"JSON Query\",\n \"JQ Query\",\n ],\n }\n actions_data = {\n \"Select Keys\": [\"select_keys_input\", \"operations\"],\n \"Literal Eval\": [],\n \"Combine\": [],\n \"Filter Values\": [\"filter_values\", \"operations\", \"operator\", \"filter_key\"],\n \"Append or Update\": [\"append_update_data\", \"operations\"],\n \"Remove Keys\": [\"remove_keys_input\", \"operations\"],\n \"Rename Keys\": [\"rename_keys_input\", \"operations\"],\n \"Path Selection\": [\"mapped_json_display\", \"selected_key\", \"operations\"],\n \"JQ Expression\": [\"query\", \"operations\"],\n }\n\n # All operation-specific input fields (used to hide and reset when no operation selected).\n ALL_OPERATION_FIELDS = [\n \"select_keys_input\",\n \"filter_key\",\n \"operator\",\n \"filter_values\",\n \"append_update_data\",\n \"remove_keys_input\",\n \"rename_keys_input\",\n \"mapped_json_display\",\n \"selected_key\",\n \"query\",\n ]\n\n @staticmethod\n def extract_all_paths(obj, path=\"\"):\n paths = []\n if isinstance(obj, dict):\n for k, v in obj.items():\n new_path = f\"{path}.{k}\" if path else f\".{k}\"\n paths.append(new_path)\n paths.extend(DataOperationsComponent.extract_all_paths(v, new_path))\n elif isinstance(obj, list) and obj:\n new_path = f\"{path}[0]\"\n paths.append(new_path)\n paths.extend(DataOperationsComponent.extract_all_paths(obj[0], new_path))\n return paths\n\n @staticmethod\n def remove_keys_recursive(obj, keys_to_remove):\n if isinstance(obj, dict):\n return {\n k: DataOperationsComponent.remove_keys_recursive(v, keys_to_remove)\n for k, v in obj.items()\n if k not in keys_to_remove\n }\n if isinstance(obj, list):\n return [DataOperationsComponent.remove_keys_recursive(item, keys_to_remove) for item in obj]\n return obj\n\n @staticmethod\n def rename_keys_recursive(obj, rename_map):\n if isinstance(obj, dict):\n return {\n rename_map.get(k, k): DataOperationsComponent.rename_keys_recursive(v, rename_map)\n for k, v in obj.items()\n }\n if isinstance(obj, list):\n return [DataOperationsComponent.rename_keys_recursive(item, rename_map) for item in obj]\n return obj\n\n inputs = [\n DataInput(name=\"data\", display_name=\"JSON\", info=\"Data object to filter.\", required=True, is_list=True),\n SortableListInput(\n name=\"operations\",\n display_name=\"Operations\",\n placeholder=\"Select Operation\",\n info=\"List of operations to perform on the data.\",\n options=[\n {\"name\": \"Select Keys\", \"icon\": \"lasso-select\"},\n {\"name\": \"Literal Eval\", \"icon\": \"braces\"},\n {\"name\": \"Combine\", \"icon\": \"merge\"},\n {\"name\": \"Filter Values\", \"icon\": \"filter\"},\n {\"name\": \"Append or Update\", \"icon\": \"circle-plus\"},\n {\"name\": \"Remove Keys\", \"icon\": \"eraser\"},\n {\"name\": \"Rename Keys\", \"icon\": \"pencil-line\"},\n {\"name\": \"Path Selection\", \"icon\": \"mouse-pointer\"},\n {\"name\": \"JQ Expression\", \"icon\": \"terminal\"},\n ],\n real_time_refresh=True,\n limit=1,\n ),\n # select keys inputs\n MessageTextInput(\n name=\"select_keys_input\",\n display_name=\"Select Keys\",\n info=\"List of keys to select from the data. Only top-level keys can be selected.\",\n show=False,\n is_list=True,\n value=[],\n ),\n # filter values inputs\n MessageTextInput(\n name=\"filter_key\",\n display_name=\"Filter Key\",\n info=(\n \"Name of the key containing the list to filter. \"\n \"It must be a top-level key in the JSON and its value must be a list.\"\n ),\n is_list=True,\n show=False,\n value=[],\n ),\n DropdownInput(\n name=\"operator\",\n display_name=\"Comparison Operator\",\n options=[\"equals\", \"not equals\", \"contains\", \"starts with\", \"ends with\"],\n info=\"The operator to apply for comparing the values.\",\n value=\"equals\",\n advanced=False,\n show=False,\n ),\n DictInput(\n name=\"filter_values\",\n display_name=\"Filter Values\",\n info=\"List of values to filter by.\",\n show=False,\n is_list=True,\n value={},\n ),\n # update/ Append data inputs\n DictInput(\n name=\"append_update_data\",\n display_name=\"Append or Update\",\n info=\"Data to append or update the existing data with. Only top-level keys are checked.\",\n show=False,\n value={\"key\": \"value\"},\n is_list=True,\n ),\n # remove keys inputs\n MessageTextInput(\n name=\"remove_keys_input\",\n display_name=\"Remove Keys\",\n info=\"List of keys to remove from the data.\",\n show=False,\n is_list=True,\n value=[],\n ),\n # rename keys inputs\n DictInput(\n name=\"rename_keys_input\",\n display_name=\"Rename Keys\",\n info=\"List of keys to rename in the data.\",\n show=False,\n is_list=True,\n value={\"old_key\": \"new_key\"},\n ),\n MultilineInput(\n name=\"mapped_json_display\",\n display_name=\"JSON to Map\",\n info=\"Paste or preview your JSON here to explore its structure and select a path for extraction.\",\n required=False,\n refresh_button=True,\n real_time_refresh=True,\n placeholder=\"Add a JSON example.\",\n show=False,\n ),\n DropdownInput(\n name=\"selected_key\",\n display_name=\"Select Path\",\n options=[],\n required=False,\n dynamic=True,\n show=False,\n value=None,\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"JQ Expression\",\n info=\"JSON Query to filter the data. Used by Parse JSON operation.\",\n placeholder=\"e.g., .properties.id\",\n show=False,\n ),\n ]\n\n # Default values for operation fields when clearing (match input definitions)\n OPERATION_FIELD_DEFAULTS: dict[str, Any] = {\n \"select_keys_input\": [],\n \"filter_key\": [],\n \"operator\": \"equals\",\n \"filter_values\": {},\n \"append_update_data\": {\"key\": \"value\"},\n \"remove_keys_input\": [],\n \"rename_keys_input\": {\"old_key\": \"new_key\"},\n \"mapped_json_display\": \"\",\n \"selected_key\": None,\n \"query\": \"\",\n }\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data_output\", method=\"as_data\"),\n ]\n\n # Helper methods for data operations\n def get_data_dict(self) -> dict:\n \"\"\"Extract data dictionary from Data object.\"\"\"\n data = self.data[0] if isinstance(self.data, list) and len(self.data) == 1 else self.data\n return data.model_dump()\n\n def json_query(self) -> Data:\n import json\n\n import jq\n\n if not self.query or not self.query.strip():\n msg = \"JSON Query is required and cannot be blank.\"\n raise ValueError(msg)\n raw_data = self.get_data_dict()\n try:\n input_str = json.dumps(raw_data)\n repaired = repair_json(input_str)\n data_json = json.loads(repaired)\n jq_input = data_json[\"data\"] if isinstance(data_json, dict) and \"data\" in data_json else data_json\n results = jq.compile(self.query).input(jq_input).all()\n if not results:\n msg = \"No result from JSON query.\"\n raise ValueError(msg)\n result = results[0] if len(results) == 1 else results\n if result is None or result == \"None\":\n msg = \"JSON query returned null/None. Check if the path exists in your data.\"\n raise ValueError(msg)\n if isinstance(result, dict):\n return Data(data=result)\n return Data(data={\"result\": result})\n except (ValueError, TypeError, KeyError, json.JSONDecodeError) as e:\n logger.error(f\"JSON Query failed: {e}\")\n msg = f\"JSON Query error: {e}\"\n raise ValueError(msg) from e\n\n def get_normalized_data(self) -> dict:\n \"\"\"Get normalized data dictionary, handling the 'data' key if present.\"\"\"\n data_dict = self.get_data_dict()\n return data_dict.get(\"data\", data_dict)\n\n def data_is_list(self) -> bool:\n \"\"\"Check if data contains multiple items.\"\"\"\n return isinstance(self.data, list) and len(self.data) > 1\n\n def validate_single_data(self, operation: str) -> None:\n \"\"\"Validate that the operation is being performed on a single data object.\"\"\"\n if self.data_is_list():\n msg = f\"{operation} operation is not supported for multiple data objects.\"\n raise ValueError(msg)\n\n def operation_exception(self, operations: list[str]) -> None:\n \"\"\"Raise exception for incompatible operations.\"\"\"\n msg = f\"{operations} operations are not supported in combination with each other.\"\n raise ValueError(msg)\n\n # Data transformation operations\n def select_keys(self, *, evaluate: bool | None = None) -> Data:\n \"\"\"Select specific keys from the data dictionary.\"\"\"\n self.validate_single_data(\"Select Keys\")\n data_dict = self.get_normalized_data()\n filter_criteria: list[str] = self.select_keys_input\n\n # Filter the data\n if len(filter_criteria) == 1 and filter_criteria[0] == \"data\":\n filtered = data_dict[\"data\"]\n else:\n if not all(key in data_dict for key in filter_criteria):\n msg = f\"Select key not found in data. Available keys: {list(data_dict.keys())}\"\n raise ValueError(msg)\n filtered = {key: value for key, value in data_dict.items() if key in filter_criteria}\n\n # Create a new Data object with the filtered data\n if evaluate:\n filtered = self.recursive_eval(filtered)\n\n # Return a new Data object with the filtered data directly in the data attribute\n return Data(data=filtered)\n\n def remove_keys(self) -> Data:\n \"\"\"Remove specified keys from the data dictionary, recursively.\"\"\"\n self.validate_single_data(\"Remove Keys\")\n data_dict = self.get_normalized_data()\n remove_keys_input: list[str] = self.remove_keys_input\n\n filtered = DataOperationsComponent.remove_keys_recursive(data_dict, set(remove_keys_input))\n return Data(data=filtered)\n\n def rename_keys(self) -> Data:\n \"\"\"Rename keys in the data dictionary, recursively.\"\"\"\n self.validate_single_data(\"Rename Keys\")\n data_dict = self.get_normalized_data()\n rename_keys_input: dict[str, str] = self.rename_keys_input\n\n renamed = DataOperationsComponent.rename_keys_recursive(data_dict, rename_keys_input)\n return Data(data=renamed)\n\n def recursive_eval(self, data: Any) -> Any:\n \"\"\"Recursively evaluate string values in a dictionary or list.\n\n If the value is a string that can be evaluated, it will be evaluated.\n Otherwise, the original value is returned.\n \"\"\"\n if isinstance(data, dict):\n return {k: self.recursive_eval(v) for k, v in data.items()}\n if isinstance(data, list):\n return [self.recursive_eval(item) for item in data]\n if isinstance(data, str):\n try:\n # Only attempt to evaluate strings that look like Python literals\n if (\n data.strip().startswith((\"{\", \"[\", \"(\", \"'\", '\"'))\n or data.strip().lower() in (\"true\", \"false\", \"none\")\n or data.strip().replace(\".\", \"\").isdigit()\n ):\n return ast.literal_eval(data)\n # return data\n except (ValueError, SyntaxError, TypeError, MemoryError):\n # If evaluation fails for any reason, return the original string\n return data\n else:\n return data\n return data\n\n def evaluate_data(self) -> Data:\n \"\"\"Evaluate string values in the data dictionary.\"\"\"\n self.validate_single_data(\"Literal Eval\")\n logger.info(\"evaluating data\")\n return Data(**self.recursive_eval(self.get_data_dict()))\n\n def combine_data(self, *, evaluate: bool | None = None) -> Data:\n \"\"\"Combine multiple data objects into one.\"\"\"\n logger.info(\"combining data\")\n if not self.data_is_list():\n return self.data[0] if self.data else Data(data={})\n\n if len(self.data) == 1:\n msg = \"Combine operation requires multiple data inputs.\"\n raise ValueError(msg)\n\n data_dicts = [data.model_dump().get(\"data\", data.model_dump()) for data in self.data]\n combined_data = {}\n\n for data_dict in data_dicts:\n for key, value in data_dict.items():\n if key not in combined_data:\n combined_data[key] = value\n elif isinstance(combined_data[key], list):\n if isinstance(value, list):\n combined_data[key].extend(value)\n else:\n combined_data[key].append(value)\n else:\n # If current value is not a list, convert it to list and add new value\n combined_data[key] = (\n [combined_data[key], value] if not isinstance(value, list) else [combined_data[key], *value]\n )\n\n if evaluate:\n combined_data = self.recursive_eval(combined_data)\n\n return Data(**combined_data)\n\n def filter_data(self, input_data: list[dict[str, Any]], filter_key: str, filter_value: str, operator: str) -> list:\n \"\"\"Filter list data based on key, value, and operator.\"\"\"\n # Validate inputs\n if not input_data:\n self.status = \"Input data is empty.\"\n return []\n\n if not filter_key or not filter_value:\n self.status = \"Filter key or value is missing.\"\n return input_data\n\n # Filter the data\n filtered_data = []\n for item in input_data:\n if isinstance(item, dict) and filter_key in item:\n if self.compare_values(item[filter_key], filter_value, operator):\n filtered_data.append(item)\n else:\n self.status = f\"Warning: Some items don't have the key '{filter_key}' or are not dictionaries.\"\n\n return filtered_data\n\n def compare_values(self, item_value: Any, filter_value: str, operator: str) -> bool:\n comparison_func = OPERATORS.get(operator)\n if comparison_func:\n return comparison_func(item_value, filter_value)\n return False\n\n def multi_filter_data(self) -> Data:\n \"\"\"Apply multiple filters to the data.\"\"\"\n self.validate_single_data(\"Filter Values\")\n data_filtered = self.get_normalized_data()\n\n for filter_key in self.filter_key:\n if filter_key not in data_filtered:\n msg = f\"Filter key '{filter_key}' not found in data. Available keys: {list(data_filtered.keys())}\"\n raise ValueError(msg)\n\n if isinstance(data_filtered[filter_key], list):\n for filter_data in self.filter_values:\n filter_value = self.filter_values.get(filter_data)\n if filter_value is not None:\n data_filtered[filter_key] = self.filter_data(\n input_data=data_filtered[filter_key],\n filter_key=filter_data,\n filter_value=filter_value,\n operator=self.operator,\n )\n else:\n msg = f\"Filter key '{filter_key}' is not a list.\"\n raise TypeError(msg)\n\n return Data(**data_filtered)\n\n def append_update(self) -> Data:\n \"\"\"Append or Update with new key-value pairs.\"\"\"\n self.validate_single_data(\"Append or Update\")\n data_filtered = self.get_normalized_data()\n\n for key, value in self.append_update_data.items():\n data_filtered[key] = value\n\n return Data(**data_filtered)\n\n # Configuration and execution methods\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name == \"operations\":\n build_config[\"operations\"][\"value\"] = field_value\n # Mirror Text Operations: first hide all operation-specific fields and clear their values\n for field in self.ALL_OPERATION_FIELDS:\n if field in build_config:\n build_config[field][\"show\"] = False\n if field in self.OPERATION_FIELD_DEFAULTS:\n build_config[field][\"value\"] = self.OPERATION_FIELD_DEFAULTS[field]\n\n selected_actions = [\n action[\"name\"] for action in (field_value or []) if isinstance(action, dict) and \"name\" in action\n ]\n if len(selected_actions) == 1 and selected_actions[0] in ACTION_CONFIG:\n action = selected_actions[0]\n config = ACTION_CONFIG[action]\n build_config[\"data\"][\"is_list\"] = config[\"is_list\"]\n logger.info(config[\"log_msg\"])\n return set_current_fields(\n build_config=build_config,\n action_fields=self.actions_data,\n selected_action=action,\n default_fields=[\"operations\", \"data\"],\n func=set_field_display,\n )\n return build_config\n\n if field_name == \"mapped_json_display\":\n try:\n parsed_json = json.loads(field_value)\n keys = DataOperationsComponent.extract_all_paths(parsed_json)\n build_config[\"selected_key\"][\"options\"] = keys\n build_config[\"selected_key\"][\"show\"] = True\n except (json.JSONDecodeError, TypeError, ValueError) as e:\n logger.error(f\"Error parsing mapped JSON: {e}\")\n build_config[\"selected_key\"][\"show\"] = False\n\n return build_config\n\n def json_path(self) -> Data:\n try:\n if not self.data or not self.selected_key:\n msg = \"Missing input data or selected key.\"\n raise ValueError(msg)\n input_payload = self.data[0].data if isinstance(self.data, list) else self.data.data\n compiled = jq.compile(self.selected_key)\n result = compiled.input(input_payload).first()\n if isinstance(result, dict):\n return Data(data=result)\n return Data(data={\"result\": result})\n except (ValueError, TypeError, KeyError) as e:\n self.status = f\"Error: {e!s}\"\n self.log(self.status)\n return Data(data={\"error\": str(e)})\n\n def as_data(self) -> Data:\n if not hasattr(self, \"operations\") or not self.operations:\n return Data(data={})\n\n selected_actions = [action[\"name\"] for action in self.operations]\n logger.info(f\"selected_actions: {selected_actions}\")\n if len(selected_actions) != 1:\n return Data(data={})\n\n action_map: dict[str, Callable[[], Data]] = {\n \"Select Keys\": self.select_keys,\n \"Literal Eval\": self.evaluate_data,\n \"Combine\": self.combine_data,\n \"Filter Values\": self.multi_filter_data,\n \"Append or Update\": self.append_update,\n \"Remove Keys\": self.remove_keys,\n \"Rename Keys\": self.rename_keys,\n \"Path Selection\": self.json_path,\n \"JQ Expression\": self.json_query,\n }\n handler: Callable[[], Data] | None = action_map.get(selected_actions[0])\n if handler:\n try:\n return handler()\n except Exception as e:\n logger.error(f\"Error executing {selected_actions[0]}: {e!s}\")\n raise\n return Data(data={})\n" }, "data": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, - "display_name": "Data", + "display_name": "JSON", "dynamic": false, "info": "Data object to filter.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -101028,7 +101143,7 @@ }, "DataToDataFrame": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -101044,7 +101159,7 @@ "icon": "table", "legacy": true, "metadata": { - "code_hash": "57b9f79028e9", + "code_hash": "edcdf6feefd2", "dependencies": { "dependencies": [ { @@ -101062,14 +101177,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "build_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -101097,16 +101212,17 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass DataToDataFrameComponent(Component):\n display_name = \"Data → DataFrame\"\n description = (\n \"Converts one or multiple Data objects into a DataFrame. \"\n \"Each Data object corresponds to one row. Fields from `.data` become columns, \"\n \"and the `.text` (if present) is placed in a 'text' column.\"\n )\n icon = \"table\"\n name = \"DataToDataFrame\"\n legacy = True\n replacement = [\"processing.DataOperations\", \"processing.TypeConverterComponent\"]\n\n inputs = [\n DataInput(\n name=\"data_list\",\n display_name=\"Data or Data List\",\n info=\"One or multiple Data objects to transform into a DataFrame.\",\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"DataFrame\",\n name=\"dataframe\",\n method=\"build_dataframe\",\n info=\"A DataFrame built from each Data object's fields plus a 'text' column.\",\n ),\n ]\n\n def build_dataframe(self) -> DataFrame:\n \"\"\"Builds a DataFrame from Data objects by combining their fields.\n\n For each Data object:\n - Merge item.data (dictionary) as columns\n - If item.text is present, add 'text' column\n\n Returns a DataFrame with one row per Data object.\n \"\"\"\n data_input = self.data_list\n\n # If user passed a single Data, it might come in as a single object rather than a list\n if not isinstance(data_input, list):\n data_input = [data_input]\n\n rows = []\n for item in data_input:\n if not isinstance(item, Data):\n msg = f\"Expected Data objects, got {type(item)} instead.\"\n raise TypeError(msg)\n\n # Start with a copy of item.data or an empty dict\n row_dict = dict(item.data) if item.data else {}\n\n # If the Data object has text, store it under 'text' col\n text_val = item.get_text()\n if text_val:\n row_dict[\"text\"] = text_val\n\n rows.append(row_dict)\n\n # Build a DataFrame from these row dictionaries\n df_result = DataFrame(rows)\n self.status = df_result # store in self.status for logs\n return df_result\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass DataToDataFrameComponent(Component):\n display_name = \"Data → DataFrame\"\n description = (\n \"Converts one or multiple Data objects into a DataFrame. \"\n \"Each Data object corresponds to one row. Fields from `.data` become columns, \"\n \"and the `.text` (if present) is placed in a 'text' column.\"\n )\n icon = \"table\"\n name = \"DataToDataFrame\"\n legacy = True\n replacement = [\"processing.DataOperations\", \"processing.TypeConverterComponent\"]\n\n inputs = [\n DataInput(\n name=\"data_list\",\n display_name=\"Data or Data List\",\n info=\"One or multiple Data objects to transform into a DataFrame.\",\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Table\",\n name=\"dataframe\",\n method=\"build_dataframe\",\n info=\"A DataFrame built from each Data object's fields plus a 'text' column.\",\n ),\n ]\n\n def build_dataframe(self) -> DataFrame:\n \"\"\"Builds a DataFrame from Data objects by combining their fields.\n\n For each Data object:\n - Merge item.data (dictionary) as columns\n - If item.text is present, add 'text' column\n\n Returns a DataFrame with one row per Data object.\n \"\"\"\n data_input = self.data_list\n\n # If user passed a single Data, it might come in as a single object rather than a list\n if not isinstance(data_input, list):\n data_input = [data_input]\n\n rows = []\n for item in data_input:\n if not isinstance(item, Data):\n msg = f\"Expected Data objects, got {type(item)} instead.\"\n raise TypeError(msg)\n\n # Start with a copy of item.data or an empty dict\n row_dict = dict(item.data) if item.data else {}\n\n # If the Data object has text, store it under 'text' col\n text_val = item.get_text()\n if text_val:\n row_dict[\"text\"] = text_val\n\n rows.append(row_dict)\n\n # Build a DataFrame from these row dictionaries\n df_result = DataFrame(rows)\n self.status = df_result # store in self.status for logs\n return df_result\n" }, "data_list": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Data or Data List", "dynamic": false, "info": "One or multiple Data objects to transform into a DataFrame.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -101128,7 +101244,7 @@ }, "DynamicCreateData": { "base_classes": [ - "Data", + "JSON", "Message" ], "beta": false, @@ -101146,7 +101262,7 @@ "icon": "ListFilter", "legacy": false, "metadata": { - "code_hash": "0457c4acdf45", + "code_hash": "8af479187c18", "dependencies": { "dependencies": [ { @@ -101164,14 +101280,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "process_form", "name": "form_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -101209,7 +101325,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom lfx.custom import Component\nfrom lfx.io import (\n BoolInput,\n FloatInput,\n HandleInput,\n IntInput,\n MultilineInput,\n Output,\n StrInput,\n TableInput,\n)\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass DynamicCreateDataComponent(Component):\n display_name: str = \"Dynamic Create Data\"\n description: str = \"Dynamically create a Data with a specified number of fields.\"\n name: str = \"DynamicCreateData\"\n MAX_FIELDS = 15 # Define a constant for maximum number of fields\n icon = \"ListFilter\"\n\n def __init__(self, **kwargs):\n super().__init__(**kwargs)\n\n inputs = [\n TableInput(\n name=\"form_fields\",\n display_name=\"Input Configuration\",\n info=(\n \"Define the dynamic form fields. Each row creates a new input field \"\n \"that can connect to other components.\"\n ),\n table_schema=[\n {\n \"name\": \"field_name\",\n \"display_name\": \"Field Name\",\n \"type\": \"str\",\n \"description\": \"Name for the field (used as both internal name and display label)\",\n },\n {\n \"name\": \"field_type\",\n \"display_name\": \"Field Type\",\n \"type\": \"str\",\n \"description\": \"Type of input field to create\",\n \"options\": [\"Text\", \"Data\", \"Number\", \"Handle\", \"Boolean\"],\n \"value\": \"Text\",\n },\n ],\n value=[],\n real_time_refresh=True,\n ),\n BoolInput(\n name=\"include_metadata\",\n display_name=\"Include Metadata\",\n info=\"Include form configuration metadata in the output.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"form_data\", method=\"process_form\"),\n Output(display_name=\"Message\", name=\"message\", method=\"get_message\"),\n ]\n\n def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:\n \"\"\"Update build configuration to add dynamic inputs that can connect to other components.\"\"\"\n if field_name == \"form_fields\":\n # Clear existing dynamic inputs from build config\n keys_to_remove = [key for key in build_config if key.startswith(\"dynamic_\")]\n for key in keys_to_remove:\n del build_config[key]\n\n # Add dynamic inputs based on table configuration\n # Safety check to ensure field_value is not None and is iterable\n if field_value is None:\n field_value = []\n\n for i, field_config in enumerate(field_value):\n # Safety check to ensure field_config is not None\n if field_config is None:\n continue\n\n field_name = field_config.get(\"field_name\", f\"field_{i}\")\n display_name = field_name # Use field_name as display_name\n field_type_option = field_config.get(\"field_type\", \"Text\")\n default_value = \"\" # All fields have empty default value\n required = False # All fields are optional by default\n help_text = \"\" # All fields have empty help text\n\n # Map field type options to actual field types and input types\n field_type_mapping = {\n \"Text\": {\"field_type\": \"multiline\", \"input_types\": [\"Text\", \"Message\"]},\n \"Data\": {\"field_type\": \"data\", \"input_types\": [\"Data\"]},\n \"Number\": {\"field_type\": \"number\", \"input_types\": [\"Text\", \"Message\"]},\n \"Handle\": {\"field_type\": \"handle\", \"input_types\": [\"Text\", \"Data\", \"Message\"]},\n \"Boolean\": {\"field_type\": \"boolean\", \"input_types\": None},\n }\n\n field_config_mapped = field_type_mapping.get(\n field_type_option, {\"field_type\": \"text\", \"input_types\": []}\n )\n if not isinstance(field_config_mapped, dict):\n field_config_mapped = {\"field_type\": \"text\", \"input_types\": []}\n field_type = field_config_mapped[\"field_type\"]\n input_types_list = field_config_mapped[\"input_types\"]\n\n # Create the appropriate input type based on field_type\n dynamic_input_name = f\"dynamic_{field_name}\"\n\n if field_type == \"text\":\n if input_types_list:\n build_config[dynamic_input_name] = StrInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Can connect to: {', '.join(input_types_list)})\",\n value=default_value,\n required=required,\n input_types=input_types_list,\n )\n else:\n build_config[dynamic_input_name] = StrInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_value,\n required=required,\n )\n\n elif field_type == \"multiline\":\n if input_types_list:\n build_config[dynamic_input_name] = MultilineInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Can connect to: {', '.join(input_types_list)})\",\n value=default_value,\n required=required,\n input_types=input_types_list,\n )\n else:\n build_config[dynamic_input_name] = MultilineInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_value,\n required=required,\n )\n\n elif field_type == \"number\":\n try:\n default_int = int(default_value) if default_value else 0\n except ValueError:\n default_int = 0\n\n if input_types_list:\n build_config[dynamic_input_name] = IntInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Can connect to: {', '.join(input_types_list)})\",\n value=default_int,\n required=required,\n input_types=input_types_list,\n )\n else:\n build_config[dynamic_input_name] = IntInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_int,\n required=required,\n )\n\n elif field_type == \"float\":\n try:\n default_float = float(default_value) if default_value else 0.0\n except ValueError:\n default_float = 0.0\n\n if input_types_list:\n build_config[dynamic_input_name] = FloatInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Can connect to: {', '.join(input_types_list)})\",\n value=default_float,\n required=required,\n input_types=input_types_list,\n )\n else:\n build_config[dynamic_input_name] = FloatInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_float,\n required=required,\n )\n\n elif field_type == \"boolean\":\n default_bool = default_value.lower() in [\"true\", \"1\", \"yes\"] if default_value else False\n\n # Boolean fields don't use input_types parameter to avoid errors\n build_config[dynamic_input_name] = BoolInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_bool,\n input_types=[],\n required=required,\n )\n\n elif field_type == \"handle\":\n # HandleInput for generic data connections\n build_config[dynamic_input_name] = HandleInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Accepts: {', '.join(input_types_list) if input_types_list else 'Any'})\",\n input_types=input_types_list if input_types_list else [\"Data\", \"Text\", \"Message\"],\n required=required,\n )\n\n elif field_type == \"data\":\n # Specialized for Data type connections\n build_config[dynamic_input_name] = HandleInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Data input)\",\n input_types=input_types_list if input_types_list else [\"Data\"],\n required=required,\n )\n\n else:\n # Default to text input for unknown types\n build_config[dynamic_input_name] = StrInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Unknown type '{field_type}', defaulting to text)\",\n value=default_value,\n required=required,\n )\n\n return build_config\n\n def get_dynamic_values(self) -> dict[str, Any]:\n \"\"\"Extract simple values from all dynamic inputs, handling both manual and connected inputs.\"\"\"\n dynamic_values = {}\n connection_info = {}\n form_fields = getattr(self, \"form_fields\", [])\n\n for field_config in form_fields:\n # Safety check to ensure field_config is not None\n if field_config is None:\n continue\n\n field_name = field_config.get(\"field_name\", \"\")\n if field_name:\n dynamic_input_name = f\"dynamic_{field_name}\"\n value = getattr(self, dynamic_input_name, None)\n\n # Extract simple values from connections or manual input\n if value is not None:\n try:\n extracted_value = self._extract_simple_value(value)\n dynamic_values[field_name] = extracted_value\n\n # Determine connection type for status\n if hasattr(value, \"text\") and hasattr(value, \"timestamp\"):\n connection_info[field_name] = \"Connected (Message)\"\n elif hasattr(value, \"data\"):\n connection_info[field_name] = \"Connected (Data)\"\n elif isinstance(value, (str, int, float, bool, list, dict)):\n connection_info[field_name] = \"Manual input\"\n else:\n connection_info[field_name] = \"Connected (Object)\"\n\n except (AttributeError, TypeError, ValueError):\n # Fallback to string representation if all else fails\n dynamic_values[field_name] = str(value)\n connection_info[field_name] = \"Error\"\n else:\n # Use empty default value if nothing connected\n dynamic_values[field_name] = \"\"\n connection_info[field_name] = \"Empty default\"\n\n # Store connection info for status output\n self._connection_info = connection_info\n return dynamic_values\n\n def _extract_simple_value(self, value: Any) -> Any:\n \"\"\"Extract the simplest, most useful value from any input type.\"\"\"\n # Handle None\n if value is None:\n return None\n\n # Handle simple types directly\n if isinstance(value, (str, int, float, bool)):\n return value\n\n # Handle lists and tuples - keep simple\n if isinstance(value, (list, tuple)):\n return [self._extract_simple_value(item) for item in value]\n\n # Handle dictionaries - keep simple\n if isinstance(value, dict):\n return {str(k): self._extract_simple_value(v) for k, v in value.items()}\n\n # Handle Message objects - extract only the text\n if hasattr(value, \"text\"):\n return str(value.text) if value.text is not None else \"\"\n\n # Handle Data objects - extract the data content\n if hasattr(value, \"data\") and value.data is not None:\n return self._extract_simple_value(value.data)\n\n # For any other object, convert to string\n return str(value)\n\n def process_form(self) -> Data:\n \"\"\"Process all dynamic form inputs and return clean data with just field values.\"\"\"\n # Get all dynamic values (just the key:value pairs)\n dynamic_values = self.get_dynamic_values()\n\n # Update status with connection info\n connected_fields = len([v for v in getattr(self, \"_connection_info\", {}).values() if \"Connected\" in v])\n total_fields = len(dynamic_values)\n\n self.status = f\"Form processed successfully. {connected_fields}/{total_fields} fields connected to components.\"\n\n # Return clean Data object with just the field values\n return Data(data=dynamic_values)\n\n def get_message(self) -> Message:\n \"\"\"Return form data as a formatted text message.\"\"\"\n # Get all dynamic values\n dynamic_values = self.get_dynamic_values()\n\n if not dynamic_values:\n return Message(text=\"No form data available\")\n\n # Format as text message\n message_lines = [\"📋 Form Data:\"]\n message_lines.append(\"=\" * 40)\n\n for field_name, value in dynamic_values.items():\n # Use field_name as display_name\n display_name = field_name\n\n message_lines.append(f\"• {display_name}: {value}\")\n\n message_lines.append(\"=\" * 40)\n message_lines.append(f\"Total fields: {len(dynamic_values)}\")\n\n message_text = \"\\n\".join(message_lines)\n self.status = f\"Message formatted with {len(dynamic_values)} fields\"\n\n return Message(text=message_text)\n" + "value": "from typing import Any\n\nfrom lfx.custom import Component\nfrom lfx.io import (\n BoolInput,\n FloatInput,\n HandleInput,\n IntInput,\n MultilineInput,\n Output,\n StrInput,\n TableInput,\n)\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass DynamicCreateDataComponent(Component):\n display_name: str = \"Dynamic Create Data\"\n description: str = \"Dynamically create a Data with a specified number of fields.\"\n name: str = \"DynamicCreateData\"\n MAX_FIELDS = 15 # Define a constant for maximum number of fields\n icon = \"ListFilter\"\n\n def __init__(self, **kwargs):\n super().__init__(**kwargs)\n\n inputs = [\n TableInput(\n name=\"form_fields\",\n display_name=\"Input Configuration\",\n info=(\n \"Define the dynamic form fields. Each row creates a new input field \"\n \"that can connect to other components.\"\n ),\n table_schema=[\n {\n \"name\": \"field_name\",\n \"display_name\": \"Field Name\",\n \"type\": \"str\",\n \"description\": \"Name for the field (used as both internal name and display label)\",\n },\n {\n \"name\": \"field_type\",\n \"display_name\": \"Field Type\",\n \"type\": \"str\",\n \"description\": \"Type of input field to create\",\n \"options\": [\"Text\", \"Data\", \"Number\", \"Handle\", \"Boolean\"],\n \"value\": \"Text\",\n },\n ],\n value=[],\n real_time_refresh=True,\n ),\n BoolInput(\n name=\"include_metadata\",\n display_name=\"Include Metadata\",\n info=\"Include form configuration metadata in the output.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"form_data\", method=\"process_form\"),\n Output(display_name=\"Message\", name=\"message\", method=\"get_message\"),\n ]\n\n def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:\n \"\"\"Update build configuration to add dynamic inputs that can connect to other components.\"\"\"\n if field_name == \"form_fields\":\n # Clear existing dynamic inputs from build config\n keys_to_remove = [key for key in build_config if key.startswith(\"dynamic_\")]\n for key in keys_to_remove:\n del build_config[key]\n\n # Add dynamic inputs based on table configuration\n # Safety check to ensure field_value is not None and is iterable\n if field_value is None:\n field_value = []\n\n for i, field_config in enumerate(field_value):\n # Safety check to ensure field_config is not None\n if field_config is None:\n continue\n\n field_name = field_config.get(\"field_name\", f\"field_{i}\")\n display_name = field_name # Use field_name as display_name\n field_type_option = field_config.get(\"field_type\", \"Text\")\n default_value = \"\" # All fields have empty default value\n required = False # All fields are optional by default\n help_text = \"\" # All fields have empty help text\n\n # Map field type options to actual field types and input types\n field_type_mapping = {\n \"Text\": {\"field_type\": \"multiline\", \"input_types\": [\"Text\", \"Message\"]},\n \"Data\": {\"field_type\": \"data\", \"input_types\": [\"Data\"]},\n \"Number\": {\"field_type\": \"number\", \"input_types\": [\"Text\", \"Message\"]},\n \"Handle\": {\"field_type\": \"handle\", \"input_types\": [\"Text\", \"Data\", \"Message\"]},\n \"Boolean\": {\"field_type\": \"boolean\", \"input_types\": None},\n }\n\n field_config_mapped = field_type_mapping.get(\n field_type_option, {\"field_type\": \"text\", \"input_types\": []}\n )\n if not isinstance(field_config_mapped, dict):\n field_config_mapped = {\"field_type\": \"text\", \"input_types\": []}\n field_type = field_config_mapped[\"field_type\"]\n input_types_list = field_config_mapped[\"input_types\"]\n\n # Create the appropriate input type based on field_type\n dynamic_input_name = f\"dynamic_{field_name}\"\n\n if field_type == \"text\":\n if input_types_list:\n build_config[dynamic_input_name] = StrInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Can connect to: {', '.join(input_types_list)})\",\n value=default_value,\n required=required,\n input_types=input_types_list,\n )\n else:\n build_config[dynamic_input_name] = StrInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_value,\n required=required,\n )\n\n elif field_type == \"multiline\":\n if input_types_list:\n build_config[dynamic_input_name] = MultilineInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Can connect to: {', '.join(input_types_list)})\",\n value=default_value,\n required=required,\n input_types=input_types_list,\n )\n else:\n build_config[dynamic_input_name] = MultilineInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_value,\n required=required,\n )\n\n elif field_type == \"number\":\n try:\n default_int = int(default_value) if default_value else 0\n except ValueError:\n default_int = 0\n\n if input_types_list:\n build_config[dynamic_input_name] = IntInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Can connect to: {', '.join(input_types_list)})\",\n value=default_int,\n required=required,\n input_types=input_types_list,\n )\n else:\n build_config[dynamic_input_name] = IntInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_int,\n required=required,\n )\n\n elif field_type == \"float\":\n try:\n default_float = float(default_value) if default_value else 0.0\n except ValueError:\n default_float = 0.0\n\n if input_types_list:\n build_config[dynamic_input_name] = FloatInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Can connect to: {', '.join(input_types_list)})\",\n value=default_float,\n required=required,\n input_types=input_types_list,\n )\n else:\n build_config[dynamic_input_name] = FloatInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_float,\n required=required,\n )\n\n elif field_type == \"boolean\":\n default_bool = default_value.lower() in [\"true\", \"1\", \"yes\"] if default_value else False\n\n # Boolean fields don't use input_types parameter to avoid errors\n build_config[dynamic_input_name] = BoolInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=help_text,\n value=default_bool,\n input_types=[],\n required=required,\n )\n\n elif field_type == \"handle\":\n # HandleInput for generic data connections\n build_config[dynamic_input_name] = HandleInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Accepts: {', '.join(input_types_list) if input_types_list else 'Any'})\",\n input_types=input_types_list if input_types_list else [\"Data\", \"Text\", \"Message\"],\n required=required,\n )\n\n elif field_type == \"data\":\n # Specialized for Data type connections\n build_config[dynamic_input_name] = HandleInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Data input)\",\n input_types=input_types_list if input_types_list else [\"Data\"],\n required=required,\n )\n\n else:\n # Default to text input for unknown types\n build_config[dynamic_input_name] = StrInput(\n name=dynamic_input_name,\n display_name=display_name,\n info=f\"{help_text} (Unknown type '{field_type}', defaulting to text)\",\n value=default_value,\n required=required,\n )\n\n return build_config\n\n def get_dynamic_values(self) -> dict[str, Any]:\n \"\"\"Extract simple values from all dynamic inputs, handling both manual and connected inputs.\"\"\"\n dynamic_values = {}\n connection_info = {}\n form_fields = getattr(self, \"form_fields\", [])\n\n for field_config in form_fields:\n # Safety check to ensure field_config is not None\n if field_config is None:\n continue\n\n field_name = field_config.get(\"field_name\", \"\")\n if field_name:\n dynamic_input_name = f\"dynamic_{field_name}\"\n value = getattr(self, dynamic_input_name, None)\n\n # Extract simple values from connections or manual input\n if value is not None:\n try:\n extracted_value = self._extract_simple_value(value)\n dynamic_values[field_name] = extracted_value\n\n # Determine connection type for status\n if hasattr(value, \"text\") and hasattr(value, \"timestamp\"):\n connection_info[field_name] = \"Connected (Message)\"\n elif hasattr(value, \"data\"):\n connection_info[field_name] = \"Connected (Data)\"\n elif isinstance(value, (str, int, float, bool, list, dict)):\n connection_info[field_name] = \"Manual input\"\n else:\n connection_info[field_name] = \"Connected (Object)\"\n\n except (AttributeError, TypeError, ValueError):\n # Fallback to string representation if all else fails\n dynamic_values[field_name] = str(value)\n connection_info[field_name] = \"Error\"\n else:\n # Use empty default value if nothing connected\n dynamic_values[field_name] = \"\"\n connection_info[field_name] = \"Empty default\"\n\n # Store connection info for status output\n self._connection_info = connection_info\n return dynamic_values\n\n def _extract_simple_value(self, value: Any) -> Any:\n \"\"\"Extract the simplest, most useful value from any input type.\"\"\"\n # Handle None\n if value is None:\n return None\n\n # Handle simple types directly\n if isinstance(value, (str, int, float, bool)):\n return value\n\n # Handle lists and tuples - keep simple\n if isinstance(value, (list, tuple)):\n return [self._extract_simple_value(item) for item in value]\n\n # Handle dictionaries - keep simple\n if isinstance(value, dict):\n return {str(k): self._extract_simple_value(v) for k, v in value.items()}\n\n # Handle Message objects - extract only the text\n if hasattr(value, \"text\"):\n return str(value.text) if value.text is not None else \"\"\n\n # Handle Data objects - extract the data content\n if hasattr(value, \"data\") and value.data is not None:\n return self._extract_simple_value(value.data)\n\n # For any other object, convert to string\n return str(value)\n\n def process_form(self) -> Data:\n \"\"\"Process all dynamic form inputs and return clean data with just field values.\"\"\"\n # Get all dynamic values (just the key:value pairs)\n dynamic_values = self.get_dynamic_values()\n\n # Update status with connection info\n connected_fields = len([v for v in getattr(self, \"_connection_info\", {}).values() if \"Connected\" in v])\n total_fields = len(dynamic_values)\n\n self.status = f\"Form processed successfully. {connected_fields}/{total_fields} fields connected to components.\"\n\n # Return clean Data object with just the field values\n return Data(data=dynamic_values)\n\n def get_message(self) -> Message:\n \"\"\"Return form data as a formatted text message.\"\"\"\n # Get all dynamic values\n dynamic_values = self.get_dynamic_values()\n\n if not dynamic_values:\n return Message(text=\"No form data available\")\n\n # Format as text message\n message_lines = [\"📋 Form Data:\"]\n message_lines.append(\"=\" * 40)\n\n for field_name, value in dynamic_values.items():\n # Use field_name as display_name\n display_name = field_name\n\n message_lines.append(f\"• {display_name}: {value}\")\n\n message_lines.append(\"=\" * 40)\n message_lines.append(f\"Total fields: {len(dynamic_values)}\")\n\n message_text = \"\\n\".join(message_lines)\n self.status = f\"Message formatted with {len(dynamic_values)} fields\"\n\n return Message(text=message_text)\n" }, "form_fields": { "_input_type": "TableInput", @@ -101217,6 +101333,10 @@ "display_name": "Input Configuration", "dynamic": false, "info": "Define the dynamic form fields. Each row creates a new input field that can connect to other components.", + "input_types": [ + "DataFrame", + "Table" + ], "is_list": true, "list_add_label": "Add More", "name": "form_fields", @@ -101282,7 +101402,7 @@ }, "ExtractaKey": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -101321,10 +101441,10 @@ "group_outputs": false, "method": "extract_key", "name": "extracted_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -101354,13 +101474,14 @@ "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, Output, StrInput\nfrom lfx.schema.data import Data\n\n\nclass ExtractDataKeyComponent(Component):\n display_name = \"Extract Key\"\n description = (\n \"Extract a specific key from a Data object or a list of \"\n \"Data objects and return the extracted value(s) as Data object(s).\"\n )\n icon = \"key\"\n name = \"ExtractaKey\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(\n name=\"data_input\",\n display_name=\"Data Input\",\n info=\"The Data object or list of Data objects to extract the key from.\",\n ),\n StrInput(\n name=\"key\",\n display_name=\"Key to Extract\",\n info=\"The key in the Data object(s) to extract.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Extracted Data\", name=\"extracted_data\", method=\"extract_key\"),\n ]\n\n def extract_key(self) -> Data | list[Data]:\n key = self.key\n\n if isinstance(self.data_input, list):\n result = []\n for item in self.data_input:\n if isinstance(item, Data) and key in item.data:\n extracted_value = item.data[key]\n result.append(Data(data={key: extracted_value}))\n self.status = result\n return result\n if isinstance(self.data_input, Data):\n if key in self.data_input.data:\n extracted_value = self.data_input.data[key]\n result = Data(data={key: extracted_value})\n self.status = result\n return result\n self.status = f\"Key '{key}' not found in Data object.\"\n return Data(data={\"error\": f\"Key '{key}' not found in Data object.\"})\n self.status = \"Invalid input. Expected Data object or list of Data objects.\"\n return Data(data={\"error\": \"Invalid input. Expected Data object or list of Data objects.\"})\n" }, "data_input": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Data Input", "dynamic": false, "info": "The Data object or list of Data objects to extract the key from.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -101403,7 +101524,7 @@ }, "FilterData": { "base_classes": [ - "Data" + "JSON" ], "beta": true, "conditional_paths": [], @@ -101420,7 +101541,7 @@ "icon": "filter", "legacy": true, "metadata": { - "code_hash": "04c50937216d", + "code_hash": "5f364efb79fc", "dependencies": { "dependencies": [ { @@ -101442,10 +101563,10 @@ "group_outputs": false, "method": "filter_data", "name": "filtered_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -101472,16 +101593,17 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\n\n\nclass FilterDataComponent(Component):\n display_name = \"Filter Data\"\n description = \"Filters a Data object based on a list of keys.\"\n icon = \"filter\"\n beta = True\n name = \"FilterData\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"Data object to filter.\",\n ),\n MessageTextInput(\n name=\"filter_criteria\",\n display_name=\"Filter Criteria\",\n info=\"List of keys to filter by.\",\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Filtered Data\", name=\"filtered_data\", method=\"filter_data\"),\n ]\n\n def filter_data(self) -> Data:\n filter_criteria: list[str] = self.filter_criteria\n data = self.data.data if isinstance(self.data, Data) else {}\n\n # Filter the data\n filtered = {key: value for key, value in data.items() if key in filter_criteria}\n\n # Create a new Data object with the filtered data\n filtered_data = Data(data=filtered)\n self.status = filtered_data\n return filtered_data\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\n\n\nclass FilterDataComponent(Component):\n display_name = \"Filter Data\"\n description = \"Filters a Data object based on a list of keys.\"\n icon = \"filter\"\n beta = True\n name = \"FilterData\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"JSON\",\n info=\"Data object to filter.\",\n ),\n MessageTextInput(\n name=\"filter_criteria\",\n display_name=\"Filter Criteria\",\n info=\"List of keys to filter by.\",\n is_list=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Filtered Data\", name=\"filtered_data\", method=\"filter_data\"),\n ]\n\n def filter_data(self) -> Data:\n filter_criteria: list[str] = self.filter_criteria\n data = self.data.data if isinstance(self.data, Data) else {}\n\n # Filter the data\n filtered = {key: value for key, value in data.items() if key in filter_criteria}\n\n # Create a new Data object with the filtered data\n filtered_data = Data(data=filtered)\n self.status = filtered_data\n return filtered_data\n" }, "data": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, - "display_name": "Data", + "display_name": "JSON", "dynamic": false, "info": "Data object to filter.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -101528,7 +101650,7 @@ }, "FilterDataValues": { "base_classes": [ - "Data" + "JSON" ], "beta": true, "conditional_paths": [], @@ -101547,7 +101669,7 @@ "icon": "filter", "legacy": true, "metadata": { - "code_hash": "847522549c67", + "code_hash": "274c9e3a6e7e", "dependencies": { "dependencies": [ { @@ -101569,10 +101691,10 @@ "group_outputs": false, "method": "filter_data", "name": "filtered_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -101599,7 +101721,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, DropdownInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\n\n\nclass DataFilterComponent(Component):\n display_name = \"Filter Values\"\n description = (\n \"Filter a list of data items based on a specified key, filter value,\"\n \" and comparison operator. Check advanced options to select match comparision.\"\n )\n icon = \"filter\"\n beta = True\n name = \"FilterDataValues\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(name=\"input_data\", display_name=\"Input Data\", info=\"The list of data items to filter.\", is_list=True),\n MessageTextInput(\n name=\"filter_key\",\n display_name=\"Filter Key\",\n info=\"The key to filter on (e.g., 'route').\",\n value=\"route\",\n input_types=[\"Data\"],\n ),\n MessageTextInput(\n name=\"filter_value\",\n display_name=\"Filter Value\",\n info=\"The value to filter by (e.g., 'CMIP').\",\n value=\"CMIP\",\n input_types=[\"Data\"],\n ),\n DropdownInput(\n name=\"operator\",\n display_name=\"Comparison Operator\",\n options=[\"equals\", \"not equals\", \"contains\", \"starts with\", \"ends with\"],\n info=\"The operator to apply for comparing the values.\",\n value=\"equals\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Filtered Data\", name=\"filtered_data\", method=\"filter_data\"),\n ]\n\n def compare_values(self, item_value: Any, filter_value: str, operator: str) -> bool:\n if operator == \"equals\":\n return str(item_value) == filter_value\n if operator == \"not equals\":\n return str(item_value) != filter_value\n if operator == \"contains\":\n return filter_value in str(item_value)\n if operator == \"starts with\":\n return str(item_value).startswith(filter_value)\n if operator == \"ends with\":\n return str(item_value).endswith(filter_value)\n return False\n\n def filter_data(self) -> list[Data]:\n # Extract inputs\n input_data: list[Data] = self.input_data\n filter_key: str = self.filter_key.text\n filter_value: str = self.filter_value.text\n operator: str = self.operator\n\n # Validate inputs\n if not input_data:\n self.status = \"Input data is empty.\"\n return []\n\n if not filter_key or not filter_value:\n self.status = \"Filter key or value is missing.\"\n return input_data\n\n # Filter the data\n filtered_data = []\n for item in input_data:\n if isinstance(item.data, dict) and filter_key in item.data:\n if self.compare_values(item.data[filter_key], filter_value, operator):\n filtered_data.append(item)\n else:\n self.status = f\"Warning: Some items don't have the key '{filter_key}' or are not dictionaries.\"\n\n self.status = filtered_data\n return filtered_data\n" + "value": "from typing import Any\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, DropdownInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\n\n\nclass DataFilterComponent(Component):\n display_name = \"Filter Values\"\n description = (\n \"Filter a list of data items based on a specified key, filter value,\"\n \" and comparison operator. Check advanced options to select match comparision.\"\n )\n icon = \"filter\"\n beta = True\n name = \"FilterDataValues\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(name=\"input_data\", display_name=\"Input Data\", info=\"The list of data items to filter.\", is_list=True),\n MessageTextInput(\n name=\"filter_key\",\n display_name=\"Filter Key\",\n info=\"The key to filter on (e.g., 'route').\",\n value=\"route\",\n input_types=[\"Data\", \"JSON\"],\n ),\n MessageTextInput(\n name=\"filter_value\",\n display_name=\"Filter Value\",\n info=\"The value to filter by (e.g., 'CMIP').\",\n value=\"CMIP\",\n input_types=[\"Data\", \"JSON\"],\n ),\n DropdownInput(\n name=\"operator\",\n display_name=\"Comparison Operator\",\n options=[\"equals\", \"not equals\", \"contains\", \"starts with\", \"ends with\"],\n info=\"The operator to apply for comparing the values.\",\n value=\"equals\",\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Filtered Data\", name=\"filtered_data\", method=\"filter_data\"),\n ]\n\n def compare_values(self, item_value: Any, filter_value: str, operator: str) -> bool:\n if operator == \"equals\":\n return str(item_value) == filter_value\n if operator == \"not equals\":\n return str(item_value) != filter_value\n if operator == \"contains\":\n return filter_value in str(item_value)\n if operator == \"starts with\":\n return str(item_value).startswith(filter_value)\n if operator == \"ends with\":\n return str(item_value).endswith(filter_value)\n return False\n\n def filter_data(self) -> list[Data]:\n # Extract inputs\n input_data: list[Data] = self.input_data\n filter_key: str = self.filter_key.text\n filter_value: str = self.filter_value.text\n operator: str = self.operator\n\n # Validate inputs\n if not input_data:\n self.status = \"Input data is empty.\"\n return []\n\n if not filter_key or not filter_value:\n self.status = \"Filter key or value is missing.\"\n return input_data\n\n # Filter the data\n filtered_data = []\n for item in input_data:\n if isinstance(item.data, dict) and filter_key in item.data:\n if self.compare_values(item.data[filter_key], filter_value, operator):\n filtered_data.append(item)\n else:\n self.status = f\"Warning: Some items don't have the key '{filter_key}' or are not dictionaries.\"\n\n self.status = filtered_data\n return filtered_data\n" }, "filter_key": { "_input_type": "MessageTextInput", @@ -101608,7 +101730,8 @@ "dynamic": false, "info": "The key to filter on (e.g., 'route').", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -101633,7 +101756,8 @@ "dynamic": false, "info": "The value to filter by (e.g., 'CMIP').", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -101652,13 +101776,14 @@ "value": "CMIP" }, "input_data": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Input Data", "dynamic": false, "info": "The list of data items to filter.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -101877,7 +102002,7 @@ }, "MergeDataComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -101894,7 +102019,7 @@ "icon": "merge", "legacy": true, "metadata": { - "code_hash": "a2ecb813aac5", + "code_hash": "3d8c0fa8f47c", "dependencies": { "dependencies": [ { @@ -101912,14 +102037,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "combine_data", "name": "combined_data", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -101946,16 +102071,17 @@ "show": true, "title_case": false, "type": "code", - "value": "from enum import Enum\nfrom typing import cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, DropdownInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass DataOperation(str, Enum):\n CONCATENATE = \"Concatenate\"\n APPEND = \"Append\"\n MERGE = \"Merge\"\n JOIN = \"Join\"\n\n\nclass MergeDataComponent(Component):\n display_name = \"Combine Data\"\n description = \"Combines data using different operations\"\n icon = \"merge\"\n MIN_INPUTS_REQUIRED = 2\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(name=\"data_inputs\", display_name=\"Data Inputs\", info=\"Data to combine\", is_list=True, required=True),\n DropdownInput(\n name=\"operation\",\n display_name=\"Operation Type\",\n options=[op.value for op in DataOperation],\n value=DataOperation.CONCATENATE.value,\n ),\n ]\n outputs = [Output(display_name=\"DataFrame\", name=\"combined_data\", method=\"combine_data\")]\n\n def combine_data(self) -> DataFrame:\n if not self.data_inputs or len(self.data_inputs) < self.MIN_INPUTS_REQUIRED:\n empty_dataframe = DataFrame()\n self.status = empty_dataframe\n return empty_dataframe\n\n operation = DataOperation(self.operation)\n try:\n combined_dataframe = self._process_operation(operation)\n self.status = combined_dataframe\n except Exception as e:\n logger.error(f\"Error during operation {operation}: {e!s}\")\n raise\n else:\n return combined_dataframe\n\n def _process_operation(self, operation: DataOperation) -> DataFrame:\n if operation == DataOperation.CONCATENATE:\n combined_data: dict[str, str | object] = {}\n for data_input in self.data_inputs:\n for key, value in data_input.data.items():\n if key in combined_data:\n if isinstance(combined_data[key], str) and isinstance(value, str):\n combined_data[key] = f\"{combined_data[key]}\\n{value}\"\n else:\n combined_data[key] = value\n else:\n combined_data[key] = value\n return DataFrame([combined_data])\n\n if operation == DataOperation.APPEND:\n rows = [data_input.data for data_input in self.data_inputs]\n return DataFrame(rows)\n\n if operation == DataOperation.MERGE:\n result_data: dict[str, str | list[str] | object] = {}\n for data_input in self.data_inputs:\n for key, value in data_input.data.items():\n if key in result_data and isinstance(value, str):\n if isinstance(result_data[key], list):\n cast(\"list[str]\", result_data[key]).append(value)\n else:\n result_data[key] = [result_data[key], value]\n else:\n result_data[key] = value\n return DataFrame([result_data])\n\n if operation == DataOperation.JOIN:\n combined_data = {}\n for idx, data_input in enumerate(self.data_inputs, 1):\n for key, value in data_input.data.items():\n new_key = f\"{key}_doc{idx}\" if idx > 1 else key\n combined_data[new_key] = value\n return DataFrame([combined_data])\n\n return DataFrame()\n" + "value": "from enum import Enum\nfrom typing import cast\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, DropdownInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass DataOperation(str, Enum):\n CONCATENATE = \"Concatenate\"\n APPEND = \"Append\"\n MERGE = \"Merge\"\n JOIN = \"Join\"\n\n\nclass MergeDataComponent(Component):\n display_name = \"Combine Data\"\n description = \"Combines data using different operations\"\n icon = \"merge\"\n MIN_INPUTS_REQUIRED = 2\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(name=\"data_inputs\", display_name=\"Data Inputs\", info=\"Data to combine\", is_list=True, required=True),\n DropdownInput(\n name=\"operation\",\n display_name=\"Operation Type\",\n options=[op.value for op in DataOperation],\n value=DataOperation.CONCATENATE.value,\n ),\n ]\n outputs = [Output(display_name=\"Table\", name=\"combined_data\", method=\"combine_data\")]\n\n def combine_data(self) -> DataFrame:\n if not self.data_inputs or len(self.data_inputs) < self.MIN_INPUTS_REQUIRED:\n empty_dataframe = DataFrame()\n self.status = empty_dataframe\n return empty_dataframe\n\n operation = DataOperation(self.operation)\n try:\n combined_dataframe = self._process_operation(operation)\n self.status = combined_dataframe\n except Exception as e:\n logger.error(f\"Error during operation {operation}: {e!s}\")\n raise\n else:\n return combined_dataframe\n\n def _process_operation(self, operation: DataOperation) -> DataFrame:\n if operation == DataOperation.CONCATENATE:\n combined_data: dict[str, str | object] = {}\n for data_input in self.data_inputs:\n for key, value in data_input.data.items():\n if key in combined_data:\n if isinstance(combined_data[key], str) and isinstance(value, str):\n combined_data[key] = f\"{combined_data[key]}\\n{value}\"\n else:\n combined_data[key] = value\n else:\n combined_data[key] = value\n return DataFrame([combined_data])\n\n if operation == DataOperation.APPEND:\n rows = [data_input.data for data_input in self.data_inputs]\n return DataFrame(rows)\n\n if operation == DataOperation.MERGE:\n result_data: dict[str, str | list[str] | object] = {}\n for data_input in self.data_inputs:\n for key, value in data_input.data.items():\n if key in result_data and isinstance(value, str):\n if isinstance(result_data[key], list):\n cast(\"list[str]\", result_data[key]).append(value)\n else:\n result_data[key] = [result_data[key], value]\n else:\n result_data[key] = value\n return DataFrame([result_data])\n\n if operation == DataOperation.JOIN:\n combined_data = {}\n for idx, data_input in enumerate(self.data_inputs, 1):\n for key, value in data_input.data.items():\n new_key = f\"{key}_doc{idx}\" if idx > 1 else key\n combined_data[new_key] = value\n return DataFrame([combined_data])\n\n return DataFrame()\n" }, "data_inputs": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Data Inputs", "dynamic": false, "info": "Data to combine", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -102006,7 +102132,7 @@ }, "MessagetoData": { "base_classes": [ - "Data" + "JSON" ], "beta": true, "conditional_paths": [], @@ -102022,7 +102148,7 @@ "icon": "message-square-share", "legacy": true, "metadata": { - "code_hash": "d0af1222aeaf", + "code_hash": "cc86df1d6415", "dependencies": { "dependencies": [ { @@ -102040,14 +102166,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "convert_message_to_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -102074,7 +102200,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import MessageInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass MessageToDataComponent(Component):\n display_name = \"Message to Data\"\n description = \"Convert a Message object to a Data object\"\n icon = \"message-square-share\"\n beta = True\n name = \"MessagetoData\"\n legacy = True\n replacement = [\"processing.TypeConverterComponent\"]\n\n inputs = [\n MessageInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The Message object to convert to a Data object\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"convert_message_to_data\"),\n ]\n\n def convert_message_to_data(self) -> Data:\n # Check for Message by checking if it has the expected attributes instead of isinstance\n if hasattr(self.message, \"data\") and hasattr(self.message, \"text\") and hasattr(self.message, \"get_text\"):\n # Convert Message to Data - this works for both langflow.Message and lfx.Message\n return Data(data=self.message.data)\n\n msg = \"Error converting Message to Data: Input must be a Message object\"\n logger.debug(msg, exc_info=True)\n self.status = msg\n return Data(data={\"error\": msg})\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import MessageInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass MessageToDataComponent(Component):\n display_name = \"Message to Data\"\n description = \"Convert a Message object to a Data object\"\n icon = \"message-square-share\"\n beta = True\n name = \"MessagetoData\"\n legacy = True\n replacement = [\"processing.TypeConverterComponent\"]\n\n inputs = [\n MessageInput(\n name=\"message\",\n display_name=\"Message\",\n info=\"The Message object to convert to a Data object\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"convert_message_to_data\"),\n ]\n\n def convert_message_to_data(self) -> Data:\n # Check for Message by checking if it has the expected attributes instead of isinstance\n if hasattr(self.message, \"data\") and hasattr(self.message, \"text\") and hasattr(self.message, \"get_text\"):\n # Convert Message to Data - this works for both langflow.Message and lfx.Message\n return Data(data=self.message.data)\n\n msg = \"Error converting Message to Data: Input must be a Message object\"\n logger.debug(msg, exc_info=True)\n self.status = msg\n return Data(data={\"error\": msg})\n" }, "message": { "_input_type": "MessageInput", @@ -102227,7 +102353,7 @@ }, "ParseData": { "base_classes": [ - "Data", + "JSON", "Message" ], "beta": false, @@ -102246,7 +102372,7 @@ "icon": "message-square", "legacy": true, "metadata": { - "code_hash": "3fac44a9bb37", + "code_hash": "73e818f86943", "dependencies": { "dependencies": [ { @@ -102279,14 +102405,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data List", + "display_name": "JSON List", "group_outputs": false, "method": "parse_data_as_list", "name": "data_list", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -102314,16 +102440,17 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text, data_to_text_list\nfrom lfx.io import DataInput, MultilineInput, Output, StrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n legacy = True\n replacement = [\"processing.DataOperations\", \"processing.TypeConverterComponent\"]\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"Data\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"Data List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import data_to_text, data_to_text_list\nfrom lfx.io import DataInput, MultilineInput, Output, StrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass ParseDataComponent(Component):\n display_name = \"Data to Message\"\n description = \"Convert Data objects into Messages using any {field_name} from input data.\"\n icon = \"message-square\"\n name = \"ParseData\"\n legacy = True\n replacement = [\"processing.DataOperations\", \"processing.TypeConverterComponent\"]\n metadata = {\n \"legacy_name\": \"Parse Data\",\n }\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"JSON\",\n info=\"The data to convert to text.\",\n is_list=True,\n required=True,\n ),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=\"The template to use for formatting the data. \"\n \"It can contain the keys {text}, {data} or any other key in the Data.\",\n value=\"{text}\",\n required=True,\n ),\n StrInput(name=\"sep\", display_name=\"Separator\", advanced=True, value=\"\\n\"),\n ]\n\n outputs = [\n Output(\n display_name=\"Message\",\n name=\"text\",\n info=\"Data as a single Message, with each input Data separated by Separator\",\n method=\"parse_data\",\n ),\n Output(\n display_name=\"JSON List\",\n name=\"data_list\",\n info=\"Data as a list of new Data, each having `text` formatted by Template\",\n method=\"parse_data_as_list\",\n ),\n ]\n\n def _clean_args(self) -> tuple[list[Data], str, str]:\n data = self.data if isinstance(self.data, list) else [self.data]\n template = self.template\n sep = self.sep\n return data, template, sep\n\n def parse_data(self) -> Message:\n data, template, sep = self._clean_args()\n result_string = data_to_text(template, data, sep)\n self.status = result_string\n return Message(text=result_string)\n\n def parse_data_as_list(self) -> list[Data]:\n data, template, _ = self._clean_args()\n text_list, data_list = data_to_text_list(template, data)\n for item, text in zip(data_list, text_list, strict=True):\n item.set_text(text)\n self.status = data_list\n return data_list\n" }, "data": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, - "display_name": "Data", + "display_name": "JSON", "dynamic": false, "info": "The data to convert to text.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -102413,7 +102540,7 @@ "icon": "braces", "legacy": true, "metadata": { - "code_hash": "9d4b05cf1564", + "code_hash": "af6b7e66d77e", "dependencies": { "dependencies": [ { @@ -102466,16 +102593,17 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import DataFrameInput, MultilineInput, Output, StrInput\nfrom lfx.schema.message import Message\n\n\nclass ParseDataFrameComponent(Component):\n display_name = \"Parse DataFrame\"\n description = (\n \"Convert a DataFrame into plain text following a specified template. \"\n \"Each column in the DataFrame is treated as a possible template key, e.g. {col_name}.\"\n )\n icon = \"braces\"\n name = \"ParseDataFrame\"\n legacy = True\n replacement = [\"processing.DataFrameOperations\", \"processing.TypeConverterComponent\"]\n\n inputs = [\n DataFrameInput(name=\"df\", display_name=\"DataFrame\", info=\"The DataFrame to convert to text rows.\"),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=(\n \"The template for formatting each row. \"\n \"Use placeholders matching column names in the DataFrame, for example '{col1}', '{col2}'.\"\n ),\n value=\"{text}\",\n ),\n StrInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String that joins all row texts when building the single Text output.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Text\",\n name=\"text\",\n info=\"All rows combined into a single text, each row formatted by the template and separated by `sep`.\",\n method=\"parse_data\",\n ),\n ]\n\n def _clean_args(self):\n dataframe = self.df\n template = self.template or \"{text}\"\n sep = self.sep or \"\\n\"\n return dataframe, template, sep\n\n def parse_data(self) -> Message:\n \"\"\"Converts each row of the DataFrame into a formatted string using the template.\n\n then joins them with `sep`. Returns a single combined string as a Message.\n \"\"\"\n dataframe, template, sep = self._clean_args()\n\n lines = []\n # For each row in the DataFrame, build a dict and format\n for _, row in dataframe.iterrows():\n row_dict = row.to_dict()\n text_line = template.format(**row_dict) # e.g. template=\"{text}\", row_dict={\"text\": \"Hello\"}\n lines.append(text_line)\n\n # Join all lines with the provided separator\n result_string = sep.join(lines)\n self.status = result_string # store in self.status for UI logs\n return Message(text=result_string)\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import DataFrameInput, MultilineInput, Output, StrInput\nfrom lfx.schema.message import Message\n\n\nclass ParseDataFrameComponent(Component):\n display_name = \"Parse DataFrame\"\n description = (\n \"Convert a DataFrame into plain text following a specified template. \"\n \"Each column in the DataFrame is treated as a possible template key, e.g. {col_name}.\"\n )\n icon = \"braces\"\n name = \"ParseDataFrame\"\n legacy = True\n replacement = [\"processing.DataFrameOperations\", \"processing.TypeConverterComponent\"]\n\n inputs = [\n DataFrameInput(name=\"df\", display_name=\"Table\", info=\"The DataFrame to convert to text rows.\"),\n MultilineInput(\n name=\"template\",\n display_name=\"Template\",\n info=(\n \"The template for formatting each row. \"\n \"Use placeholders matching column names in the DataFrame, for example '{col1}', '{col2}'.\"\n ),\n value=\"{text}\",\n ),\n StrInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String that joins all row texts when building the single Text output.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Text\",\n name=\"text\",\n info=\"All rows combined into a single text, each row formatted by the template and separated by `sep`.\",\n method=\"parse_data\",\n ),\n ]\n\n def _clean_args(self):\n dataframe = self.df\n template = self.template or \"{text}\"\n sep = self.sep or \"\\n\"\n return dataframe, template, sep\n\n def parse_data(self) -> Message:\n \"\"\"Converts each row of the DataFrame into a formatted string using the template.\n\n then joins them with `sep`. Returns a single combined string as a Message.\n \"\"\"\n dataframe, template, sep = self._clean_args()\n\n lines = []\n # For each row in the DataFrame, build a dict and format\n for _, row in dataframe.iterrows():\n row_dict = row.to_dict()\n text_line = template.format(**row_dict) # e.g. template=\"{text}\", row_dict={\"text\": \"Hello\"}\n lines.append(text_line)\n\n # Join all lines with the provided separator\n result_string = sep.join(lines)\n self.status = result_string # store in self.status for UI logs\n return Message(text=result_string)\n" }, "df": { "_input_type": "DataFrameInput", "advanced": false, - "display_name": "DataFrame", + "display_name": "Table", "dynamic": false, "info": "The DataFrame to convert to text rows.", "input_types": [ - "DataFrame" + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -102547,7 +102675,7 @@ }, "ParseJSONData": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -102564,7 +102692,7 @@ "icon": "braces", "legacy": true, "metadata": { - "code_hash": "5268ca4c42d6", + "code_hash": "2ad980f8bac3", "dependencies": { "dependencies": [ { @@ -102594,10 +102722,10 @@ "group_outputs": false, "method": "filter_data", "name": "filtered_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -102624,7 +102752,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nfrom json import JSONDecodeError\n\nimport jq\nfrom json_repair import repair_json\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass ParseJSONDataComponent(Component):\n display_name = \"Parse JSON\"\n description = \"Convert and extract JSON fields.\"\n icon = \"braces\"\n name = \"ParseJSONData\"\n legacy: bool = True\n replacement = [\"processing.ParserComponent\"]\n\n inputs = [\n HandleInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"Data object to filter.\",\n required=True,\n input_types=[\"Message\", \"Data\"],\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"JQ Query\",\n info=\"JQ Query to filter the data. The input is always a JSON list.\",\n required=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Filtered Data\", name=\"filtered_data\", method=\"filter_data\"),\n ]\n\n def _parse_data(self, input_value) -> str:\n if isinstance(input_value, Message) and isinstance(input_value.text, str):\n return input_value.text\n if isinstance(input_value, Data):\n return json.dumps(input_value.data)\n return str(input_value)\n\n def filter_data(self) -> list[Data]:\n to_filter = self.input_value\n if not to_filter:\n return []\n # Check if input is a list\n if isinstance(to_filter, list):\n to_filter = [self._parse_data(f) for f in to_filter]\n else:\n to_filter = self._parse_data(to_filter)\n\n # If input is not a list, don't wrap it in a list\n if not isinstance(to_filter, list):\n to_filter = repair_json(to_filter)\n try:\n to_filter_as_dict = json.loads(to_filter)\n except JSONDecodeError:\n try:\n to_filter_as_dict = json.loads(repair_json(to_filter))\n except JSONDecodeError as e:\n msg = f\"Invalid JSON: {e}\"\n raise ValueError(msg) from e\n else:\n to_filter = [repair_json(f) for f in to_filter]\n to_filter_as_dict = []\n for f in to_filter:\n try:\n to_filter_as_dict.append(json.loads(f))\n except JSONDecodeError:\n try:\n to_filter_as_dict.append(json.loads(repair_json(f)))\n except JSONDecodeError as e:\n msg = f\"Invalid JSON: {e}\"\n raise ValueError(msg) from e\n to_filter = to_filter_as_dict\n\n full_filter_str = json.dumps(to_filter_as_dict)\n\n logger.info(\"to_filter: %s\", to_filter)\n\n results = jq.compile(self.query).input_text(full_filter_str).all()\n logger.info(\"results: %s\", results)\n return [Data(data=value) if isinstance(value, dict) else Data(text=str(value)) for value in results]\n" + "value": "import json\nfrom json import JSONDecodeError\n\nimport jq\nfrom json_repair import repair_json\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import HandleInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass ParseJSONDataComponent(Component):\n display_name = \"Parse JSON\"\n description = \"Convert and extract JSON fields.\"\n icon = \"braces\"\n name = \"ParseJSONData\"\n legacy: bool = True\n replacement = [\"processing.ParserComponent\"]\n\n inputs = [\n HandleInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"Data object to filter.\",\n required=True,\n input_types=[\"Message\", \"Data\", \"JSON\"],\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"JQ Query\",\n info=\"JQ Query to filter the data. The input is always a JSON list.\",\n required=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Filtered Data\", name=\"filtered_data\", method=\"filter_data\"),\n ]\n\n def _parse_data(self, input_value) -> str:\n if isinstance(input_value, Message) and isinstance(input_value.text, str):\n return input_value.text\n if isinstance(input_value, Data):\n return json.dumps(input_value.data)\n return str(input_value)\n\n def filter_data(self) -> list[Data]:\n to_filter = self.input_value\n if not to_filter:\n return []\n # Check if input is a list\n if isinstance(to_filter, list):\n to_filter = [self._parse_data(f) for f in to_filter]\n else:\n to_filter = self._parse_data(to_filter)\n\n # If input is not a list, don't wrap it in a list\n if not isinstance(to_filter, list):\n to_filter = repair_json(to_filter)\n try:\n to_filter_as_dict = json.loads(to_filter)\n except JSONDecodeError:\n try:\n to_filter_as_dict = json.loads(repair_json(to_filter))\n except JSONDecodeError as e:\n msg = f\"Invalid JSON: {e}\"\n raise ValueError(msg) from e\n else:\n to_filter = [repair_json(f) for f in to_filter]\n to_filter_as_dict = []\n for f in to_filter:\n try:\n to_filter_as_dict.append(json.loads(f))\n except JSONDecodeError:\n try:\n to_filter_as_dict.append(json.loads(repair_json(f)))\n except JSONDecodeError as e:\n msg = f\"Invalid JSON: {e}\"\n raise ValueError(msg) from e\n to_filter = to_filter_as_dict\n\n full_filter_str = json.dumps(to_filter_as_dict)\n\n logger.info(\"to_filter: %s\", to_filter)\n\n results = jq.compile(self.query).input_text(full_filter_str).all()\n logger.info(\"results: %s\", results)\n return [Data(data=value) if isinstance(value, dict) else Data(text=str(value)) for value in results]\n" }, "input_value": { "_input_type": "HandleInput", @@ -102634,7 +102762,8 @@ "info": "Data object to filter.", "input_types": [ "Message", - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -102698,7 +102827,7 @@ "icon": "braces", "legacy": false, "metadata": { - "code_hash": "3cda25c3f7b5", + "code_hash": "cda7b997a730", "dependencies": { "dependencies": [ { @@ -102747,17 +102876,19 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Data or DataFrame\",\n input_types=[\"DataFrame\", \"Data\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.helpers.data import safe_convert\nfrom lfx.inputs.inputs import BoolInput, HandleInput, MessageTextInput, MultilineInput, TabInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.template.field.base import Output\n\n\nclass ParserComponent(Component):\n display_name = \"Parser\"\n description = \"Extracts text using a template.\"\n documentation: str = \"https://docs.langflow.org/parser\"\n icon = \"braces\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"JSON or Table\",\n input_types=[\"DataFrame\", \"Table\", \"Data\", \"JSON\"],\n info=\"Accepts either a DataFrame or a Data object.\",\n required=True,\n ),\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Parser\", \"Stringify\"],\n value=\"Parser\",\n info=\"Convert into raw string instead of using a template.\",\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"pattern\",\n display_name=\"Template\",\n info=(\n \"Use variables within curly brackets to extract column values for DataFrames \"\n \"or key values for Data.\"\n \"For example: `Name: {Name}, Age: {Age}, Country: {Country}`\"\n ),\n value=\"Text: {text}\", # Example default\n dynamic=True,\n show=True,\n required=True,\n ),\n MessageTextInput(\n name=\"sep\",\n display_name=\"Separator\",\n advanced=True,\n value=\"\\n\",\n info=\"String used to separate rows/items.\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Parsed Text\",\n name=\"parsed_text\",\n info=\"Formatted text output.\",\n method=\"parse_combined_text\",\n ),\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n \"\"\"Dynamically hide/show `template` and enforce requirement based on `stringify`.\"\"\"\n if field_name == \"mode\":\n build_config[\"pattern\"][\"show\"] = self.mode == \"Parser\"\n build_config[\"pattern\"][\"required\"] = self.mode == \"Parser\"\n if field_value:\n clean_data = BoolInput(\n name=\"clean_data\",\n display_name=\"Clean Data\",\n info=(\n \"Enable to clean the data by removing empty rows and lines \"\n \"in each cell of the DataFrame/ Data object.\"\n ),\n value=True,\n advanced=True,\n required=False,\n )\n build_config[\"clean_data\"] = clean_data.to_dict()\n else:\n build_config.pop(\"clean_data\", None)\n\n return build_config\n\n def _clean_args(self):\n \"\"\"Prepare arguments based on input type.\"\"\"\n input_data = self.input_data\n\n match input_data:\n case list() if all(isinstance(item, Data) for item in input_data):\n msg = \"List of Data objects is not supported.\"\n raise ValueError(msg)\n case DataFrame():\n return input_data, None\n case Data():\n return None, input_data\n case dict() if \"data\" in input_data:\n try:\n if \"columns\" in input_data: # Likely a DataFrame\n return DataFrame.from_dict(input_data), None\n # Likely a Data object\n return None, Data(**input_data)\n except (TypeError, ValueError, KeyError) as e:\n msg = f\"Invalid structured input provided: {e!s}\"\n raise ValueError(msg) from e\n case _:\n msg = f\"Unsupported input type: {type(input_data)}. Expected DataFrame or Data.\"\n raise ValueError(msg)\n\n def parse_combined_text(self) -> Message:\n \"\"\"Parse all rows/items into a single text or convert input to string if `stringify` is enabled.\"\"\"\n # Early return for stringify option\n if self.mode == \"Stringify\":\n return self.convert_to_string()\n\n df, data = self._clean_args()\n\n lines = []\n if df is not None:\n for _, row in df.iterrows():\n formatted_text = self.pattern.format(**row.to_dict())\n lines.append(formatted_text)\n elif data is not None:\n # Use format_map with a dict that returns default_value for missing keys\n class DefaultDict(dict):\n def __missing__(self, key):\n return data.default_value or \"\"\n\n formatted_text = self.pattern.format_map(DefaultDict(data.data))\n lines.append(formatted_text)\n\n combined_text = self.sep.join(lines)\n self.status = combined_text\n return Message(text=combined_text)\n\n def convert_to_string(self) -> Message:\n \"\"\"Convert input data to string with proper error handling.\"\"\"\n result = \"\"\n if isinstance(self.input_data, list):\n result = \"\\n\".join([safe_convert(item, clean_data=self.clean_data or False) for item in self.input_data])\n else:\n result = safe_convert(self.input_data or False)\n self.log(f\"Converted to string with length: {len(result)}\")\n\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, - "display_name": "Data or DataFrame", + "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", "input_types": [ "DataFrame", - "Data" + "Table", + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -102854,7 +102985,7 @@ }, "RegexExtractorComponent": { "base_classes": [ - "Data", + "JSON", "Message" ], "beta": false, @@ -102872,7 +103003,7 @@ "icon": "regex", "legacy": true, "metadata": { - "code_hash": "f67d7bd7f65e", + "code_hash": "6e5d844f29b3", "dependencies": { "dependencies": [ { @@ -102890,14 +103021,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "extract_matches", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -102938,7 +103069,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import re\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass RegexExtractorComponent(Component):\n display_name = \"Regex Extractor\"\n description = \"Extract patterns from text using regular expressions.\"\n icon = \"regex\"\n legacy = True\n replacement = [\"processing.ParserComponent\"]\n\n inputs = [\n MessageTextInput(\n name=\"input_text\",\n display_name=\"Input Text\",\n info=\"The text to analyze\",\n required=True,\n ),\n MessageTextInput(\n name=\"pattern\",\n display_name=\"Regex Pattern\",\n info=\"The regular expression pattern to match\",\n value=r\"\",\n required=True,\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"extract_matches\"),\n Output(display_name=\"Message\", name=\"text\", method=\"get_matches_text\"),\n ]\n\n def extract_matches(self) -> list[Data]:\n if not self.pattern or not self.input_text:\n self.status = []\n return []\n\n try:\n # Compile regex pattern\n pattern = re.compile(self.pattern)\n\n # Find all matches in the input text\n matches = pattern.findall(self.input_text)\n\n # Filter out empty matches\n filtered_matches = [match for match in matches if match] # Remove empty matches\n\n # Return empty list for no matches, or list of matches if found\n result: list = [] if not filtered_matches else [Data(data={\"match\": match}) for match in filtered_matches]\n\n except re.error as e:\n error_message = f\"Invalid regex pattern: {e!s}\"\n result = [Data(data={\"error\": error_message})]\n except ValueError as e:\n error_message = f\"Error extracting matches: {e!s}\"\n result = [Data(data={\"error\": error_message})]\n\n self.status = result\n return result\n\n def get_matches_text(self) -> Message:\n \"\"\"Get matches as a formatted text message.\"\"\"\n matches = self.extract_matches()\n\n if not matches:\n message = Message(text=\"No matches found\")\n self.status = message\n return message\n\n if \"error\" in matches[0].data:\n message = Message(text=matches[0].data[\"error\"])\n self.status = message\n return message\n\n result = \"\\n\".join(match.data[\"match\"] for match in matches)\n message = Message(text=result)\n self.status = message\n return message\n" + "value": "import re\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass RegexExtractorComponent(Component):\n display_name = \"Regex Extractor\"\n description = \"Extract patterns from text using regular expressions.\"\n icon = \"regex\"\n legacy = True\n replacement = [\"processing.ParserComponent\"]\n\n inputs = [\n MessageTextInput(\n name=\"input_text\",\n display_name=\"Input Text\",\n info=\"The text to analyze\",\n required=True,\n ),\n MessageTextInput(\n name=\"pattern\",\n display_name=\"Regex Pattern\",\n info=\"The regular expression pattern to match\",\n value=r\"\",\n required=True,\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"extract_matches\"),\n Output(display_name=\"Message\", name=\"text\", method=\"get_matches_text\"),\n ]\n\n def extract_matches(self) -> list[Data]:\n if not self.pattern or not self.input_text:\n self.status = []\n return []\n\n try:\n # Compile regex pattern\n pattern = re.compile(self.pattern)\n\n # Find all matches in the input text\n matches = pattern.findall(self.input_text)\n\n # Filter out empty matches\n filtered_matches = [match for match in matches if match] # Remove empty matches\n\n # Return empty list for no matches, or list of matches if found\n result: list = [] if not filtered_matches else [Data(data={\"match\": match}) for match in filtered_matches]\n\n except re.error as e:\n error_message = f\"Invalid regex pattern: {e!s}\"\n result = [Data(data={\"error\": error_message})]\n except ValueError as e:\n error_message = f\"Error extracting matches: {e!s}\"\n result = [Data(data={\"error\": error_message})]\n\n self.status = result\n return result\n\n def get_matches_text(self) -> Message:\n \"\"\"Get matches as a formatted text message.\"\"\"\n matches = self.extract_matches()\n\n if not matches:\n message = Message(text=\"No matches found\")\n self.status = message\n return message\n\n if \"error\" in matches[0].data:\n message = Message(text=matches[0].data[\"error\"])\n self.status = message\n return message\n\n result = \"\\n\".join(match.data[\"match\"] for match in matches)\n message = Message(text=result)\n self.status = message\n return message\n" }, "input_text": { "_input_type": "MessageTextInput", @@ -102995,7 +103126,7 @@ }, "SelectData": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -103012,7 +103143,7 @@ "icon": "prototypes", "legacy": true, "metadata": { - "code_hash": "0512bd98ce4d", + "code_hash": "943bab86d962", "dependencies": { "dependencies": [ { @@ -103034,10 +103165,10 @@ "group_outputs": false, "method": "select_data", "name": "selected_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -103064,7 +103195,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import DataInput, IntInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass SelectDataComponent(Component):\n display_name: str = \"Select Data\"\n description: str = \"Select a single data from a list of data.\"\n name: str = \"SelectData\"\n icon = \"prototypes\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(\n name=\"data_list\",\n display_name=\"Data List\",\n info=\"List of data to select from.\",\n is_list=True, # Specify that this input takes a list of Data objects\n ),\n IntInput(\n name=\"data_index\",\n display_name=\"Data Index\",\n info=\"Index of the data to select.\",\n value=0, # Will be populated dynamically based on the length of data_list\n range_spec=RangeSpec(min=0, max=15, step=1, step_type=\"int\"),\n ),\n ]\n\n outputs = [\n Output(display_name=\"Selected Data\", name=\"selected_data\", method=\"select_data\"),\n ]\n\n async def select_data(self) -> Data:\n # Retrieve the selected index from the dropdown\n selected_index = int(self.data_index)\n # Get the data list\n\n # Validate that the selected index is within bounds\n if selected_index < 0 or selected_index >= len(self.data_list):\n msg = f\"Selected index {selected_index} is out of range.\"\n raise ValueError(msg)\n\n # Return the selected Data object\n selected_data = self.data_list[selected_index]\n self.status = selected_data # Update the component status to reflect the selected data\n return selected_data\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import DataInput, IntInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass SelectDataComponent(Component):\n display_name: str = \"Select Data\"\n description: str = \"Select a single data from a list of data.\"\n name: str = \"SelectData\"\n icon = \"prototypes\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(\n name=\"data_list\",\n display_name=\"JSON List\",\n info=\"List of data to select from.\",\n is_list=True, # Specify that this input takes a list of Data objects\n ),\n IntInput(\n name=\"data_index\",\n display_name=\"Data Index\",\n info=\"Index of the data to select.\",\n value=0, # Will be populated dynamically based on the length of data_list\n range_spec=RangeSpec(min=0, max=15, step=1, step_type=\"int\"),\n ),\n ]\n\n outputs = [\n Output(display_name=\"Selected Data\", name=\"selected_data\", method=\"select_data\"),\n ]\n\n async def select_data(self) -> Data:\n # Retrieve the selected index from the dropdown\n selected_index = int(self.data_index)\n # Get the data list\n\n # Validate that the selected index is within bounds\n if selected_index < 0 or selected_index >= len(self.data_list):\n msg = f\"Selected index {selected_index} is out of range.\"\n raise ValueError(msg)\n\n # Return the selected Data object\n selected_data = self.data_list[selected_index]\n self.status = selected_data # Update the component status to reflect the selected data\n return selected_data\n" }, "data_index": { "_input_type": "IntInput", @@ -103093,13 +103224,14 @@ "value": 0 }, "data_list": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, - "display_name": "Data List", + "display_name": "JSON List", "dynamic": false, "info": "List of data to select from.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -103121,7 +103253,7 @@ }, "SplitText": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -103143,7 +103275,7 @@ "icon": "scissors-line-dashed", "legacy": false, "metadata": { - "code_hash": "29ae597d2d86", + "code_hash": "859adebdf672", "dependencies": { "dependencies": [ { @@ -103169,10 +103301,10 @@ "group_outputs": false, "method": "split_text", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -103256,7 +103388,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from langchain_text_splitters import CharacterTextSplitter\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.util import unescape_string\n\n\nclass SplitTextComponent(Component):\n display_name: str = \"Split Text\"\n description: str = \"Split text into chunks based on specified criteria.\"\n documentation: str = \"https://docs.langflow.org/split-text\"\n icon = \"scissors-line-dashed\"\n name = \"SplitText\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Input\",\n info=\"The data with texts to split in chunks.\",\n input_types=[\"Data\", \"DataFrame\", \"Message\"],\n required=True,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"Number of characters to overlap between chunks.\",\n value=200,\n ),\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=(\n \"The maximum length of each chunk. Text is first split by separator, \"\n \"then chunks are merged up to this size. \"\n \"Individual splits larger than this won't be further divided.\"\n ),\n value=1000,\n ),\n MessageTextInput(\n name=\"separator\",\n display_name=\"Separator\",\n info=(\n \"The character to split on. Use \\\\n for newline. \"\n \"Examples: \\\\n\\\\n for paragraphs, \\\\n for lines, . for sentences\"\n ),\n value=\"\\n\",\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"The key to use for the text column.\",\n value=\"text\",\n advanced=True,\n ),\n DropdownInput(\n name=\"keep_separator\",\n display_name=\"Keep Separator\",\n info=\"Whether to keep the separator in the output chunks and where to place it.\",\n options=[\"False\", \"True\", \"Start\", \"End\"],\n value=\"False\",\n advanced=True,\n ),\n BoolInput(\n name=\"clean_output\",\n display_name=\"Clean Output\",\n info=\"When enabled, only the text column is included in the output. Metadata columns are removed.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Chunks\", name=\"dataframe\", method=\"split_text\"),\n ]\n\n def _docs_to_data(self, docs, *, clean: bool = False) -> list[Data]:\n return [\n Data(text=doc.page_content) if clean else Data(text=doc.page_content, data=doc.metadata) for doc in docs\n ]\n\n def _fix_separator(self, separator: str) -> str:\n \"\"\"Fix common separator issues and convert to proper format.\"\"\"\n if separator == \"/n\":\n return \"\\n\"\n if separator == \"/t\":\n return \"\\t\"\n return separator\n\n def split_text_base(self):\n separator = self._fix_separator(self.separator)\n separator = unescape_string(separator)\n\n if isinstance(self.data_inputs, DataFrame):\n if not len(self.data_inputs):\n msg = \"DataFrame is empty\"\n raise TypeError(msg)\n\n self.data_inputs.text_key = self.text_key\n try:\n documents = self.data_inputs.to_lc_documents()\n except Exception as e:\n msg = f\"Error converting DataFrame to documents: {e}\"\n raise TypeError(msg) from e\n elif isinstance(self.data_inputs, Message):\n self.data_inputs = [self.data_inputs.to_data()]\n return self.split_text_base()\n else:\n if not self.data_inputs:\n msg = \"No data inputs provided\"\n raise TypeError(msg)\n\n documents = []\n if isinstance(self.data_inputs, Data):\n self.data_inputs.text_key = self.text_key\n documents = [self.data_inputs.to_lc_document()]\n else:\n try:\n documents = [input_.to_lc_document() for input_ in self.data_inputs if isinstance(input_, Data)]\n if not documents:\n msg = f\"No valid Data inputs found in {type(self.data_inputs)}\"\n raise TypeError(msg)\n except AttributeError as e:\n msg = f\"Invalid input type in collection: {e}\"\n raise TypeError(msg) from e\n try:\n # Convert string 'False'/'True' to boolean\n keep_sep = self.keep_separator\n if isinstance(keep_sep, str):\n if keep_sep.lower() == \"false\":\n keep_sep = False\n elif keep_sep.lower() == \"true\":\n keep_sep = True\n # 'start' and 'end' are kept as strings\n\n splitter = CharacterTextSplitter(\n chunk_overlap=self.chunk_overlap,\n chunk_size=self.chunk_size,\n separator=separator,\n keep_separator=keep_sep,\n )\n return splitter.split_documents(documents)\n except Exception as e:\n msg = f\"Error splitting text: {e}\"\n raise TypeError(msg) from e\n\n def split_text(self) -> DataFrame:\n docs = self.split_text_base()\n df = DataFrame(self._docs_to_data(docs, clean=self.clean_output))\n return df if self.clean_output else df.smart_column_order()\n" + "value": "from langchain_text_splitters import CharacterTextSplitter\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.util import unescape_string\n\n\nclass SplitTextComponent(Component):\n display_name: str = \"Split Text\"\n description: str = \"Split text into chunks based on specified criteria.\"\n documentation: str = \"https://docs.langflow.org/split-text\"\n icon = \"scissors-line-dashed\"\n name = \"SplitText\"\n\n inputs = [\n HandleInput(\n name=\"data_inputs\",\n display_name=\"Input\",\n info=\"The data with texts to split in chunks.\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\", \"Message\"],\n required=True,\n ),\n IntInput(\n name=\"chunk_overlap\",\n display_name=\"Chunk Overlap\",\n info=\"Number of characters to overlap between chunks.\",\n value=200,\n ),\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n info=(\n \"The maximum length of each chunk. Text is first split by separator, \"\n \"then chunks are merged up to this size. \"\n \"Individual splits larger than this won't be further divided.\"\n ),\n value=1000,\n ),\n MessageTextInput(\n name=\"separator\",\n display_name=\"Separator\",\n info=(\n \"The character to split on. Use \\\\n for newline. \"\n \"Examples: \\\\n\\\\n for paragraphs, \\\\n for lines, . for sentences\"\n ),\n value=\"\\n\",\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"The key to use for the text column.\",\n value=\"text\",\n advanced=True,\n ),\n DropdownInput(\n name=\"keep_separator\",\n display_name=\"Keep Separator\",\n info=\"Whether to keep the separator in the output chunks and where to place it.\",\n options=[\"False\", \"True\", \"Start\", \"End\"],\n value=\"False\",\n advanced=True,\n ),\n BoolInput(\n name=\"clean_output\",\n display_name=\"Clean Output\",\n info=\"When enabled, only the text column is included in the output. Metadata columns are removed.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Chunks\", name=\"dataframe\", method=\"split_text\"),\n ]\n\n def _docs_to_data(self, docs, *, clean: bool = False) -> list[Data]:\n return [\n Data(text=doc.page_content) if clean else Data(text=doc.page_content, data=doc.metadata) for doc in docs\n ]\n\n def _fix_separator(self, separator: str) -> str:\n \"\"\"Fix common separator issues and convert to proper format.\"\"\"\n if separator == \"/n\":\n return \"\\n\"\n if separator == \"/t\":\n return \"\\t\"\n return separator\n\n def split_text_base(self):\n separator = self._fix_separator(self.separator)\n separator = unescape_string(separator)\n\n if isinstance(self.data_inputs, DataFrame):\n if not len(self.data_inputs):\n msg = \"DataFrame is empty\"\n raise TypeError(msg)\n\n self.data_inputs.text_key = self.text_key\n try:\n documents = self.data_inputs.to_lc_documents()\n except Exception as e:\n msg = f\"Error converting DataFrame to documents: {e}\"\n raise TypeError(msg) from e\n elif isinstance(self.data_inputs, Message):\n self.data_inputs = [self.data_inputs.to_data()]\n return self.split_text_base()\n else:\n if not self.data_inputs:\n msg = \"No data inputs provided\"\n raise TypeError(msg)\n\n documents = []\n if isinstance(self.data_inputs, Data):\n self.data_inputs.text_key = self.text_key\n documents = [self.data_inputs.to_lc_document()]\n else:\n try:\n documents = [input_.to_lc_document() for input_ in self.data_inputs if isinstance(input_, Data)]\n if not documents:\n msg = f\"No valid Data inputs found in {type(self.data_inputs)}\"\n raise TypeError(msg)\n except AttributeError as e:\n msg = f\"Invalid input type in collection: {e}\"\n raise TypeError(msg) from e\n try:\n # Convert string 'False'/'True' to boolean\n keep_sep = self.keep_separator\n if isinstance(keep_sep, str):\n if keep_sep.lower() == \"false\":\n keep_sep = False\n elif keep_sep.lower() == \"true\":\n keep_sep = True\n # 'start' and 'end' are kept as strings\n\n splitter = CharacterTextSplitter(\n chunk_overlap=self.chunk_overlap,\n chunk_size=self.chunk_size,\n separator=separator,\n keep_separator=keep_sep,\n )\n return splitter.split_documents(documents)\n except Exception as e:\n msg = f\"Error splitting text: {e}\"\n raise TypeError(msg) from e\n\n def split_text(self) -> DataFrame:\n docs = self.split_text_base()\n df = DataFrame(self._docs_to_data(docs, clean=self.clean_output))\n return df if self.clean_output else df.smart_column_order()\n" }, "data_inputs": { "_input_type": "HandleInput", @@ -103266,7 +103398,9 @@ "info": "The data with texts to split in chunks.", "input_types": [ "Data", + "JSON", "DataFrame", + "Table", "Message" ], "list": false, @@ -103602,7 +103736,7 @@ "icon": "type", "legacy": false, "metadata": { - "code_hash": "2c2991ef0a37", + "code_hash": "008b8a7b612e", "dependencies": { "dependencies": [ { @@ -103670,7 +103804,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import contextlib\nimport re\nfrom typing import Any\n\nimport pandas as pd\n\nfrom lfx.custom import Component\nfrom lfx.field_typing import RangeSpec\nfrom lfx.inputs import (\n BoolInput,\n DropdownInput,\n IntInput,\n SortableListInput,\n StrInput,\n)\nfrom lfx.inputs.inputs import MultilineInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\n\n\nclass TextOperations(Component):\n display_name = \"Text Operations\"\n description = \"Perform various text processing operations including text-to-DataFrame conversion.\"\n icon = \"type\"\n name = \"TextOperations\"\n\n # Configuration for operation-specific input fields\n OPERATION_FIELDS: dict[str, list[str]] = {\n \"Text to DataFrame\": [\"table_separator\", \"has_header\"],\n \"Word Count\": [\"count_words\", \"count_characters\", \"count_lines\"],\n \"Case Conversion\": [\"case_type\"],\n \"Text Replace\": [\"search_pattern\", \"replacement_text\", \"use_regex\"],\n \"Text Extract\": [\"extract_pattern\", \"max_matches\"],\n \"Text Head\": [\"head_characters\"],\n \"Text Tail\": [\"tail_characters\"],\n \"Text Strip\": [\"strip_mode\", \"strip_characters\"],\n \"Text Join\": [\"text_input_2\"],\n \"Text Clean\": [\"remove_extra_spaces\", \"remove_special_chars\", \"remove_empty_lines\"],\n }\n\n ALL_DYNAMIC_FIELDS: list[str] = [\n \"table_separator\",\n \"has_header\",\n \"count_words\",\n \"count_characters\",\n \"count_lines\",\n \"case_type\",\n \"search_pattern\",\n \"replacement_text\",\n \"use_regex\",\n \"extract_pattern\",\n \"max_matches\",\n \"head_characters\",\n \"tail_characters\",\n \"strip_mode\",\n \"strip_characters\",\n \"text_input_2\",\n \"remove_extra_spaces\",\n \"remove_special_chars\",\n \"remove_empty_lines\",\n ]\n\n CASE_CONVERTERS: dict[str, Any] = {\n \"uppercase\": str.upper,\n \"lowercase\": str.lower,\n \"title\": str.title,\n \"capitalize\": str.capitalize,\n \"swapcase\": str.swapcase,\n }\n\n inputs = [\n MultilineInput(\n name=\"text_input\",\n display_name=\"Text Input\",\n info=\"The input text to process.\",\n required=True,\n ),\n SortableListInput(\n name=\"operation\",\n display_name=\"Operation\",\n placeholder=\"Select Operation\",\n info=\"Select the text operation to perform.\",\n options=[\n {\"name\": \"Word Count\", \"icon\": \"hash\"},\n {\"name\": \"Case Conversion\", \"icon\": \"type\"},\n {\"name\": \"Text Replace\", \"icon\": \"replace\"},\n {\"name\": \"Text Extract\", \"icon\": \"search\"},\n {\"name\": \"Text Head\", \"icon\": \"chevron-left\"},\n {\"name\": \"Text Tail\", \"icon\": \"chevron-right\"},\n {\"name\": \"Text Strip\", \"icon\": \"minus\"},\n {\"name\": \"Text Join\", \"icon\": \"link\"},\n {\"name\": \"Text Clean\", \"icon\": \"sparkles\"},\n {\"name\": \"Text to DataFrame\", \"icon\": \"table\"},\n ],\n real_time_refresh=True,\n limit=1,\n ),\n StrInput(\n name=\"table_separator\",\n display_name=\"Table Separator\",\n info=\"Separator used in the table (default: '|').\",\n value=\"|\",\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"has_header\",\n display_name=\"Has Header\",\n info=\"Whether the table has a header row.\",\n value=True,\n dynamic=True,\n advanced=True,\n show=False,\n ),\n BoolInput(\n name=\"count_words\",\n display_name=\"Count Words\",\n info=\"Include word count in analysis.\",\n value=True,\n dynamic=True,\n advanced=True,\n show=False,\n ),\n BoolInput(\n name=\"count_characters\",\n display_name=\"Count Characters\",\n info=\"Include character count in analysis.\",\n value=True,\n dynamic=True,\n advanced=True,\n show=False,\n ),\n BoolInput(\n name=\"count_lines\",\n display_name=\"Count Lines\",\n info=\"Include line count in analysis.\",\n value=True,\n dynamic=True,\n advanced=True,\n show=False,\n ),\n DropdownInput(\n name=\"case_type\",\n display_name=\"Case Type\",\n options=[\"uppercase\", \"lowercase\", \"title\", \"capitalize\", \"swapcase\"],\n value=\"lowercase\",\n info=\"Type of case conversion to apply.\",\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"use_regex\",\n display_name=\"Use Regex\",\n info=\"Whether to treat search pattern as regex.\",\n value=False,\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"search_pattern\",\n display_name=\"Search Pattern\",\n info=\"Text pattern to search for (supports regex).\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"replacement_text\",\n display_name=\"Replacement Text\",\n info=\"Text to replace the search pattern with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"extract_pattern\",\n display_name=\"Extract Pattern\",\n info=\"Regex pattern to extract from text.\",\n dynamic=True,\n show=False,\n ),\n IntInput(\n name=\"max_matches\",\n display_name=\"Max Matches\",\n info=\"Maximum number of matches to extract.\",\n value=10,\n dynamic=True,\n show=False,\n ),\n IntInput(\n name=\"head_characters\",\n display_name=\"Characters from Start\",\n info=\"Number of characters to extract from the beginning of text. Must be non-negative.\",\n value=100,\n dynamic=True,\n show=False,\n range_spec=RangeSpec(min=0, max=1000000, step=1, step_type=\"int\"),\n ),\n IntInput(\n name=\"tail_characters\",\n display_name=\"Characters from End\",\n info=\"Number of characters to extract from the end of text. Must be non-negative.\",\n value=100,\n dynamic=True,\n show=False,\n range_spec=RangeSpec(min=0, max=1000000, step=1, step_type=\"int\"),\n ),\n DropdownInput(\n name=\"strip_mode\",\n display_name=\"Strip Mode\",\n options=[\"both\", \"left\", \"right\"],\n value=\"both\",\n info=\"Which sides to strip whitespace from.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"strip_characters\",\n display_name=\"Characters to Strip\",\n info=\"Specific characters to remove (leave empty for whitespace).\",\n value=\"\",\n dynamic=True,\n show=False,\n ),\n MultilineInput(\n name=\"text_input_2\",\n display_name=\"Second Text Input\",\n info=\"Second text to join with the first text.\",\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"remove_extra_spaces\",\n display_name=\"Remove Extra Spaces\",\n info=\"Remove multiple consecutive spaces.\",\n value=True,\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"remove_special_chars\",\n display_name=\"Remove Special Characters\",\n info=\"Remove special characters except alphanumeric and spaces.\",\n value=False,\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"remove_empty_lines\",\n display_name=\"Remove Empty Lines\",\n info=\"Remove empty lines from text.\",\n value=False,\n dynamic=True,\n show=False,\n ),\n ]\n\n outputs = []\n\n def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:\n \"\"\"Update build configuration to show/hide relevant inputs based on operation.\"\"\"\n for field in self.ALL_DYNAMIC_FIELDS:\n if field in build_config:\n build_config[field][\"show\"] = False\n\n if field_name != \"operation\":\n return build_config\n\n operation_name = self._extract_operation_name(field_value)\n if not operation_name:\n return build_config\n\n fields_to_show = self.OPERATION_FIELDS.get(operation_name, [])\n for field in fields_to_show:\n if field in build_config:\n build_config[field][\"show\"] = True\n\n return build_config\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Create dynamic outputs based on selected operation.\"\"\"\n if field_name != \"operation\":\n return frontend_node\n\n frontend_node[\"outputs\"] = []\n operation_name = self._extract_operation_name(field_value)\n\n if operation_name == \"Word Count\":\n frontend_node[\"outputs\"].append(Output(display_name=\"Data\", name=\"data\", method=\"get_data\"))\n elif operation_name == \"Text to DataFrame\":\n frontend_node[\"outputs\"].append(Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"get_dataframe\"))\n elif operation_name == \"Text Join\":\n frontend_node[\"outputs\"].append(Output(display_name=\"Text\", name=\"text\", method=\"get_text\"))\n frontend_node[\"outputs\"].append(Output(display_name=\"Message\", name=\"message\", method=\"get_message\"))\n elif operation_name:\n frontend_node[\"outputs\"].append(Output(display_name=\"Message\", name=\"message\", method=\"get_message\"))\n\n return frontend_node\n\n def _extract_operation_name(self, field_value: Any) -> str:\n \"\"\"Extract operation name from SortableListInput value.\"\"\"\n if isinstance(field_value, list) and len(field_value) > 0:\n return field_value[0].get(\"name\", \"\")\n return \"\"\n\n def get_operation_name(self) -> str:\n \"\"\"Get the selected operation name.\"\"\"\n operation_input = getattr(self, \"operation\", [])\n return self._extract_operation_name(operation_input)\n\n def process_text(self) -> Any:\n \"\"\"Process text based on selected operation.\"\"\"\n text = getattr(self, \"text_input\", \"\")\n operation = self.get_operation_name()\n\n # Allow empty text for Text Join (second input might have content)\n # and Word Count (should return zeros for empty text)\n if not text and operation not in (\"Text Join\", \"Word Count\"):\n return None\n operation_handlers = {\n \"Text to DataFrame\": self._text_to_dataframe,\n \"Word Count\": self._word_count,\n \"Case Conversion\": self._case_conversion,\n \"Text Replace\": self._text_replace,\n \"Text Extract\": self._text_extract,\n \"Text Head\": self._text_head,\n \"Text Tail\": self._text_tail,\n \"Text Strip\": self._text_strip,\n \"Text Join\": self._text_join,\n \"Text Clean\": self._text_clean,\n }\n\n handler = operation_handlers.get(operation)\n if handler:\n return handler(text)\n return text\n\n def _text_to_dataframe(self, text: str) -> DataFrame:\n \"\"\"Convert markdown-style table text to DataFrame.\"\"\"\n lines = [line.strip() for line in text.strip().split(\"\\n\") if line.strip()]\n if not lines:\n return DataFrame(pd.DataFrame())\n\n separator = getattr(self, \"table_separator\", \"|\")\n has_header = getattr(self, \"has_header\", True)\n\n rows = self._parse_table_rows(lines, separator)\n if not rows:\n return DataFrame(pd.DataFrame())\n\n df = self._create_dataframe(rows, has_header=has_header)\n self._convert_numeric_columns(df)\n\n self.log(f\"Converted text to DataFrame: {len(df)} rows, {len(df.columns)} columns\")\n return DataFrame(df)\n\n def _parse_table_rows(self, lines: list[str], separator: str) -> list[list[str]]:\n \"\"\"Parse table lines into rows of cells.\"\"\"\n rows = []\n for line in lines:\n cleaned_line = line.strip(separator)\n cells = [cell.strip() for cell in cleaned_line.split(separator)]\n rows.append(cells)\n return rows\n\n def _create_dataframe(self, rows: list[list[str]], *, has_header: bool) -> pd.DataFrame:\n \"\"\"Create DataFrame from parsed rows.\"\"\"\n if has_header and len(rows) > 1:\n header = rows[0]\n data_rows = rows[1:]\n header_col_count = len(header)\n\n # Validate that all data rows have the same number of columns as header\n for i, row in enumerate(data_rows):\n row_col_count = len(row)\n if row_col_count != header_col_count:\n msg = (\n f\"Header mismatch: {header_col_count} column(s) in header vs \"\n f\"{row_col_count} column(s) in data row {i + 1}. \"\n \"Please ensure the header has the same number of columns as your data.\"\n )\n raise ValueError(msg)\n\n return pd.DataFrame(data_rows, columns=header)\n\n max_cols = max(len(row) for row in rows) if rows else 0\n columns = [f\"col_{i}\" for i in range(max_cols)]\n return pd.DataFrame(rows, columns=columns)\n\n def _convert_numeric_columns(self, df: pd.DataFrame) -> None:\n \"\"\"Attempt to convert string columns to numeric where possible.\"\"\"\n for col in df.columns:\n with contextlib.suppress(ValueError, TypeError):\n df[col] = pd.to_numeric(df[col])\n\n def _word_count(self, text: str) -> dict[str, Any]:\n \"\"\"Count words, characters, and lines in text.\"\"\"\n result: dict[str, Any] = {}\n\n # Handle empty or whitespace-only text - return zeros\n text_str = str(text) if text else \"\"\n is_empty = not text_str or not text_str.strip()\n\n if getattr(self, \"count_words\", True):\n if is_empty:\n result[\"word_count\"] = 0\n result[\"unique_words\"] = 0\n else:\n words = text_str.split()\n result[\"word_count\"] = len(words)\n result[\"unique_words\"] = len(set(words))\n\n if getattr(self, \"count_characters\", True):\n if is_empty:\n result[\"character_count\"] = 0\n result[\"character_count_no_spaces\"] = 0\n else:\n result[\"character_count\"] = len(text_str)\n result[\"character_count_no_spaces\"] = len(text_str.replace(\" \", \"\"))\n\n if getattr(self, \"count_lines\", True):\n if is_empty:\n result[\"line_count\"] = 0\n result[\"non_empty_lines\"] = 0\n else:\n lines = text_str.split(\"\\n\")\n result[\"line_count\"] = len(lines)\n result[\"non_empty_lines\"] = len([line for line in lines if line.strip()])\n\n return result\n\n def _case_conversion(self, text: str) -> str:\n \"\"\"Convert text case.\"\"\"\n case_type = getattr(self, \"case_type\", \"lowercase\")\n converter = self.CASE_CONVERTERS.get(case_type)\n return converter(text) if converter else text\n\n def _text_replace(self, text: str) -> str:\n \"\"\"Replace text patterns.\"\"\"\n search_pattern = getattr(self, \"search_pattern\", \"\")\n if not search_pattern:\n return text\n\n replacement_text = getattr(self, \"replacement_text\", \"\")\n use_regex = getattr(self, \"use_regex\", False)\n\n if use_regex:\n try:\n return re.sub(search_pattern, replacement_text, text)\n except re.error as e:\n self.log(f\"Invalid regex pattern: {e}\")\n return text\n\n return text.replace(search_pattern, replacement_text)\n\n def _text_extract(self, text: str) -> list[str]:\n \"\"\"Extract text matching patterns.\"\"\"\n extract_pattern = getattr(self, \"extract_pattern\", \"\")\n if not extract_pattern:\n return []\n\n max_matches = getattr(self, \"max_matches\", 10)\n\n try:\n matches = re.findall(extract_pattern, text)\n except re.error as e:\n msg = f\"Invalid regex pattern '{extract_pattern}': {e}\"\n raise ValueError(msg) from e\n\n return matches[:max_matches] if max_matches > 0 else matches\n\n def _text_head(self, text: str) -> str:\n \"\"\"Extract characters from the beginning of text.\"\"\"\n head_characters = getattr(self, \"head_characters\", 100)\n if head_characters < 0:\n msg = f\"Characters from Start must be a non-negative integer, got {head_characters}\"\n raise ValueError(msg)\n if head_characters == 0:\n return \"\"\n return text[:head_characters]\n\n def _text_tail(self, text: str) -> str:\n \"\"\"Extract characters from the end of text.\"\"\"\n tail_characters = getattr(self, \"tail_characters\", 100)\n if tail_characters < 0:\n msg = f\"Characters from End must be a non-negative integer, got {tail_characters}\"\n raise ValueError(msg)\n if tail_characters == 0:\n return \"\"\n return text[-tail_characters:]\n\n def _text_strip(self, text: str) -> str:\n \"\"\"Remove whitespace or specific characters from text edges.\"\"\"\n strip_mode = getattr(self, \"strip_mode\", \"both\")\n strip_characters = getattr(self, \"strip_characters\", \"\")\n\n # Convert to string to ensure proper handling\n text_str = str(text) if text else \"\"\n\n # None means strip all whitespace (spaces, tabs, newlines, etc.)\n chars_to_strip = strip_characters if strip_characters else None\n\n if strip_mode == \"left\":\n return text_str.lstrip(chars_to_strip)\n if strip_mode == \"right\":\n return text_str.rstrip(chars_to_strip)\n # Default: \"both\"\n return text_str.strip(chars_to_strip)\n\n def _text_join(self, text: str) -> str:\n \"\"\"Join two texts with line break separator.\"\"\"\n text_input_2 = getattr(self, \"text_input_2\", \"\")\n\n text1 = str(text) if text else \"\"\n text2 = str(text_input_2) if text_input_2 else \"\"\n\n if text1 and text2:\n return f\"{text1}\\n{text2}\"\n return text1 or text2\n\n def _text_clean(self, text: str) -> str:\n \"\"\"Clean text by removing extra spaces, special chars, etc.\"\"\"\n result = text\n\n if getattr(self, \"remove_extra_spaces\", True):\n result = re.sub(r\"\\s+\", \" \", result)\n\n if getattr(self, \"remove_special_chars\", False):\n # Remove ALL special characters except alphanumeric and spaces\n result = re.sub(r\"[^\\w\\s]\", \"\", result)\n\n if getattr(self, \"remove_empty_lines\", False):\n lines = [line for line in result.split(\"\\n\") if line.strip()]\n result = \"\\n\".join(lines)\n\n return result\n\n def _format_result_as_text(self, result: Any) -> str:\n \"\"\"Format result as text string.\"\"\"\n if result is None:\n return \"\"\n if isinstance(result, list):\n return \"\\n\".join(str(item) for item in result)\n return str(result)\n\n def get_dataframe(self) -> DataFrame:\n \"\"\"Return result as DataFrame - only for Text to DataFrame operation.\"\"\"\n if self.get_operation_name() != \"Text to DataFrame\":\n return DataFrame(pd.DataFrame())\n\n text = getattr(self, \"text_input\", \"\")\n if not text:\n return DataFrame(pd.DataFrame())\n\n return self._text_to_dataframe(text)\n\n def get_text(self) -> Message:\n \"\"\"Return result as Message - for text operations only.\"\"\"\n result = self.process_text()\n return Message(text=self._format_result_as_text(result))\n\n def get_data(self) -> Data:\n \"\"\"Return result as Data object - only for Word Count operation.\"\"\"\n if self.get_operation_name() != \"Word Count\":\n return Data(data={})\n\n result = self.process_text()\n if result is None:\n return Data(data={})\n\n if isinstance(result, dict):\n return Data(data=result)\n if isinstance(result, list):\n return Data(data={\"items\": result})\n return Data(data={\"result\": str(result)})\n\n def get_message(self) -> Message:\n \"\"\"Return result as simple message with the processed text.\"\"\"\n result = self.process_text()\n return Message(text=self._format_result_as_text(result))\n" + "value": "import contextlib\nimport re\nfrom typing import Any\n\nimport pandas as pd\n\nfrom lfx.custom import Component\nfrom lfx.field_typing import RangeSpec\nfrom lfx.inputs import (\n BoolInput,\n DropdownInput,\n IntInput,\n SortableListInput,\n StrInput,\n)\nfrom lfx.inputs.inputs import MultilineInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\n\n\nclass TextOperations(Component):\n display_name = \"Text Operations\"\n description = \"Perform various text processing operations including text-to-DataFrame conversion.\"\n icon = \"type\"\n name = \"TextOperations\"\n\n # Configuration for operation-specific input fields\n OPERATION_FIELDS: dict[str, list[str]] = {\n \"Text to DataFrame\": [\"table_separator\", \"has_header\"],\n \"Word Count\": [\"count_words\", \"count_characters\", \"count_lines\"],\n \"Case Conversion\": [\"case_type\"],\n \"Text Replace\": [\"search_pattern\", \"replacement_text\", \"use_regex\"],\n \"Text Extract\": [\"extract_pattern\", \"max_matches\"],\n \"Text Head\": [\"head_characters\"],\n \"Text Tail\": [\"tail_characters\"],\n \"Text Strip\": [\"strip_mode\", \"strip_characters\"],\n \"Text Join\": [\"text_input_2\"],\n \"Text Clean\": [\"remove_extra_spaces\", \"remove_special_chars\", \"remove_empty_lines\"],\n }\n\n ALL_DYNAMIC_FIELDS: list[str] = [\n \"table_separator\",\n \"has_header\",\n \"count_words\",\n \"count_characters\",\n \"count_lines\",\n \"case_type\",\n \"search_pattern\",\n \"replacement_text\",\n \"use_regex\",\n \"extract_pattern\",\n \"max_matches\",\n \"head_characters\",\n \"tail_characters\",\n \"strip_mode\",\n \"strip_characters\",\n \"text_input_2\",\n \"remove_extra_spaces\",\n \"remove_special_chars\",\n \"remove_empty_lines\",\n ]\n\n CASE_CONVERTERS: dict[str, Any] = {\n \"uppercase\": str.upper,\n \"lowercase\": str.lower,\n \"title\": str.title,\n \"capitalize\": str.capitalize,\n \"swapcase\": str.swapcase,\n }\n\n inputs = [\n MultilineInput(\n name=\"text_input\",\n display_name=\"Text Input\",\n info=\"The input text to process.\",\n required=True,\n ),\n SortableListInput(\n name=\"operation\",\n display_name=\"Operation\",\n placeholder=\"Select Operation\",\n info=\"Select the text operation to perform.\",\n options=[\n {\"name\": \"Word Count\", \"icon\": \"hash\"},\n {\"name\": \"Case Conversion\", \"icon\": \"type\"},\n {\"name\": \"Text Replace\", \"icon\": \"replace\"},\n {\"name\": \"Text Extract\", \"icon\": \"search\"},\n {\"name\": \"Text Head\", \"icon\": \"chevron-left\"},\n {\"name\": \"Text Tail\", \"icon\": \"chevron-right\"},\n {\"name\": \"Text Strip\", \"icon\": \"minus\"},\n {\"name\": \"Text Join\", \"icon\": \"link\"},\n {\"name\": \"Text Clean\", \"icon\": \"sparkles\"},\n {\"name\": \"Text to DataFrame\", \"icon\": \"table\"},\n ],\n real_time_refresh=True,\n limit=1,\n ),\n StrInput(\n name=\"table_separator\",\n display_name=\"Table Separator\",\n info=\"Separator used in the table (default: '|').\",\n value=\"|\",\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"has_header\",\n display_name=\"Has Header\",\n info=\"Whether the table has a header row.\",\n value=True,\n dynamic=True,\n advanced=True,\n show=False,\n ),\n BoolInput(\n name=\"count_words\",\n display_name=\"Count Words\",\n info=\"Include word count in analysis.\",\n value=True,\n dynamic=True,\n advanced=True,\n show=False,\n ),\n BoolInput(\n name=\"count_characters\",\n display_name=\"Count Characters\",\n info=\"Include character count in analysis.\",\n value=True,\n dynamic=True,\n advanced=True,\n show=False,\n ),\n BoolInput(\n name=\"count_lines\",\n display_name=\"Count Lines\",\n info=\"Include line count in analysis.\",\n value=True,\n dynamic=True,\n advanced=True,\n show=False,\n ),\n DropdownInput(\n name=\"case_type\",\n display_name=\"Case Type\",\n options=[\"uppercase\", \"lowercase\", \"title\", \"capitalize\", \"swapcase\"],\n value=\"lowercase\",\n info=\"Type of case conversion to apply.\",\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"use_regex\",\n display_name=\"Use Regex\",\n info=\"Whether to treat search pattern as regex.\",\n value=False,\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"search_pattern\",\n display_name=\"Search Pattern\",\n info=\"Text pattern to search for (supports regex).\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"replacement_text\",\n display_name=\"Replacement Text\",\n info=\"Text to replace the search pattern with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"extract_pattern\",\n display_name=\"Extract Pattern\",\n info=\"Regex pattern to extract from text.\",\n dynamic=True,\n show=False,\n ),\n IntInput(\n name=\"max_matches\",\n display_name=\"Max Matches\",\n info=\"Maximum number of matches to extract.\",\n value=10,\n dynamic=True,\n show=False,\n ),\n IntInput(\n name=\"head_characters\",\n display_name=\"Characters from Start\",\n info=\"Number of characters to extract from the beginning of text. Must be non-negative.\",\n value=100,\n dynamic=True,\n show=False,\n range_spec=RangeSpec(min=0, max=1000000, step=1, step_type=\"int\"),\n ),\n IntInput(\n name=\"tail_characters\",\n display_name=\"Characters from End\",\n info=\"Number of characters to extract from the end of text. Must be non-negative.\",\n value=100,\n dynamic=True,\n show=False,\n range_spec=RangeSpec(min=0, max=1000000, step=1, step_type=\"int\"),\n ),\n DropdownInput(\n name=\"strip_mode\",\n display_name=\"Strip Mode\",\n options=[\"both\", \"left\", \"right\"],\n value=\"both\",\n info=\"Which sides to strip whitespace from.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"strip_characters\",\n display_name=\"Characters to Strip\",\n info=\"Specific characters to remove (leave empty for whitespace).\",\n value=\"\",\n dynamic=True,\n show=False,\n ),\n MultilineInput(\n name=\"text_input_2\",\n display_name=\"Second Text Input\",\n info=\"Second text to join with the first text.\",\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"remove_extra_spaces\",\n display_name=\"Remove Extra Spaces\",\n info=\"Remove multiple consecutive spaces.\",\n value=True,\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"remove_special_chars\",\n display_name=\"Remove Special Characters\",\n info=\"Remove special characters except alphanumeric and spaces.\",\n value=False,\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"remove_empty_lines\",\n display_name=\"Remove Empty Lines\",\n info=\"Remove empty lines from text.\",\n value=False,\n dynamic=True,\n show=False,\n ),\n ]\n\n outputs = []\n\n def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:\n \"\"\"Update build configuration to show/hide relevant inputs based on operation.\"\"\"\n for field in self.ALL_DYNAMIC_FIELDS:\n if field in build_config:\n build_config[field][\"show\"] = False\n\n if field_name != \"operation\":\n return build_config\n\n operation_name = self._extract_operation_name(field_value)\n if not operation_name:\n return build_config\n\n fields_to_show = self.OPERATION_FIELDS.get(operation_name, [])\n for field in fields_to_show:\n if field in build_config:\n build_config[field][\"show\"] = True\n\n return build_config\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Create dynamic outputs based on selected operation.\"\"\"\n if field_name != \"operation\":\n return frontend_node\n\n frontend_node[\"outputs\"] = []\n operation_name = self._extract_operation_name(field_value)\n\n if operation_name == \"Word Count\":\n frontend_node[\"outputs\"].append(Output(display_name=\"JSON\", name=\"data\", method=\"get_data\"))\n elif operation_name == \"Text to DataFrame\":\n frontend_node[\"outputs\"].append(Output(display_name=\"Table\", name=\"dataframe\", method=\"get_dataframe\"))\n elif operation_name == \"Text Join\":\n frontend_node[\"outputs\"].append(Output(display_name=\"Text\", name=\"text\", method=\"get_text\"))\n frontend_node[\"outputs\"].append(Output(display_name=\"Message\", name=\"message\", method=\"get_message\"))\n elif operation_name:\n frontend_node[\"outputs\"].append(Output(display_name=\"Message\", name=\"message\", method=\"get_message\"))\n\n return frontend_node\n\n def _extract_operation_name(self, field_value: Any) -> str:\n \"\"\"Extract operation name from SortableListInput value.\"\"\"\n if isinstance(field_value, list) and len(field_value) > 0:\n return field_value[0].get(\"name\", \"\")\n return \"\"\n\n def get_operation_name(self) -> str:\n \"\"\"Get the selected operation name.\"\"\"\n operation_input = getattr(self, \"operation\", [])\n return self._extract_operation_name(operation_input)\n\n def process_text(self) -> Any:\n \"\"\"Process text based on selected operation.\"\"\"\n text = getattr(self, \"text_input\", \"\")\n operation = self.get_operation_name()\n\n # Allow empty text for Text Join (second input might have content)\n # and Word Count (should return zeros for empty text)\n if not text and operation not in (\"Text Join\", \"Word Count\"):\n return None\n operation_handlers = {\n \"Text to DataFrame\": self._text_to_dataframe,\n \"Word Count\": self._word_count,\n \"Case Conversion\": self._case_conversion,\n \"Text Replace\": self._text_replace,\n \"Text Extract\": self._text_extract,\n \"Text Head\": self._text_head,\n \"Text Tail\": self._text_tail,\n \"Text Strip\": self._text_strip,\n \"Text Join\": self._text_join,\n \"Text Clean\": self._text_clean,\n }\n\n handler = operation_handlers.get(operation)\n if handler:\n return handler(text)\n return text\n\n def _text_to_dataframe(self, text: str) -> DataFrame:\n \"\"\"Convert markdown-style table text to DataFrame.\"\"\"\n lines = [line.strip() for line in text.strip().split(\"\\n\") if line.strip()]\n if not lines:\n return DataFrame(pd.DataFrame())\n\n separator = getattr(self, \"table_separator\", \"|\")\n has_header = getattr(self, \"has_header\", True)\n\n rows = self._parse_table_rows(lines, separator)\n if not rows:\n return DataFrame(pd.DataFrame())\n\n df = self._create_dataframe(rows, has_header=has_header)\n self._convert_numeric_columns(df)\n\n self.log(f\"Converted text to DataFrame: {len(df)} rows, {len(df.columns)} columns\")\n return DataFrame(df)\n\n def _parse_table_rows(self, lines: list[str], separator: str) -> list[list[str]]:\n \"\"\"Parse table lines into rows of cells.\"\"\"\n rows = []\n for line in lines:\n cleaned_line = line.strip(separator)\n cells = [cell.strip() for cell in cleaned_line.split(separator)]\n rows.append(cells)\n return rows\n\n def _create_dataframe(self, rows: list[list[str]], *, has_header: bool) -> pd.DataFrame:\n \"\"\"Create DataFrame from parsed rows.\"\"\"\n if has_header and len(rows) > 1:\n header = rows[0]\n data_rows = rows[1:]\n header_col_count = len(header)\n\n # Validate that all data rows have the same number of columns as header\n for i, row in enumerate(data_rows):\n row_col_count = len(row)\n if row_col_count != header_col_count:\n msg = (\n f\"Header mismatch: {header_col_count} column(s) in header vs \"\n f\"{row_col_count} column(s) in data row {i + 1}. \"\n \"Please ensure the header has the same number of columns as your data.\"\n )\n raise ValueError(msg)\n\n return pd.DataFrame(data_rows, columns=header)\n\n max_cols = max(len(row) for row in rows) if rows else 0\n columns = [f\"col_{i}\" for i in range(max_cols)]\n return pd.DataFrame(rows, columns=columns)\n\n def _convert_numeric_columns(self, df: pd.DataFrame) -> None:\n \"\"\"Attempt to convert string columns to numeric where possible.\"\"\"\n for col in df.columns:\n with contextlib.suppress(ValueError, TypeError):\n df[col] = pd.to_numeric(df[col])\n\n def _word_count(self, text: str) -> dict[str, Any]:\n \"\"\"Count words, characters, and lines in text.\"\"\"\n result: dict[str, Any] = {}\n\n # Handle empty or whitespace-only text - return zeros\n text_str = str(text) if text else \"\"\n is_empty = not text_str or not text_str.strip()\n\n if getattr(self, \"count_words\", True):\n if is_empty:\n result[\"word_count\"] = 0\n result[\"unique_words\"] = 0\n else:\n words = text_str.split()\n result[\"word_count\"] = len(words)\n result[\"unique_words\"] = len(set(words))\n\n if getattr(self, \"count_characters\", True):\n if is_empty:\n result[\"character_count\"] = 0\n result[\"character_count_no_spaces\"] = 0\n else:\n result[\"character_count\"] = len(text_str)\n result[\"character_count_no_spaces\"] = len(text_str.replace(\" \", \"\"))\n\n if getattr(self, \"count_lines\", True):\n if is_empty:\n result[\"line_count\"] = 0\n result[\"non_empty_lines\"] = 0\n else:\n lines = text_str.split(\"\\n\")\n result[\"line_count\"] = len(lines)\n result[\"non_empty_lines\"] = len([line for line in lines if line.strip()])\n\n return result\n\n def _case_conversion(self, text: str) -> str:\n \"\"\"Convert text case.\"\"\"\n case_type = getattr(self, \"case_type\", \"lowercase\")\n converter = self.CASE_CONVERTERS.get(case_type)\n return converter(text) if converter else text\n\n def _text_replace(self, text: str) -> str:\n \"\"\"Replace text patterns.\"\"\"\n search_pattern = getattr(self, \"search_pattern\", \"\")\n if not search_pattern:\n return text\n\n replacement_text = getattr(self, \"replacement_text\", \"\")\n use_regex = getattr(self, \"use_regex\", False)\n\n if use_regex:\n try:\n return re.sub(search_pattern, replacement_text, text)\n except re.error as e:\n self.log(f\"Invalid regex pattern: {e}\")\n return text\n\n return text.replace(search_pattern, replacement_text)\n\n def _text_extract(self, text: str) -> list[str]:\n \"\"\"Extract text matching patterns.\"\"\"\n extract_pattern = getattr(self, \"extract_pattern\", \"\")\n if not extract_pattern:\n return []\n\n max_matches = getattr(self, \"max_matches\", 10)\n\n try:\n matches = re.findall(extract_pattern, text)\n except re.error as e:\n msg = f\"Invalid regex pattern '{extract_pattern}': {e}\"\n raise ValueError(msg) from e\n\n return matches[:max_matches] if max_matches > 0 else matches\n\n def _text_head(self, text: str) -> str:\n \"\"\"Extract characters from the beginning of text.\"\"\"\n head_characters = getattr(self, \"head_characters\", 100)\n if head_characters < 0:\n msg = f\"Characters from Start must be a non-negative integer, got {head_characters}\"\n raise ValueError(msg)\n if head_characters == 0:\n return \"\"\n return text[:head_characters]\n\n def _text_tail(self, text: str) -> str:\n \"\"\"Extract characters from the end of text.\"\"\"\n tail_characters = getattr(self, \"tail_characters\", 100)\n if tail_characters < 0:\n msg = f\"Characters from End must be a non-negative integer, got {tail_characters}\"\n raise ValueError(msg)\n if tail_characters == 0:\n return \"\"\n return text[-tail_characters:]\n\n def _text_strip(self, text: str) -> str:\n \"\"\"Remove whitespace or specific characters from text edges.\"\"\"\n strip_mode = getattr(self, \"strip_mode\", \"both\")\n strip_characters = getattr(self, \"strip_characters\", \"\")\n\n # Convert to string to ensure proper handling\n text_str = str(text) if text else \"\"\n\n # None means strip all whitespace (spaces, tabs, newlines, etc.)\n chars_to_strip = strip_characters if strip_characters else None\n\n if strip_mode == \"left\":\n return text_str.lstrip(chars_to_strip)\n if strip_mode == \"right\":\n return text_str.rstrip(chars_to_strip)\n # Default: \"both\"\n return text_str.strip(chars_to_strip)\n\n def _text_join(self, text: str) -> str:\n \"\"\"Join two texts with line break separator.\"\"\"\n text_input_2 = getattr(self, \"text_input_2\", \"\")\n\n text1 = str(text) if text else \"\"\n text2 = str(text_input_2) if text_input_2 else \"\"\n\n if text1 and text2:\n return f\"{text1}\\n{text2}\"\n return text1 or text2\n\n def _text_clean(self, text: str) -> str:\n \"\"\"Clean text by removing extra spaces, special chars, etc.\"\"\"\n result = text\n\n if getattr(self, \"remove_extra_spaces\", True):\n result = re.sub(r\"\\s+\", \" \", result)\n\n if getattr(self, \"remove_special_chars\", False):\n # Remove ALL special characters except alphanumeric and spaces\n result = re.sub(r\"[^\\w\\s]\", \"\", result)\n\n if getattr(self, \"remove_empty_lines\", False):\n lines = [line for line in result.split(\"\\n\") if line.strip()]\n result = \"\\n\".join(lines)\n\n return result\n\n def _format_result_as_text(self, result: Any) -> str:\n \"\"\"Format result as text string.\"\"\"\n if result is None:\n return \"\"\n if isinstance(result, list):\n return \"\\n\".join(str(item) for item in result)\n return str(result)\n\n def get_dataframe(self) -> DataFrame:\n \"\"\"Return result as DataFrame - only for Text to DataFrame operation.\"\"\"\n if self.get_operation_name() != \"Text to DataFrame\":\n return DataFrame(pd.DataFrame())\n\n text = getattr(self, \"text_input\", \"\")\n if not text:\n return DataFrame(pd.DataFrame())\n\n return self._text_to_dataframe(text)\n\n def get_text(self) -> Message:\n \"\"\"Return result as Message - for text operations only.\"\"\"\n result = self.process_text()\n return Message(text=self._format_result_as_text(result))\n\n def get_data(self) -> Data:\n \"\"\"Return result as Data object - only for Word Count operation.\"\"\"\n if self.get_operation_name() != \"Word Count\":\n return Data(data={})\n\n result = self.process_text()\n if result is None:\n return Data(data={})\n\n if isinstance(result, dict):\n return Data(data=result)\n if isinstance(result, list):\n return Data(data={\"items\": result})\n return Data(data={\"result\": str(result)})\n\n def get_message(self) -> Message:\n \"\"\"Return result as simple message with the processed text.\"\"\"\n result = self.process_text()\n return Message(text=self._format_result_as_text(result))\n" }, "count_characters": { "_input_type": "BoolInput", @@ -104163,12 +104297,14 @@ }, "TypeConverterComponent": { "base_classes": [ - "Message" + "JSON", + "Message", + "Table" ], "beta": false, "conditional_paths": [], "custom_fields": {}, - "description": "Convert between different types (Message, Data, DataFrame)", + "description": "Convert between different types (Message, JSON, Table)", "display_name": "Type Convert", "documentation": "https://docs.langflow.org/type-convert", "edited": false, @@ -104181,7 +104317,7 @@ "icon": "repeat", "legacy": false, "metadata": { - "code_hash": "be7797f8df1c", + "code_hash": "6ce26e994c2d", "dependencies": { "dependencies": [ { @@ -104213,6 +104349,34 @@ "Message" ], "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "JSON Output", + "group_outputs": false, + "method": "convert_to_data", + "name": "data_output", + "selected": "JSON", + "tool_mode": true, + "types": [ + "JSON" + ], + "value": "__UNDEFINED__" + }, + { + "allows_loop": false, + "cache": true, + "display_name": "Table Output", + "group_outputs": false, + "method": "convert_to_dataframe", + "name": "dataframe_output", + "selected": "Table", + "tool_mode": true, + "types": [ + "Table" + ], + "value": "__UNDEFINED__" } ], "pinned": false, @@ -104254,18 +104418,20 @@ "show": true, "title_case": false, "type": "code", - "value": "import json\nfrom typing import Any\n\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, HandleInput, Output, TabInput\nfrom lfx.schema import Data, DataFrame, Message\n\nMIN_CSV_LINES = 2\n\n\ndef convert_to_message(v) -> Message:\n \"\"\"Convert input to Message type.\n\n Args:\n v: Input to convert (Message, Data, DataFrame, or dict)\n\n Returns:\n Message: Converted Message object\n \"\"\"\n return v if isinstance(v, Message) else v.to_message()\n\n\ndef convert_to_data(v: DataFrame | Data | Message | dict, *, auto_parse: bool) -> Data:\n \"\"\"Convert input to Data type.\n\n Args:\n v: Input to convert (Message, Data, DataFrame, or dict)\n auto_parse: Enable automatic parsing of structured data (JSON/CSV)\n\n Returns:\n Data: Converted Data object\n \"\"\"\n if isinstance(v, dict):\n return Data(v)\n if isinstance(v, Message):\n data = Data(data={\"text\": v.data[\"text\"]})\n return parse_structured_data(data) if auto_parse else data\n\n return v if isinstance(v, Data) else v.to_data()\n\n\ndef convert_to_dataframe(v: DataFrame | Data | Message | dict, *, auto_parse: bool) -> DataFrame:\n \"\"\"Convert input to DataFrame type.\n\n Args:\n v: Input to convert (Message, Data, DataFrame, or dict)\n auto_parse: Enable automatic parsing of structured data (JSON/CSV)\n\n Returns:\n DataFrame: Converted DataFrame object\n \"\"\"\n import pandas as pd\n\n if isinstance(v, dict):\n return DataFrame([v])\n if isinstance(v, DataFrame):\n return v\n # Handle pandas DataFrame\n if isinstance(v, pd.DataFrame):\n # Convert pandas DataFrame to our DataFrame by creating Data objects\n return DataFrame(data=v)\n\n if isinstance(v, Message):\n data = Data(data={\"text\": v.data[\"text\"]})\n return parse_structured_data(data).to_dataframe() if auto_parse else data.to_dataframe()\n # For other types, call to_dataframe method\n return v.to_dataframe()\n\n\ndef parse_structured_data(data: Data) -> Data:\n \"\"\"Parse structured data (JSON, CSV) from Data's text field.\n\n Args:\n data: Data object with text content to parse\n\n Returns:\n Data: Modified Data object with parsed content or original if parsing fails\n \"\"\"\n raw_text = data.get_text() or \"\"\n text = raw_text.lstrip(\"\\ufeff\").strip()\n\n # Try JSON parsing first\n parsed_json = _try_parse_json(text)\n if parsed_json is not None:\n return parsed_json\n\n # Try CSV parsing\n if _looks_like_csv(text):\n try:\n return _parse_csv_to_data(text)\n except Exception: # noqa: BLE001\n # Heuristic misfire or malformed CSV — keep original data\n return data\n\n # Return original data if no parsing succeeded\n return data\n\n\ndef _try_parse_json(text: str) -> Data | None:\n \"\"\"Try to parse text as JSON and return Data object.\"\"\"\n try:\n parsed = json.loads(text)\n\n if isinstance(parsed, dict):\n # Single JSON object\n return Data(data=parsed)\n if isinstance(parsed, list) and all(isinstance(item, dict) for item in parsed):\n # Array of JSON objects - create Data with the list\n return Data(data={\"records\": parsed})\n\n except (json.JSONDecodeError, ValueError):\n pass\n\n return None\n\n\ndef _looks_like_csv(text: str) -> bool:\n \"\"\"Simple heuristic to detect CSV content.\"\"\"\n lines = text.strip().split(\"\\n\")\n if len(lines) < MIN_CSV_LINES:\n return False\n\n header_line = lines[0]\n return \",\" in header_line and len(lines) > 1\n\n\ndef _parse_csv_to_data(text: str) -> Data:\n \"\"\"Parse CSV text and return Data object.\"\"\"\n from io import StringIO\n\n import pandas as pd\n\n # Parse CSV to DataFrame, then convert to list of dicts\n parsed_df = pd.read_csv(StringIO(text))\n records = parsed_df.to_dict(orient=\"records\")\n\n return Data(data={\"records\": records})\n\n\nclass TypeConverterComponent(Component):\n display_name = \"Type Convert\"\n description = \"Convert between different types (Message, Data, DataFrame)\"\n documentation: str = \"https://docs.langflow.org/type-convert\"\n icon = \"repeat\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Input\",\n input_types=[\"Message\", \"Data\", \"DataFrame\"],\n info=\"Accept Message, Data or DataFrame as input\",\n required=True,\n ),\n BoolInput(\n name=\"auto_parse\",\n display_name=\"Auto Parse\",\n info=\"Detect and convert JSON/CSV strings automatically.\",\n advanced=True,\n value=False,\n required=False,\n ),\n TabInput(\n name=\"output_type\",\n display_name=\"Output Type\",\n options=[\"Message\", \"Data\", \"DataFrame\"],\n info=\"Select the desired output data type\",\n real_time_refresh=True,\n value=\"Message\",\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Message Output\",\n name=\"message_output\",\n method=\"convert_to_message\",\n )\n ]\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"output_type\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n\n # Add only the selected output type\n if field_value == \"Message\":\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"Message Output\",\n name=\"message_output\",\n method=\"convert_to_message\",\n ).to_dict()\n )\n elif field_value == \"Data\":\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"Data Output\",\n name=\"data_output\",\n method=\"convert_to_data\",\n ).to_dict()\n )\n elif field_value == \"DataFrame\":\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"DataFrame Output\",\n name=\"dataframe_output\",\n method=\"convert_to_dataframe\",\n ).to_dict()\n )\n\n return frontend_node\n\n def convert_to_message(self) -> Message:\n \"\"\"Convert input to Message type.\"\"\"\n input_value = self.input_data[0] if isinstance(self.input_data, list) else self.input_data\n\n # Handle string input by converting to Message first\n if isinstance(input_value, str):\n input_value = Message(text=input_value)\n\n result = convert_to_message(input_value)\n self.status = result\n return result\n\n def convert_to_data(self) -> Data:\n \"\"\"Convert input to Data type.\"\"\"\n input_value = self.input_data[0] if isinstance(self.input_data, list) else self.input_data\n\n # Handle string input by converting to Message first\n if isinstance(input_value, str):\n input_value = Message(text=input_value)\n\n result = convert_to_data(input_value, auto_parse=self.auto_parse)\n self.status = result\n return result\n\n def convert_to_dataframe(self) -> DataFrame:\n \"\"\"Convert input to DataFrame type.\"\"\"\n input_value = self.input_data[0] if isinstance(self.input_data, list) else self.input_data\n\n # Handle string input by converting to Message first\n if isinstance(input_value, str):\n input_value = Message(text=input_value)\n\n result = convert_to_dataframe(input_value, auto_parse=self.auto_parse)\n self.status = result\n return result\n" + "value": "import json\nfrom typing import Any\n\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, HandleInput, Output, TabInput\nfrom lfx.schema import Data, DataFrame, Message\nfrom lfx.schema.data import JSON\nfrom lfx.schema.dataframe import Table\n\nMIN_CSV_LINES = 2\n\n\ndef convert_to_message(v) -> Message:\n \"\"\"Convert input to Message type.\n\n Args:\n v: Input to convert (Message, Data, DataFrame, or dict)\n\n Returns:\n Message: Converted Message object\n \"\"\"\n return v if isinstance(v, Message) else v.to_message()\n\n\ndef convert_to_data(v: Table | Data | Message | dict, *, auto_parse: bool) -> JSON:\n \"\"\"Convert input to JSON type.\n\n Args:\n v: Input to convert (Message, Data, Table, or dict)\n auto_parse: Enable automatic parsing of structured data (JSON/CSV)\n\n Returns:\n JSON: Converted JSON object\n \"\"\"\n if isinstance(v, dict):\n return Data(v)\n if isinstance(v, Message):\n data = Data(data={\"text\": v.data[\"text\"]})\n return parse_structured_data(data) if auto_parse else data\n\n return v if isinstance(v, Data) else v.to_data()\n\n\ndef convert_to_dataframe(v: Table | Data | Message | dict, *, auto_parse: bool) -> Table:\n \"\"\"Convert input to Table type.\n\n Args:\n v: Input to convert (Message, Data, Table, or dict)\n auto_parse: Enable automatic parsing of structured data (JSON/CSV)\n\n Returns:\n Table: Converted Table object\n \"\"\"\n import pandas as pd\n\n if isinstance(v, dict):\n return DataFrame([v])\n if isinstance(v, DataFrame):\n return v\n # Handle pandas DataFrame\n if isinstance(v, pd.DataFrame):\n # Convert pandas DataFrame to our DataFrame by creating Data objects\n return DataFrame(data=v)\n\n if isinstance(v, Message):\n data = Data(data={\"text\": v.data[\"text\"]})\n return parse_structured_data(data).to_dataframe() if auto_parse else data.to_dataframe()\n # For other types, call to_dataframe method\n return v.to_dataframe()\n\n\ndef parse_structured_data(data: JSON) -> JSON:\n \"\"\"Parse structured data (JSON, CSV) from JSON's text field.\n\n Args:\n data: JSON object with text content to parse\n\n Returns:\n JSON: Modified JSON object with parsed content or original if parsing fails\n \"\"\"\n raw_text = data.get_text() or \"\"\n text = raw_text.lstrip(\"\\ufeff\").strip()\n\n # Try JSON parsing first\n parsed_json = _try_parse_json(text)\n if parsed_json is not None:\n return parsed_json\n\n # Try CSV parsing\n if _looks_like_csv(text):\n try:\n return _parse_csv_to_data(text)\n except Exception: # noqa: BLE001\n # Heuristic misfire or malformed CSV — keep original data\n return data\n\n # Return original data if no parsing succeeded\n return data\n\n\ndef _try_parse_json(text: str) -> JSON | None:\n \"\"\"Try to parse text as JSON and return JSON object.\"\"\"\n try:\n parsed = json.loads(text)\n\n if isinstance(parsed, dict):\n # Single JSON object\n return Data(data=parsed)\n if isinstance(parsed, list) and all(isinstance(item, dict) for item in parsed):\n # Array of JSON objects - create JSON with the list\n return Data(data={\"records\": parsed})\n\n except (json.JSONDecodeError, ValueError):\n pass\n\n return None\n\n\ndef _looks_like_csv(text: str) -> bool:\n \"\"\"Simple heuristic to detect CSV content.\"\"\"\n lines = text.strip().split(\"\\n\")\n if len(lines) < MIN_CSV_LINES:\n return False\n\n header_line = lines[0]\n return \",\" in header_line and len(lines) > 1\n\n\ndef _parse_csv_to_data(text: str) -> JSON:\n \"\"\"Parse CSV text and return JSON object.\"\"\"\n from io import StringIO\n\n import pandas as pd\n\n # Parse CSV to DataFrame, then convert to list of dicts\n parsed_df = pd.read_csv(StringIO(text))\n records = parsed_df.to_dict(orient=\"records\")\n\n return Data(data={\"records\": records})\n\n\nclass TypeConverterComponent(Component):\n display_name = \"Type Convert\"\n description = \"Convert between different types (Message, JSON, Table)\"\n documentation: str = \"https://docs.langflow.org/type-convert\"\n icon = \"repeat\"\n\n inputs = [\n HandleInput(\n name=\"input_data\",\n display_name=\"Input\",\n input_types=[\"Message\", \"Data\", \"JSON\", \"DataFrame\", \"Table\"],\n info=\"Accept Message, JSON or Table as input\",\n required=True,\n ),\n BoolInput(\n name=\"auto_parse\",\n display_name=\"Auto Parse\",\n info=\"Detect and convert JSON/CSV strings automatically.\",\n advanced=True,\n value=False,\n required=False,\n ),\n TabInput(\n name=\"output_type\",\n display_name=\"Output Type\",\n options=[\"Message\", \"JSON\", \"Table\"],\n info=\"Select the desired output data type\",\n real_time_refresh=True,\n value=\"Message\",\n ),\n ]\n\n # Define ALL outputs so they exist during validation\n # update_frontend_node will filter to show only the selected one\n outputs = [\n Output(\n display_name=\"Message Output\",\n name=\"message_output\",\n method=\"convert_to_message\",\n types=[\"Message\"],\n ),\n Output(\n display_name=\"JSON Output\",\n name=\"data_output\",\n method=\"convert_to_data\",\n types=[\"JSON\"],\n ),\n Output(\n display_name=\"Table Output\",\n name=\"dataframe_output\",\n method=\"convert_to_dataframe\",\n types=[\"Table\"],\n ),\n ]\n\n async def update_frontend_node(self, new_frontend_node: dict, current_frontend_node: dict):\n \"\"\"Ensure outputs are synced with output_type when component is loaded.\"\"\"\n # Call parent implementation first\n await super().update_frontend_node(new_frontend_node, current_frontend_node)\n\n # Then sync outputs with current output_type value\n output_type = new_frontend_node.get(\"template\", {}).get(\"output_type\", {}).get(\"value\", \"Message\")\n self.update_outputs(new_frontend_node, \"output_type\", output_type)\n\n return new_frontend_node\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Dynamically show only the relevant output based on the selected output type.\"\"\"\n if field_name == \"output_type\":\n # Start with empty outputs\n frontend_node[\"outputs\"] = []\n\n # Add only the selected output type WITH TYPES SPECIFIED\n if field_value == \"Message\":\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"Message Output\",\n name=\"message_output\",\n method=\"convert_to_message\",\n types=[\"Message\"],\n ).to_dict()\n )\n elif field_value in (\"Data\", \"JSON\"):\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"JSON Output\",\n name=\"data_output\",\n method=\"convert_to_data\",\n types=[\"JSON\"],\n ).to_dict()\n )\n elif field_value in (\"DataFrame\", \"Table\"):\n frontend_node[\"outputs\"].append(\n Output(\n display_name=\"Table Output\",\n name=\"dataframe_output\",\n method=\"convert_to_dataframe\",\n types=[\"Table\"],\n ).to_dict()\n )\n\n return frontend_node\n\n def convert_to_message(self) -> Message:\n \"\"\"Convert input to Message type.\"\"\"\n input_value = self.input_data[0] if isinstance(self.input_data, list) else self.input_data\n\n # Handle string input by converting to Message first\n if isinstance(input_value, str):\n input_value = Message(text=input_value)\n\n result = convert_to_message(input_value)\n self.status = result\n return result\n\n def convert_to_data(self) -> JSON:\n \"\"\"Convert input to JSON type.\"\"\"\n input_value = self.input_data[0] if isinstance(self.input_data, list) else self.input_data\n\n # Handle string input by converting to Message first\n if isinstance(input_value, str):\n input_value = Message(text=input_value)\n\n result = convert_to_data(input_value, auto_parse=self.auto_parse)\n self.status = result\n return result\n\n def convert_to_dataframe(self) -> Table:\n \"\"\"Convert input to Table type.\"\"\"\n input_value = self.input_data[0] if isinstance(self.input_data, list) else self.input_data\n\n # Handle string input by converting to Message first\n if isinstance(input_value, str):\n input_value = Message(text=input_value)\n\n result = convert_to_dataframe(input_value, auto_parse=self.auto_parse)\n self.status = result\n return result\n" }, "input_data": { "_input_type": "HandleInput", "advanced": false, "display_name": "Input", "dynamic": false, - "info": "Accept Message, Data or DataFrame as input", + "info": "Accept Message, JSON or Table as input", "input_types": [ "Message", "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "list": false, "list_add_label": "Add More", @@ -104289,8 +104455,8 @@ "name": "output_type", "options": [ "Message", - "Data", - "DataFrame" + "JSON", + "Table" ], "override_skip": false, "placeholder": "", @@ -104309,7 +104475,7 @@ }, "UpdateData": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -104328,7 +104494,7 @@ "icon": "FolderSync", "legacy": true, "metadata": { - "code_hash": "d0790af3ac9b", + "code_hash": "7d171034c729", "dependencies": { "dependencies": [ { @@ -104346,14 +104512,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "build_data", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -104380,7 +104546,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import (\n BoolInput,\n DataInput,\n DictInput,\n IntInput,\n MessageTextInput,\n)\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\n\n\nclass UpdateDataComponent(Component):\n display_name: str = \"Update Data\"\n description: str = \"Dynamically update or append data with the specified fields.\"\n name: str = \"UpdateData\"\n MAX_FIELDS = 15 # Define a constant for maximum number of fields\n icon = \"FolderSync\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(\n name=\"old_data\",\n display_name=\"Data\",\n info=\"The record to update.\",\n is_list=True, # Changed to True to handle list of Data objects\n required=True,\n ),\n IntInput(\n name=\"number_of_fields\",\n display_name=\"Number of Fields\",\n info=\"Number of fields to be added to the record.\",\n real_time_refresh=True,\n value=0,\n range_spec=RangeSpec(min=1, max=MAX_FIELDS, step=1, step_type=\"int\"),\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"Key that identifies the field to be used as the text content.\",\n advanced=True,\n ),\n BoolInput(\n name=\"text_key_validator\",\n display_name=\"Text Key Validator\",\n advanced=True,\n info=\"If enabled, checks if the given 'Text Key' is present in the given 'Data'.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"build_data\"),\n ]\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):\n \"\"\"Update the build configuration when the number of fields changes.\n\n Args:\n build_config (dotdict): The current build configuration.\n field_value (Any): The new value for the field.\n field_name (Optional[str]): The name of the field being updated.\n \"\"\"\n if field_name == \"number_of_fields\":\n default_keys = {\n \"code\",\n \"_type\",\n \"number_of_fields\",\n \"text_key\",\n \"old_data\",\n \"text_key_validator\",\n }\n try:\n field_value_int = int(field_value)\n except ValueError:\n return build_config\n\n if field_value_int > self.MAX_FIELDS:\n build_config[\"number_of_fields\"][\"value\"] = self.MAX_FIELDS\n msg = f\"Number of fields cannot exceed {self.MAX_FIELDS}. Try using a Component to combine two Data.\"\n raise ValueError(msg)\n\n existing_fields = {}\n # Back up the existing template fields\n for key in list(build_config.keys()):\n if key not in default_keys:\n existing_fields[key] = build_config.pop(key)\n\n for i in range(1, field_value_int + 1):\n key = f\"field_{i}_key\"\n if key in existing_fields:\n field = existing_fields[key]\n build_config[key] = field\n else:\n field = DictInput(\n display_name=f\"Field {i}\",\n name=key,\n info=f\"Key for field {i}.\",\n input_types=[\"Message\", \"Data\"],\n )\n build_config[field.name] = field.to_dict()\n\n build_config[\"number_of_fields\"][\"value\"] = field_value_int\n return build_config\n\n async def build_data(self) -> Data | list[Data]:\n \"\"\"Build the updated data by combining the old data with new fields.\"\"\"\n new_data = self.get_data()\n if isinstance(self.old_data, list):\n for data_item in self.old_data:\n if not isinstance(data_item, Data):\n continue # Skip invalid items\n data_item.data.update(new_data)\n if self.text_key:\n data_item.text_key = self.text_key\n self.validate_text_key(data_item)\n self.status = self.old_data\n return self.old_data # Returns List[Data]\n if isinstance(self.old_data, Data):\n self.old_data.data.update(new_data)\n if self.text_key:\n self.old_data.text_key = self.text_key\n self.status = self.old_data\n self.validate_text_key(self.old_data)\n return self.old_data # Returns Data\n msg = \"old_data is not a Data object or list of Data objects.\"\n raise ValueError(msg)\n\n def get_data(self):\n \"\"\"Function to get the Data from the attributes.\"\"\"\n data = {}\n default_keys = {\n \"code\",\n \"_type\",\n \"number_of_fields\",\n \"text_key\",\n \"old_data\",\n \"text_key_validator\",\n }\n for attr_name, attr_value in self._attributes.items():\n if attr_name in default_keys:\n continue # Skip default attributes\n if isinstance(attr_value, dict):\n for key, value in attr_value.items():\n data[key] = value.get_text() if isinstance(value, Data) else value\n elif isinstance(attr_value, Data):\n data[attr_name] = attr_value.get_text()\n else:\n data[attr_name] = attr_value\n return data\n\n def validate_text_key(self, data: Data) -> None:\n \"\"\"This function validates that the Text Key is one of the keys in the Data.\"\"\"\n data_keys = data.data.keys()\n if self.text_key and self.text_key not in data_keys:\n msg = f\"Text Key: '{self.text_key}' not found in the Data keys: {', '.join(data_keys)}\"\n raise ValueError(msg)\n" + "value": "from typing import Any\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import (\n BoolInput,\n DataInput,\n DictInput,\n IntInput,\n MessageTextInput,\n)\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\n\n\nclass UpdateDataComponent(Component):\n display_name: str = \"Update Data\"\n description: str = \"Dynamically update or append data with the specified fields.\"\n name: str = \"UpdateData\"\n MAX_FIELDS = 15 # Define a constant for maximum number of fields\n icon = \"FolderSync\"\n legacy = True\n replacement = [\"processing.DataOperations\"]\n\n inputs = [\n DataInput(\n name=\"old_data\",\n display_name=\"JSON\",\n info=\"The record to update.\",\n is_list=True, # Changed to True to handle list of Data objects\n required=True,\n ),\n IntInput(\n name=\"number_of_fields\",\n display_name=\"Number of Fields\",\n info=\"Number of fields to be added to the record.\",\n real_time_refresh=True,\n value=0,\n range_spec=RangeSpec(min=1, max=MAX_FIELDS, step=1, step_type=\"int\"),\n ),\n MessageTextInput(\n name=\"text_key\",\n display_name=\"Text Key\",\n info=\"Key that identifies the field to be used as the text content.\",\n advanced=True,\n ),\n BoolInput(\n name=\"text_key_validator\",\n display_name=\"Text Key Validator\",\n advanced=True,\n info=\"If enabled, checks if the given 'Text Key' is present in the given 'Data'.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"build_data\"),\n ]\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None):\n \"\"\"Update the build configuration when the number of fields changes.\n\n Args:\n build_config (dotdict): The current build configuration.\n field_value (Any): The new value for the field.\n field_name (Optional[str]): The name of the field being updated.\n \"\"\"\n if field_name == \"number_of_fields\":\n default_keys = {\n \"code\",\n \"_type\",\n \"number_of_fields\",\n \"text_key\",\n \"old_data\",\n \"text_key_validator\",\n }\n try:\n field_value_int = int(field_value)\n except ValueError:\n return build_config\n\n if field_value_int > self.MAX_FIELDS:\n build_config[\"number_of_fields\"][\"value\"] = self.MAX_FIELDS\n msg = f\"Number of fields cannot exceed {self.MAX_FIELDS}. Try using a Component to combine two Data.\"\n raise ValueError(msg)\n\n existing_fields = {}\n # Back up the existing template fields\n for key in list(build_config.keys()):\n if key not in default_keys:\n existing_fields[key] = build_config.pop(key)\n\n for i in range(1, field_value_int + 1):\n key = f\"field_{i}_key\"\n if key in existing_fields:\n field = existing_fields[key]\n build_config[key] = field\n else:\n field = DictInput(\n display_name=f\"Field {i}\",\n name=key,\n info=f\"Key for field {i}.\",\n input_types=[\"Message\", \"Data\", \"JSON\"],\n )\n build_config[field.name] = field.to_dict()\n\n build_config[\"number_of_fields\"][\"value\"] = field_value_int\n return build_config\n\n async def build_data(self) -> Data | list[Data]:\n \"\"\"Build the updated data by combining the old data with new fields.\"\"\"\n new_data = self.get_data()\n if isinstance(self.old_data, list):\n for data_item in self.old_data:\n if not isinstance(data_item, Data):\n continue # Skip invalid items\n data_item.data.update(new_data)\n if self.text_key:\n data_item.text_key = self.text_key\n self.validate_text_key(data_item)\n self.status = self.old_data\n return self.old_data # Returns List[Data]\n if isinstance(self.old_data, Data):\n self.old_data.data.update(new_data)\n if self.text_key:\n self.old_data.text_key = self.text_key\n self.status = self.old_data\n self.validate_text_key(self.old_data)\n return self.old_data # Returns Data\n msg = \"old_data is not a Data object or list of Data objects.\"\n raise ValueError(msg)\n\n def get_data(self):\n \"\"\"Function to get the Data from the attributes.\"\"\"\n data = {}\n default_keys = {\n \"code\",\n \"_type\",\n \"number_of_fields\",\n \"text_key\",\n \"old_data\",\n \"text_key_validator\",\n }\n for attr_name, attr_value in self._attributes.items():\n if attr_name in default_keys:\n continue # Skip default attributes\n if isinstance(attr_value, dict):\n for key, value in attr_value.items():\n data[key] = value.get_text() if isinstance(value, Data) else value\n elif isinstance(attr_value, Data):\n data[attr_name] = attr_value.get_text()\n else:\n data[attr_name] = attr_value\n return data\n\n def validate_text_key(self, data: Data) -> None:\n \"\"\"This function validates that the Text Key is one of the keys in the Data.\"\"\"\n data_keys = data.data.keys()\n if self.text_key and self.text_key not in data_keys:\n msg = f\"Text Key: '{self.text_key}' not found in the Data keys: {', '.join(data_keys)}\"\n raise ValueError(msg)\n" }, "number_of_fields": { "_input_type": "IntInput", @@ -104410,13 +104576,14 @@ "value": 0 }, "old_data": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, - "display_name": "Data", + "display_name": "JSON", "dynamic": false, "info": "The record to update.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -104489,7 +104656,7 @@ "PythonFunction": { "base_classes": [ "Callable", - "Data", + "JSON", "Message" ], "beta": false, @@ -104506,7 +104673,7 @@ "icon": "Python", "legacy": true, "metadata": { - "code_hash": "7da7d856a545", + "code_hash": "55dc87cf0979", "dependencies": { "dependencies": [ { @@ -104538,14 +104705,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Function Output (Data)", + "display_name": "Function Output (JSON)", "group_outputs": false, "method": "execute_function_data", "name": "function_output_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -104583,7 +104750,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.custom.utils import get_function\nfrom lfx.io import CodeInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\n\n\nclass PythonFunctionComponent(Component):\n display_name = \"Python Function\"\n description = \"Define and execute a Python function that returns a Data object or a Message.\"\n icon = \"Python\"\n name = \"PythonFunction\"\n legacy = True\n\n inputs = [\n CodeInput(\n name=\"function_code\",\n display_name=\"Function Code\",\n info=\"The code for the function.\",\n ),\n ]\n\n outputs = [\n Output(\n name=\"function_output\",\n display_name=\"Function Callable\",\n method=\"get_function_callable\",\n ),\n Output(\n name=\"function_output_data\",\n display_name=\"Function Output (Data)\",\n method=\"execute_function_data\",\n ),\n Output(\n name=\"function_output_str\",\n display_name=\"Function Output (Message)\",\n method=\"execute_function_message\",\n ),\n ]\n\n def get_function_callable(self) -> Callable:\n function_code = self.function_code\n self.status = function_code\n return get_function(function_code)\n\n def execute_function(self) -> list[dotdict | str] | dotdict | str:\n function_code = self.function_code\n\n if not function_code:\n return \"No function code provided.\"\n\n try:\n func = get_function(function_code)\n return func()\n except Exception as e: # noqa: BLE001\n logger.debug(\"Error executing function\", exc_info=True)\n return f\"Error executing function: {e}\"\n\n def execute_function_data(self) -> list[Data]:\n results = self.execute_function()\n results = results if isinstance(results, list) else [results]\n return [(Data(text=x) if isinstance(x, str) else Data(**x)) for x in results]\n\n def execute_function_message(self) -> Message:\n results = self.execute_function()\n results = results if isinstance(results, list) else [results]\n results_list = [str(x) for x in results]\n results_str = \"\\n\".join(results_list)\n return Message(text=results_str)\n" + "value": "from collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.custom.utils import get_function\nfrom lfx.io import CodeInput, Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\n\n\nclass PythonFunctionComponent(Component):\n display_name = \"Python Function\"\n description = \"Define and execute a Python function that returns a Data object or a Message.\"\n icon = \"Python\"\n name = \"PythonFunction\"\n legacy = True\n\n inputs = [\n CodeInput(\n name=\"function_code\",\n display_name=\"Function Code\",\n info=\"The code for the function.\",\n ),\n ]\n\n outputs = [\n Output(\n name=\"function_output\",\n display_name=\"Function Callable\",\n method=\"get_function_callable\",\n ),\n Output(\n name=\"function_output_data\",\n display_name=\"Function Output (JSON)\",\n method=\"execute_function_data\",\n ),\n Output(\n name=\"function_output_str\",\n display_name=\"Function Output (Message)\",\n method=\"execute_function_message\",\n ),\n ]\n\n def get_function_callable(self) -> Callable:\n function_code = self.function_code\n self.status = function_code\n return get_function(function_code)\n\n def execute_function(self) -> list[dotdict | str] | dotdict | str:\n function_code = self.function_code\n\n if not function_code:\n return \"No function code provided.\"\n\n try:\n func = get_function(function_code)\n return func()\n except Exception as e: # noqa: BLE001\n logger.debug(\"Error executing function\", exc_info=True)\n return f\"Error executing function: {e}\"\n\n def execute_function_data(self) -> list[Data]:\n results = self.execute_function()\n results = results if isinstance(results, list) else [results]\n return [(Data(text=x) if isinstance(x, str) else Data(**x)) for x in results]\n\n def execute_function_message(self) -> Message:\n results = self.execute_function()\n results = results if isinstance(results, list) else [results]\n results_list = [str(x) for x in results]\n results_str = \"\\n\".join(results_list)\n return Message(text=results_str)\n" }, "function_code": { "_input_type": "CodeInput", @@ -104615,8 +104782,8 @@ { "QdrantVectorStoreComponent": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -104682,24 +104849,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -105099,8 +105266,8 @@ { "Redis": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -105154,24 +105321,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -105959,7 +106126,7 @@ { "ScrapeGraphMarkdownifyApi": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -105975,7 +106142,7 @@ "frozen": false, "legacy": false, "metadata": { - "code_hash": "0f5f12091af6", + "code_hash": "c17524dbca7a", "dependencies": { "dependencies": [ { @@ -105997,14 +106164,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "scrape", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -106047,7 +106214,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n MessageTextInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass ScrapeGraphMarkdownifyApi(Component):\n display_name: str = \"ScrapeGraph Markdownify API\"\n description: str = \"Given a URL, it will return the markdownified content of the website.\"\n name = \"ScrapeGraphMarkdownifyApi\"\n\n output_types: list[str] = [\"Document\"]\n documentation: str = \"https://docs.scrapegraphai.com/services/markdownify\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"ScrapeGraph API Key\",\n required=True,\n password=True,\n info=\"The API key to use ScrapeGraph API.\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n tool_mode=True,\n info=\"The URL to markdownify.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"scrape\"),\n ]\n\n def scrape(self) -> list[Data]:\n try:\n from scrapegraph_py import Client\n from scrapegraph_py.logger import sgai_logger\n except ImportError as e:\n msg = \"Could not import scrapegraph-py package. Please install it with `pip install scrapegraph-py`.\"\n raise ImportError(msg) from e\n\n # Set logging level\n sgai_logger.set_logging(level=\"INFO\")\n\n # Initialize the client with API key\n sgai_client = Client(api_key=self.api_key)\n\n try:\n # Markdownify request\n response = sgai_client.markdownify(\n website_url=self.url,\n )\n\n # Close the client\n sgai_client.close()\n\n return Data(data=response)\n except Exception:\n sgai_client.close()\n raise\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n MessageTextInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass ScrapeGraphMarkdownifyApi(Component):\n display_name: str = \"ScrapeGraph Markdownify API\"\n description: str = \"Given a URL, it will return the markdownified content of the website.\"\n name = \"ScrapeGraphMarkdownifyApi\"\n\n output_types: list[str] = [\"Document\"]\n documentation: str = \"https://docs.scrapegraphai.com/services/markdownify\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"ScrapeGraph API Key\",\n required=True,\n password=True,\n info=\"The API key to use ScrapeGraph API.\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n tool_mode=True,\n info=\"The URL to markdownify.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"scrape\"),\n ]\n\n def scrape(self) -> list[Data]:\n try:\n from scrapegraph_py import Client\n from scrapegraph_py.logger import sgai_logger\n except ImportError as e:\n msg = \"Could not import scrapegraph-py package. Please install it with `pip install scrapegraph-py`.\"\n raise ImportError(msg) from e\n\n # Set logging level\n sgai_logger.set_logging(level=\"INFO\")\n\n # Initialize the client with API key\n sgai_client = Client(api_key=self.api_key)\n\n try:\n # Markdownify request\n response = sgai_client.markdownify(\n website_url=self.url,\n )\n\n # Close the client\n sgai_client.close()\n\n return Data(data=response)\n except Exception:\n sgai_client.close()\n raise\n" }, "url": { "_input_type": "MessageTextInput", @@ -106079,7 +106246,7 @@ }, "ScrapeGraphSearchApi": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -106096,7 +106263,7 @@ "icon": "ScrapeGraph", "legacy": false, "metadata": { - "code_hash": "002d2af653ef", + "code_hash": "4caa0e09ea85", "dependencies": { "dependencies": [ { @@ -106118,14 +106285,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "search", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -106168,7 +106335,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n MessageTextInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass ScrapeGraphSearchApi(Component):\n display_name: str = \"ScrapeGraph Search API\"\n description: str = \"Given a search prompt, it will return search results using ScrapeGraph's search functionality.\"\n name = \"ScrapeGraphSearchApi\"\n\n documentation: str = \"https://docs.scrapegraphai.com/services/searchscraper\"\n icon = \"ScrapeGraph\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"ScrapeGraph API Key\",\n required=True,\n password=True,\n info=\"The API key to use ScrapeGraph API.\",\n ),\n MessageTextInput(\n name=\"user_prompt\",\n display_name=\"Search Prompt\",\n tool_mode=True,\n info=\"The search prompt to use.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"search\"),\n ]\n\n def search(self) -> list[Data]:\n try:\n from scrapegraph_py import Client\n from scrapegraph_py.logger import sgai_logger\n except ImportError as e:\n msg = \"Could not import scrapegraph-py package. Please install it with `pip install scrapegraph-py`.\"\n raise ImportError(msg) from e\n\n # Set logging level\n sgai_logger.set_logging(level=\"INFO\")\n\n # Initialize the client with API key\n sgai_client = Client(api_key=self.api_key)\n\n try:\n # SearchScraper request\n response = sgai_client.searchscraper(\n user_prompt=self.user_prompt,\n )\n\n # Close the client\n sgai_client.close()\n\n return Data(data=response)\n except Exception:\n sgai_client.close()\n raise\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n MessageTextInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass ScrapeGraphSearchApi(Component):\n display_name: str = \"ScrapeGraph Search API\"\n description: str = \"Given a search prompt, it will return search results using ScrapeGraph's search functionality.\"\n name = \"ScrapeGraphSearchApi\"\n\n documentation: str = \"https://docs.scrapegraphai.com/services/searchscraper\"\n icon = \"ScrapeGraph\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"ScrapeGraph API Key\",\n required=True,\n password=True,\n info=\"The API key to use ScrapeGraph API.\",\n ),\n MessageTextInput(\n name=\"user_prompt\",\n display_name=\"Search Prompt\",\n tool_mode=True,\n info=\"The search prompt to use.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"search\"),\n ]\n\n def search(self) -> list[Data]:\n try:\n from scrapegraph_py import Client\n from scrapegraph_py.logger import sgai_logger\n except ImportError as e:\n msg = \"Could not import scrapegraph-py package. Please install it with `pip install scrapegraph-py`.\"\n raise ImportError(msg) from e\n\n # Set logging level\n sgai_logger.set_logging(level=\"INFO\")\n\n # Initialize the client with API key\n sgai_client = Client(api_key=self.api_key)\n\n try:\n # SearchScraper request\n response = sgai_client.searchscraper(\n user_prompt=self.user_prompt,\n )\n\n # Close the client\n sgai_client.close()\n\n return Data(data=response)\n except Exception:\n sgai_client.close()\n raise\n" }, "user_prompt": { "_input_type": "MessageTextInput", @@ -106200,7 +106367,7 @@ }, "ScrapeGraphSmartScraperApi": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -106217,7 +106384,7 @@ "frozen": false, "legacy": false, "metadata": { - "code_hash": "cb419bec02ed", + "code_hash": "229446ce1e37", "dependencies": { "dependencies": [ { @@ -106239,14 +106406,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "scrape", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -106289,7 +106456,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n MessageTextInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass ScrapeGraphSmartScraperApi(Component):\n display_name: str = \"ScrapeGraph Smart Scraper API\"\n description: str = \"Given a URL, it will return the structured data of the website.\"\n name = \"ScrapeGraphSmartScraperApi\"\n\n output_types: list[str] = [\"Document\"]\n documentation: str = \"https://docs.scrapegraphai.com/services/smartscraper\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"ScrapeGraph API Key\",\n required=True,\n password=True,\n info=\"The API key to use ScrapeGraph API.\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n tool_mode=True,\n info=\"The URL to scrape.\",\n ),\n MessageTextInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n tool_mode=True,\n info=\"The prompt to use for the smart scraper.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"scrape\"),\n ]\n\n def scrape(self) -> list[Data]:\n try:\n from scrapegraph_py import Client\n from scrapegraph_py.logger import sgai_logger\n except ImportError as e:\n msg = \"Could not import scrapegraph-py package. Please install it with `pip install scrapegraph-py`.\"\n raise ImportError(msg) from e\n\n # Set logging level\n sgai_logger.set_logging(level=\"INFO\")\n\n # Initialize the client with API key\n sgai_client = Client(api_key=self.api_key)\n\n try:\n # SmartScraper request\n response = sgai_client.smartscraper(\n website_url=self.url,\n user_prompt=self.prompt,\n )\n\n # Close the client\n sgai_client.close()\n\n return Data(data=response)\n except Exception:\n sgai_client.close()\n raise\n" + "value": "from lfx.custom.custom_component.component import Component\nfrom lfx.io import (\n MessageTextInput,\n Output,\n SecretStrInput,\n)\nfrom lfx.schema.data import Data\n\n\nclass ScrapeGraphSmartScraperApi(Component):\n display_name: str = \"ScrapeGraph Smart Scraper API\"\n description: str = \"Given a URL, it will return the structured data of the website.\"\n name = \"ScrapeGraphSmartScraperApi\"\n\n output_types: list[str] = [\"Document\"]\n documentation: str = \"https://docs.scrapegraphai.com/services/smartscraper\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"ScrapeGraph API Key\",\n required=True,\n password=True,\n info=\"The API key to use ScrapeGraph API.\",\n ),\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n tool_mode=True,\n info=\"The URL to scrape.\",\n ),\n MessageTextInput(\n name=\"prompt\",\n display_name=\"Prompt\",\n tool_mode=True,\n info=\"The prompt to use for the smart scraper.\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"scrape\"),\n ]\n\n def scrape(self) -> list[Data]:\n try:\n from scrapegraph_py import Client\n from scrapegraph_py.logger import sgai_logger\n except ImportError as e:\n msg = \"Could not import scrapegraph-py package. Please install it with `pip install scrapegraph-py`.\"\n raise ImportError(msg) from e\n\n # Set logging level\n sgai_logger.set_logging(level=\"INFO\")\n\n # Initialize the client with API key\n sgai_client = Client(api_key=self.api_key)\n\n try:\n # SmartScraper request\n response = sgai_client.smartscraper(\n website_url=self.url,\n user_prompt=self.prompt,\n )\n\n # Close the client\n sgai_client.close()\n\n return Data(data=response)\n except Exception:\n sgai_client.close()\n raise\n" }, "prompt": { "_input_type": "MessageTextInput", @@ -106351,7 +106518,7 @@ { "SearchComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -106372,7 +106539,7 @@ "icon": "SearchAPI", "legacy": false, "metadata": { - "code_hash": "625d1f5b3290", + "code_hash": "766aee1dff00", "dependencies": { "dependencies": [ { @@ -106394,14 +106561,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -106444,7 +106611,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_community.utilities.searchapi import SearchApiAPIWrapper\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SearchComponent(Component):\n display_name: str = \"SearchApi\"\n description: str = \"Calls the SearchApi API with result limiting. Supports Google, Bing and DuckDuckGo.\"\n documentation: str = \"https://www.searchapi.io/docs/google\"\n icon = \"SearchAPI\"\n\n inputs = [\n DropdownInput(name=\"engine\", display_name=\"Engine\", value=\"google\", options=[\"google\", \"bing\", \"duckduckgo\"]),\n SecretStrInput(name=\"api_key\", display_name=\"SearchAPI API Key\", required=True),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n DictInput(name=\"search_params\", display_name=\"Search parameters\", advanced=True, is_list=True),\n IntInput(name=\"max_results\", display_name=\"Max Results\", value=5, advanced=True),\n IntInput(name=\"max_snippet_length\", display_name=\"Max Snippet Length\", value=100, advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def _build_wrapper(self):\n return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key)\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n\n def search_func(\n query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100\n ) -> list[Data]:\n params = params or {}\n full_results = wrapper.results(query=query, **params)\n organic_results = full_results.get(\"organic_results\", [])[:max_results]\n\n return [\n Data(\n text=result.get(\"snippet\", \"\"),\n data={\n \"title\": result.get(\"title\", \"\")[:max_snippet_length],\n \"link\": result.get(\"link\", \"\"),\n \"snippet\": result.get(\"snippet\", \"\")[:max_snippet_length],\n },\n )\n for result in organic_results\n ]\n\n results = search_func(\n self.input_value,\n self.search_params or {},\n self.max_results,\n self.max_snippet_length,\n )\n self.status = results\n return results\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "from typing import Any\n\nfrom langchain_community.utilities.searchapi import SearchApiAPIWrapper\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass SearchComponent(Component):\n display_name: str = \"SearchApi\"\n description: str = \"Calls the SearchApi API with result limiting. Supports Google, Bing and DuckDuckGo.\"\n documentation: str = \"https://www.searchapi.io/docs/google\"\n icon = \"SearchAPI\"\n\n inputs = [\n DropdownInput(name=\"engine\", display_name=\"Engine\", value=\"google\", options=[\"google\", \"bing\", \"duckduckgo\"]),\n SecretStrInput(name=\"api_key\", display_name=\"SearchAPI API Key\", required=True),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n DictInput(name=\"search_params\", display_name=\"Search parameters\", advanced=True, is_list=True),\n IntInput(name=\"max_results\", display_name=\"Max Results\", value=5, advanced=True),\n IntInput(name=\"max_snippet_length\", display_name=\"Max Snippet Length\", value=100, advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def _build_wrapper(self):\n return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key)\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n\n def search_func(\n query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100\n ) -> list[Data]:\n params = params or {}\n full_results = wrapper.results(query=query, **params)\n organic_results = full_results.get(\"organic_results\", [])[:max_results]\n\n return [\n Data(\n text=result.get(\"snippet\", \"\"),\n data={\n \"title\": result.get(\"title\", \"\")[:max_snippet_length],\n \"link\": result.get(\"link\", \"\"),\n \"snippet\": result.get(\"snippet\", \"\")[:max_snippet_length],\n },\n )\n for result in organic_results\n ]\n\n results = search_func(\n self.input_value,\n self.search_params or {},\n self.max_results,\n self.max_snippet_length,\n )\n self.status = results\n return results\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" }, "engine": { "_input_type": "DropdownInput", @@ -106573,7 +106740,7 @@ { "Serp": { "base_classes": [ - "Data", + "JSON", "Message" ], "beta": false, @@ -106594,7 +106761,7 @@ "icon": "SerpSearch", "legacy": false, "metadata": { - "code_hash": "dcc2ecb44ff6", + "code_hash": "85a6736d5bb3", "dependencies": { "dependencies": [ { @@ -106624,14 +106791,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "fetch_content", "name": "data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -106669,7 +106836,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom langchain_community.utilities.serpapi import SerpAPIWrapper\nfrom langchain_core.tools import ToolException\nfrom pydantic import BaseModel, Field\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DictInput, IntInput, MultilineInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass SerpAPISchema(BaseModel):\n \"\"\"Schema for SerpAPI search parameters.\"\"\"\n\n query: str = Field(..., description=\"The search query\")\n params: dict[str, Any] | None = Field(\n default={\n \"engine\": \"google\",\n \"google_domain\": \"google.com\",\n \"gl\": \"us\",\n \"hl\": \"en\",\n },\n description=\"Additional search parameters\",\n )\n max_results: int = Field(5, description=\"Maximum number of results to return\")\n max_snippet_length: int = Field(100, description=\"Maximum length of each result snippet\")\n\n\nclass SerpComponent(Component):\n display_name = \"Serp Search API\"\n description = \"Call Serp Search API with result limiting\"\n name = \"Serp\"\n icon = \"SerpSearch\"\n\n inputs = [\n SecretStrInput(name=\"serpapi_api_key\", display_name=\"SerpAPI API Key\", required=True),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n DictInput(name=\"search_params\", display_name=\"Parameters\", advanced=True, is_list=True),\n IntInput(name=\"max_results\", display_name=\"Max Results\", value=5, advanced=True),\n IntInput(name=\"max_snippet_length\", display_name=\"Max Snippet Length\", value=100, advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n ]\n\n def _build_wrapper(self, params: dict[str, Any] | None = None) -> SerpAPIWrapper:\n \"\"\"Build a SerpAPIWrapper with the provided parameters.\"\"\"\n params = params or {}\n if params:\n return SerpAPIWrapper(\n serpapi_api_key=self.serpapi_api_key,\n params=params,\n )\n return SerpAPIWrapper(serpapi_api_key=self.serpapi_api_key)\n\n def run_model(self) -> list[Data]:\n return self.fetch_content()\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper(self.search_params)\n\n def search_func(\n query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100\n ) -> list[Data]:\n try:\n local_wrapper = wrapper\n if params:\n local_wrapper = self._build_wrapper(params)\n\n full_results = local_wrapper.results(query)\n organic_results = full_results.get(\"organic_results\", [])[:max_results]\n\n limited_results = [\n Data(\n text=result.get(\"snippet\", \"\"),\n data={\n \"title\": result.get(\"title\", \"\")[:max_snippet_length],\n \"link\": result.get(\"link\", \"\"),\n \"snippet\": result.get(\"snippet\", \"\")[:max_snippet_length],\n },\n )\n for result in organic_results\n ]\n\n except Exception as e:\n error_message = f\"Error in SerpAPI search: {e!s}\"\n logger.debug(error_message)\n raise ToolException(error_message) from e\n return limited_results\n\n results = search_func(\n self.input_value,\n params=self.search_params,\n max_results=self.max_results,\n max_snippet_length=self.max_snippet_length,\n )\n self.status = results\n return results\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n result_string = \"\"\n for item in data:\n result_string += item.text + \"\\n\"\n self.status = result_string\n return Message(text=result_string)\n" + "value": "from typing import Any\n\nfrom langchain_community.utilities.serpapi import SerpAPIWrapper\nfrom langchain_core.tools import ToolException\nfrom pydantic import BaseModel, Field\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DictInput, IntInput, MultilineInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.message import Message\n\n\nclass SerpAPISchema(BaseModel):\n \"\"\"Schema for SerpAPI search parameters.\"\"\"\n\n query: str = Field(..., description=\"The search query\")\n params: dict[str, Any] | None = Field(\n default={\n \"engine\": \"google\",\n \"google_domain\": \"google.com\",\n \"gl\": \"us\",\n \"hl\": \"en\",\n },\n description=\"Additional search parameters\",\n )\n max_results: int = Field(5, description=\"Maximum number of results to return\")\n max_snippet_length: int = Field(100, description=\"Maximum length of each result snippet\")\n\n\nclass SerpComponent(Component):\n display_name = \"Serp Search API\"\n description = \"Call Serp Search API with result limiting\"\n name = \"Serp\"\n icon = \"SerpSearch\"\n\n inputs = [\n SecretStrInput(name=\"serpapi_api_key\", display_name=\"SerpAPI API Key\", required=True),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n DictInput(name=\"search_params\", display_name=\"Parameters\", advanced=True, is_list=True),\n IntInput(name=\"max_results\", display_name=\"Max Results\", value=5, advanced=True),\n IntInput(name=\"max_snippet_length\", display_name=\"Max Snippet Length\", value=100, advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n ]\n\n def _build_wrapper(self, params: dict[str, Any] | None = None) -> SerpAPIWrapper:\n \"\"\"Build a SerpAPIWrapper with the provided parameters.\"\"\"\n params = params or {}\n if params:\n return SerpAPIWrapper(\n serpapi_api_key=self.serpapi_api_key,\n params=params,\n )\n return SerpAPIWrapper(serpapi_api_key=self.serpapi_api_key)\n\n def run_model(self) -> list[Data]:\n return self.fetch_content()\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper(self.search_params)\n\n def search_func(\n query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100\n ) -> list[Data]:\n try:\n local_wrapper = wrapper\n if params:\n local_wrapper = self._build_wrapper(params)\n\n full_results = local_wrapper.results(query)\n organic_results = full_results.get(\"organic_results\", [])[:max_results]\n\n limited_results = [\n Data(\n text=result.get(\"snippet\", \"\"),\n data={\n \"title\": result.get(\"title\", \"\")[:max_snippet_length],\n \"link\": result.get(\"link\", \"\"),\n \"snippet\": result.get(\"snippet\", \"\")[:max_snippet_length],\n },\n )\n for result in organic_results\n ]\n\n except Exception as e:\n error_message = f\"Error in SerpAPI search: {e!s}\"\n logger.debug(error_message)\n raise ToolException(error_message) from e\n return limited_results\n\n results = search_func(\n self.input_value,\n params=self.search_params,\n max_results=self.max_results,\n max_snippet_length=self.max_snippet_length,\n )\n self.status = results\n return results\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n result_string = \"\"\n for item in data:\n result_string += item.text + \"\\n\"\n self.status = result_string\n return Message(text=result_string)\n" }, "input_value": { "_input_type": "MultilineInput", @@ -106789,8 +106956,8 @@ { "SupabaseVectorStore": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -106844,24 +107011,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -107089,7 +107256,7 @@ { "TavilyExtractComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -107108,7 +107275,7 @@ "icon": "TavilyIcon", "legacy": false, "metadata": { - "code_hash": "fec95e2181d8", + "code_hash": "86266b25a045", "dependencies": { "dependencies": [ { @@ -107130,14 +107297,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content", "name": "dataframe", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -107180,7 +107347,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, DropdownInput, MessageTextInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass TavilyExtractComponent(Component):\n \"\"\"Separate component specifically for Tavily Extract functionality.\"\"\"\n\n display_name = \"Tavily Extract API\"\n description = \"\"\"**Tavily Extract** extract raw content from URLs.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Comma-separated list of URLs to extract content from.\",\n required=True,\n ),\n DropdownInput(\n name=\"extract_depth\",\n display_name=\"Extract Depth\",\n info=\"The depth of the extraction process.\",\n options=[\"basic\", \"advanced\"],\n value=\"basic\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of images extracted from the URLs.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n \"\"\"Fetches and processes extracted content into a list of Data objects.\"\"\"\n try:\n # Split URLs by comma and clean them\n urls = [url.strip() for url in (self.urls or \"\").split(\",\") if url.strip()]\n if not urls:\n error_message = \"No valid URLs provided\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n\n url = \"https://api.tavily.com/extract\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\",\n }\n payload = {\n \"urls\": urls,\n \"extract_depth\": self.extract_depth,\n \"include_images\": self.include_images,\n }\n\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n response.raise_for_status()\n\n except httpx.TimeoutException as exc:\n error_message = f\"Request timed out (90s): {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except (ValueError, KeyError, AttributeError, httpx.RequestError) as exc:\n error_message = f\"Data processing error: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n extract_results = response.json()\n data_results = []\n\n # Process successful extractions\n for result in extract_results.get(\"results\", []):\n raw_content = result.get(\"raw_content\", \"\")\n images = result.get(\"images\", [])\n result_data = {\"url\": result.get(\"url\"), \"raw_content\": raw_content, \"images\": images}\n data_results.append(Data(text=raw_content, data=result_data))\n\n # Process failed extractions\n if extract_results.get(\"failed_results\"):\n data_results.append(\n Data(\n text=\"Failed extractions\",\n data={\"failed_results\": extract_results[\"failed_results\"]},\n )\n )\n\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import httpx\n\nfrom lfx.custom import Component\nfrom lfx.io import BoolInput, DropdownInput, MessageTextInput, Output, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass TavilyExtractComponent(Component):\n \"\"\"Separate component specifically for Tavily Extract functionality.\"\"\"\n\n display_name = \"Tavily Extract API\"\n description = \"\"\"**Tavily Extract** extract raw content from URLs.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Comma-separated list of URLs to extract content from.\",\n required=True,\n ),\n DropdownInput(\n name=\"extract_depth\",\n display_name=\"Extract Depth\",\n info=\"The depth of the extraction process.\",\n options=[\"basic\", \"advanced\"],\n value=\"basic\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of images extracted from the URLs.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n \"\"\"Fetches and processes extracted content into a list of Data objects.\"\"\"\n try:\n # Split URLs by comma and clean them\n urls = [url.strip() for url in (self.urls or \"\").split(\",\") if url.strip()]\n if not urls:\n error_message = \"No valid URLs provided\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n\n url = \"https://api.tavily.com/extract\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n \"Authorization\": f\"Bearer {self.api_key}\",\n }\n payload = {\n \"urls\": urls,\n \"extract_depth\": self.extract_depth,\n \"include_images\": self.include_images,\n }\n\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n response.raise_for_status()\n\n except httpx.TimeoutException as exc:\n error_message = f\"Request timed out (90s): {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except (ValueError, KeyError, AttributeError, httpx.RequestError) as exc:\n error_message = f\"Data processing error: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n extract_results = response.json()\n data_results = []\n\n # Process successful extractions\n for result in extract_results.get(\"results\", []):\n raw_content = result.get(\"raw_content\", \"\")\n images = result.get(\"images\", [])\n result_data = {\"url\": result.get(\"url\"), \"raw_content\": raw_content, \"images\": images}\n data_results.append(Data(text=raw_content, data=result_data))\n\n # Process failed extractions\n if extract_results.get(\"failed_results\"):\n data_results.append(\n Data(\n text=\"Failed extractions\",\n data={\"failed_results\": extract_results[\"failed_results\"]},\n )\n )\n\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "extract_depth": { "_input_type": "DropdownInput", @@ -107259,7 +107426,7 @@ }, "TavilySearchComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -107287,7 +107454,7 @@ "icon": "TavilyIcon", "legacy": false, "metadata": { - "code_hash": "e602eaec8316", + "code_hash": "5638a305a99c", "dependencies": { "dependencies": [ { @@ -107309,14 +107476,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -107379,7 +107546,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import httpx\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass TavilySearchComponent(Component):\n display_name = \"Tavily Search API\"\n description = \"\"\"**Tavily Search** is a search engine optimized for LLMs and RAG, \\\n aimed at efficient, quick, and persistent search results.\"\"\"\n icon = \"TavilyIcon\"\n\n inputs = [\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Tavily API Key\",\n required=True,\n info=\"Your Tavily API Key.\",\n ),\n MessageTextInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The search query you want to execute with Tavily.\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"search_depth\",\n display_name=\"Search Depth\",\n info=\"The depth of the search.\",\n options=[\"basic\", \"advanced\"],\n value=\"advanced\",\n advanced=True,\n ),\n IntInput(\n name=\"chunks_per_source\",\n display_name=\"Chunks Per Source\",\n info=(\"The number of content chunks to retrieve from each source (1-3). Only works with advanced search.\"),\n value=3,\n advanced=True,\n ),\n DropdownInput(\n name=\"topic\",\n display_name=\"Search Topic\",\n info=\"The category of the search.\",\n options=[\"general\", \"news\"],\n value=\"general\",\n advanced=True,\n ),\n IntInput(\n name=\"days\",\n display_name=\"Days\",\n info=\"Number of days back from current date to include. Only available with news topic.\",\n value=7,\n advanced=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n info=\"The maximum number of search results to return.\",\n value=5,\n advanced=True,\n ),\n BoolInput(\n name=\"include_answer\",\n display_name=\"Include Answer\",\n info=\"Include a short answer to original query.\",\n value=True,\n advanced=True,\n ),\n DropdownInput(\n name=\"time_range\",\n display_name=\"Time Range\",\n info=\"The time range back from the current date to filter results.\",\n options=[\"day\", \"week\", \"month\", \"year\"],\n value=None, # Default to None to make it optional\n advanced=True,\n ),\n BoolInput(\n name=\"include_images\",\n display_name=\"Include Images\",\n info=\"Include a list of query-related images in the response.\",\n value=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"include_domains\",\n display_name=\"Include Domains\",\n info=\"Comma-separated list of domains to include in the search results.\",\n advanced=True,\n ),\n MessageTextInput(\n name=\"exclude_domains\",\n display_name=\"Exclude Domains\",\n info=\"Comma-separated list of domains to exclude from the search results.\",\n advanced=True,\n ),\n BoolInput(\n name=\"include_raw_content\",\n display_name=\"Include Raw Content\",\n info=\"Include the cleaned and parsed HTML content of each search result.\",\n value=False,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def fetch_content(self) -> list[Data]:\n try:\n # Only process domains if they're provided\n include_domains = None\n exclude_domains = None\n\n if self.include_domains:\n include_domains = [domain.strip() for domain in self.include_domains.split(\",\") if domain.strip()]\n\n if self.exclude_domains:\n exclude_domains = [domain.strip() for domain in self.exclude_domains.split(\",\") if domain.strip()]\n\n url = \"https://api.tavily.com/search\"\n headers = {\n \"content-type\": \"application/json\",\n \"accept\": \"application/json\",\n }\n\n payload = {\n \"api_key\": self.api_key,\n \"query\": self.query,\n \"search_depth\": self.search_depth,\n \"topic\": self.topic,\n \"max_results\": self.max_results,\n \"include_images\": self.include_images,\n \"include_answer\": self.include_answer,\n \"include_raw_content\": self.include_raw_content,\n \"days\": self.days,\n \"time_range\": self.time_range,\n }\n\n # Only add domains to payload if they exist and have values\n if include_domains:\n payload[\"include_domains\"] = include_domains\n if exclude_domains:\n payload[\"exclude_domains\"] = exclude_domains\n\n # Add conditional parameters only if they should be included\n if self.search_depth == \"advanced\" and self.chunks_per_source:\n payload[\"chunks_per_source\"] = self.chunks_per_source\n\n if self.topic == \"news\" and self.days:\n payload[\"days\"] = int(self.days) # Ensure days is an integer\n\n # Add time_range if it's set\n if hasattr(self, \"time_range\") and self.time_range:\n payload[\"time_range\"] = self.time_range\n\n # Add timeout handling\n with httpx.Client(timeout=90.0) as client:\n response = client.post(url, json=payload, headers=headers)\n\n response.raise_for_status()\n search_results = response.json()\n\n data_results = []\n\n if self.include_answer and search_results.get(\"answer\"):\n data_results.append(Data(text=search_results[\"answer\"]))\n\n for result in search_results.get(\"results\", []):\n content = result.get(\"content\", \"\")\n result_data = {\n \"title\": result.get(\"title\"),\n \"url\": result.get(\"url\"),\n \"content\": content,\n \"score\": result.get(\"score\"),\n }\n if self.include_raw_content:\n result_data[\"raw_content\"] = result.get(\"raw_content\")\n\n data_results.append(Data(text=content, data=result_data))\n\n if self.include_images and search_results.get(\"images\"):\n data_results.append(Data(text=\"Images found\", data={\"images\": search_results[\"images\"]}))\n\n except httpx.TimeoutException:\n error_message = \"Request timed out (90s). Please try again or adjust parameters.\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.HTTPStatusError as exc:\n error_message = f\"HTTP error occurred: {exc.response.status_code} - {exc.response.text}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except httpx.RequestError as exc:\n error_message = f\"Request error occurred: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n except ValueError as exc:\n error_message = f\"Invalid response format: {exc}\"\n logger.error(error_message)\n return [Data(text=error_message, data={\"error\": error_message})]\n else:\n self.status = data_results\n return data_results\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "days": { "_input_type": "IntInput", @@ -107648,7 +107815,7 @@ { "CalculatorTool": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -107699,14 +107866,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -107779,7 +107946,7 @@ }, "GoogleSearchAPI": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -107825,14 +107992,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -107964,7 +108131,7 @@ }, "GoogleSerperAPI": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -108015,14 +108182,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -108208,7 +108375,7 @@ "icon": "Python", "legacy": true, "metadata": { - "code_hash": "99f294af525b", + "code_hash": "3f913a303e47", "dependencies": { "dependencies": [ { @@ -108326,7 +108493,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport json\nfrom typing import Any\n\nfrom langchain.agents import Tool\nfrom langchain_core.tools import StructuredTool\nfrom pydantic.v1 import Field, create_model\nfrom pydantic.v1.fields import Undefined\nfrom typing_extensions import override\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, FieldTypes, HandleInput, MessageTextInput, MultilineInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\n\n\nclass PythonCodeStructuredTool(LCToolComponent):\n DEFAULT_KEYS = [\n \"code\",\n \"_type\",\n \"text_key\",\n \"tool_code\",\n \"tool_name\",\n \"tool_description\",\n \"return_direct\",\n \"tool_function\",\n \"global_variables\",\n \"_classes\",\n \"_functions\",\n ]\n display_name = \"Python Code Structured\"\n description = \"structuredtool dataclass code to tool\"\n documentation = \"https://python.langchain.com/docs/modules/tools/custom_tools/#structuredtool-dataclass\"\n name = \"PythonCodeStructuredTool\"\n icon = \"Python\"\n field_order = [\"name\", \"description\", \"tool_code\", \"return_direct\", \"tool_function\"]\n legacy: bool = True\n replacement = [\"processing.PythonREPLComponent\"]\n\n inputs = [\n MultilineInput(\n name=\"tool_code\",\n display_name=\"Tool Code\",\n info=\"Enter the dataclass code.\",\n placeholder=\"def my_function(args):\\n pass\",\n required=True,\n real_time_refresh=True,\n refresh_button=True,\n ),\n MessageTextInput(\n name=\"tool_name\",\n display_name=\"Tool Name\",\n info=\"Enter the name of the tool.\",\n required=True,\n ),\n MessageTextInput(\n name=\"tool_description\",\n display_name=\"Description\",\n info=\"Enter the description of the tool.\",\n required=True,\n ),\n BoolInput(\n name=\"return_direct\",\n display_name=\"Return Directly\",\n info=\"Should the tool return the function output directly?\",\n ),\n DropdownInput(\n name=\"tool_function\",\n display_name=\"Tool Function\",\n info=\"Select the function for additional expressions.\",\n options=[],\n required=True,\n real_time_refresh=True,\n refresh_button=True,\n ),\n HandleInput(\n name=\"global_variables\",\n display_name=\"Global Variables\",\n info=\"Enter the global variables or Create Data Component.\",\n input_types=[\"Data\"],\n field_type=FieldTypes.DICT,\n is_list=True,\n ),\n MessageTextInput(name=\"_classes\", display_name=\"Classes\", advanced=True),\n MessageTextInput(name=\"_functions\", display_name=\"Functions\", advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"Tool\", name=\"result_tool\", method=\"build_tool\"),\n ]\n\n @override\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name is None:\n return build_config\n\n if field_name not in {\"tool_code\", \"tool_function\"}:\n return build_config\n\n try:\n named_functions = {}\n [classes, functions] = self._parse_code(build_config[\"tool_code\"][\"value\"])\n existing_fields = {}\n if len(build_config) > len(self.DEFAULT_KEYS):\n for key in build_config.copy():\n if key not in self.DEFAULT_KEYS:\n existing_fields[key] = build_config.pop(key)\n\n names = []\n for func in functions:\n named_functions[func[\"name\"]] = func\n names.append(func[\"name\"])\n\n for arg in func[\"args\"]:\n field_name = f\"{func['name']}|{arg['name']}\"\n if field_name in existing_fields:\n build_config[field_name] = existing_fields[field_name]\n continue\n\n field = MessageTextInput(\n display_name=f\"{arg['name']}: Description\",\n name=field_name,\n info=f\"Enter the description for {arg['name']}\",\n required=True,\n )\n build_config[field_name] = field.to_dict()\n build_config[\"_functions\"][\"value\"] = json.dumps(named_functions)\n build_config[\"_classes\"][\"value\"] = json.dumps(classes)\n build_config[\"tool_function\"][\"options\"] = names\n except Exception as e: # noqa: BLE001\n self.status = f\"Failed to extract names: {e}\"\n logger.debug(self.status, exc_info=True)\n build_config[\"tool_function\"][\"options\"] = [\"Failed to parse\", str(e)]\n return build_config\n\n async def build_tool(self) -> Tool:\n local_namespace = {} # type: ignore[var-annotated]\n modules = self._find_imports(self.tool_code)\n import_code = \"\"\n for module in modules[\"imports\"]:\n import_code += f\"global {module}\\nimport {module}\\n\"\n for from_module in modules[\"from_imports\"]:\n for alias in from_module.names:\n import_code += f\"global {alias.name}\\n\"\n import_code += (\n f\"from {from_module.module} import {', '.join([alias.name for alias in from_module.names])}\\n\"\n )\n exec(import_code, globals())\n exec(self.tool_code, globals(), local_namespace)\n\n class PythonCodeToolFunc:\n params: dict = {}\n\n def run(**kwargs):\n for key, arg in kwargs.items():\n if key not in PythonCodeToolFunc.params:\n PythonCodeToolFunc.params[key] = arg\n return local_namespace[self.tool_function](**PythonCodeToolFunc.params)\n\n globals_ = globals()\n local = {}\n local[self.tool_function] = PythonCodeToolFunc\n globals_.update(local)\n\n if isinstance(self.global_variables, list):\n for data in self.global_variables:\n if isinstance(data, Data):\n globals_.update(data.data)\n elif isinstance(self.global_variables, dict):\n globals_.update(self.global_variables)\n\n classes = json.loads(self._attributes[\"_classes\"])\n for class_dict in classes:\n exec(\"\\n\".join(class_dict[\"code\"]), globals_)\n\n named_functions = json.loads(self._attributes[\"_functions\"])\n schema_fields = {}\n\n for attr in self._attributes:\n if attr in self.DEFAULT_KEYS:\n continue\n\n func_name = attr.split(\"|\")[0]\n field_name = attr.split(\"|\")[1]\n func_arg = self._find_arg(named_functions, func_name, field_name)\n if func_arg is None:\n msg = f\"Failed to find arg: {field_name}\"\n raise ValueError(msg)\n\n field_annotation = func_arg[\"annotation\"]\n field_description = self._get_value(self._attributes[attr], str)\n\n if field_annotation:\n exec(f\"temp_annotation_type = {field_annotation}\", globals_)\n schema_annotation = globals_[\"temp_annotation_type\"]\n else:\n schema_annotation = Any\n schema_fields[field_name] = (\n schema_annotation,\n Field(\n default=func_arg.get(\"default\", Undefined),\n description=field_description,\n ),\n )\n\n if \"temp_annotation_type\" in globals_:\n globals_.pop(\"temp_annotation_type\")\n\n python_code_tool_schema = None\n if schema_fields:\n python_code_tool_schema = create_model(\"PythonCodeToolSchema\", **schema_fields)\n\n return StructuredTool.from_function(\n func=local[self.tool_function].run,\n args_schema=python_code_tool_schema,\n name=self.tool_name,\n description=self.tool_description,\n return_direct=self.return_direct,\n )\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 frontend_node[\"template\"] = await self.update_build_config(\n frontend_node[\"template\"],\n frontend_node[\"template\"][\"tool_code\"][\"value\"],\n \"tool_code\",\n )\n frontend_node = await super().update_frontend_node(new_frontend_node, current_frontend_node)\n for key in frontend_node[\"template\"]:\n if key in self.DEFAULT_KEYS:\n continue\n frontend_node[\"template\"] = await self.update_build_config(\n frontend_node[\"template\"], frontend_node[\"template\"][key][\"value\"], key\n )\n frontend_node = await super().update_frontend_node(new_frontend_node, current_frontend_node)\n return frontend_node\n\n def _parse_code(self, code: str) -> tuple[list[dict], list[dict]]:\n parsed_code = ast.parse(code)\n lines = code.split(\"\\n\")\n classes = []\n functions = []\n for node in parsed_code.body:\n if isinstance(node, ast.ClassDef):\n class_lines = lines[node.lineno - 1 : node.end_lineno]\n class_lines[-1] = class_lines[-1][: node.end_col_offset]\n class_lines[0] = class_lines[0][node.col_offset :]\n classes.append(\n {\n \"name\": node.name,\n \"code\": class_lines,\n }\n )\n continue\n\n if not isinstance(node, ast.FunctionDef):\n continue\n\n func = {\"name\": node.name, \"args\": []}\n for arg in node.args.args:\n if arg.lineno != arg.end_lineno:\n msg = \"Multiline arguments are not supported\"\n raise ValueError(msg)\n\n func_arg = {\n \"name\": arg.arg,\n \"annotation\": None,\n }\n\n for default in node.args.defaults:\n if (\n arg.lineno > default.lineno\n or arg.col_offset > default.col_offset\n or (\n arg.end_lineno is not None\n and default.end_lineno is not None\n and arg.end_lineno < default.end_lineno\n )\n or (\n arg.end_col_offset is not None\n and default.end_col_offset is not None\n and arg.end_col_offset < default.end_col_offset\n )\n ):\n continue\n\n if isinstance(default, ast.Name):\n func_arg[\"default\"] = default.id\n elif isinstance(default, ast.Constant):\n func_arg[\"default\"] = str(default.value) if default.value is not None else None\n\n if arg.annotation:\n annotation_line = lines[arg.annotation.lineno - 1]\n annotation_line = annotation_line[: arg.annotation.end_col_offset]\n annotation_line = annotation_line[arg.annotation.col_offset :]\n func_arg[\"annotation\"] = annotation_line\n if isinstance(func_arg[\"annotation\"], str) and func_arg[\"annotation\"].count(\"=\") > 0:\n func_arg[\"annotation\"] = \"=\".join(func_arg[\"annotation\"].split(\"=\")[:-1]).strip()\n if isinstance(func[\"args\"], list):\n func[\"args\"].append(func_arg)\n functions.append(func)\n\n return classes, functions\n\n def _find_imports(self, code: str) -> dotdict:\n imports: list[str] = []\n from_imports = []\n parsed_code = ast.parse(code)\n for node in parsed_code.body:\n if isinstance(node, ast.Import):\n imports.extend(alias.name for alias in node.names)\n elif isinstance(node, ast.ImportFrom):\n from_imports.append(node)\n return dotdict({\"imports\": imports, \"from_imports\": from_imports})\n\n def _get_value(self, value: Any, annotation: Any) -> Any:\n return value if isinstance(value, annotation) else value[\"value\"]\n\n def _find_arg(self, named_functions: dict, func_name: str, arg_name: str) -> dict | None:\n for arg in named_functions[func_name][\"args\"]:\n if arg[\"name\"] == arg_name:\n return arg\n return None\n" + "value": "import ast\nimport json\nfrom typing import Any\n\nfrom langchain.agents import Tool\nfrom langchain_core.tools import StructuredTool\nfrom pydantic.v1 import Field, create_model\nfrom pydantic.v1.fields import Undefined\nfrom typing_extensions import override\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, FieldTypes, HandleInput, MessageTextInput, MultilineInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\n\n\nclass PythonCodeStructuredTool(LCToolComponent):\n DEFAULT_KEYS = [\n \"code\",\n \"_type\",\n \"text_key\",\n \"tool_code\",\n \"tool_name\",\n \"tool_description\",\n \"return_direct\",\n \"tool_function\",\n \"global_variables\",\n \"_classes\",\n \"_functions\",\n ]\n display_name = \"Python Code Structured\"\n description = \"structuredtool dataclass code to tool\"\n documentation = \"https://python.langchain.com/docs/modules/tools/custom_tools/#structuredtool-dataclass\"\n name = \"PythonCodeStructuredTool\"\n icon = \"Python\"\n field_order = [\"name\", \"description\", \"tool_code\", \"return_direct\", \"tool_function\"]\n legacy: bool = True\n replacement = [\"processing.PythonREPLComponent\"]\n\n inputs = [\n MultilineInput(\n name=\"tool_code\",\n display_name=\"Tool Code\",\n info=\"Enter the dataclass code.\",\n placeholder=\"def my_function(args):\\n pass\",\n required=True,\n real_time_refresh=True,\n refresh_button=True,\n ),\n MessageTextInput(\n name=\"tool_name\",\n display_name=\"Tool Name\",\n info=\"Enter the name of the tool.\",\n required=True,\n ),\n MessageTextInput(\n name=\"tool_description\",\n display_name=\"Description\",\n info=\"Enter the description of the tool.\",\n required=True,\n ),\n BoolInput(\n name=\"return_direct\",\n display_name=\"Return Directly\",\n info=\"Should the tool return the function output directly?\",\n ),\n DropdownInput(\n name=\"tool_function\",\n display_name=\"Tool Function\",\n info=\"Select the function for additional expressions.\",\n options=[],\n required=True,\n real_time_refresh=True,\n refresh_button=True,\n ),\n HandleInput(\n name=\"global_variables\",\n display_name=\"Global Variables\",\n info=\"Enter the global variables or Create Data Component.\",\n input_types=[\"Data\", \"JSON\"],\n field_type=FieldTypes.DICT,\n is_list=True,\n ),\n MessageTextInput(name=\"_classes\", display_name=\"Classes\", advanced=True),\n MessageTextInput(name=\"_functions\", display_name=\"Functions\", advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"Tool\", name=\"result_tool\", method=\"build_tool\"),\n ]\n\n @override\n async def update_build_config(\n self, build_config: dotdict, field_value: Any, field_name: str | None = None\n ) -> dotdict:\n if field_name is None:\n return build_config\n\n if field_name not in {\"tool_code\", \"tool_function\"}:\n return build_config\n\n try:\n named_functions = {}\n [classes, functions] = self._parse_code(build_config[\"tool_code\"][\"value\"])\n existing_fields = {}\n if len(build_config) > len(self.DEFAULT_KEYS):\n for key in build_config.copy():\n if key not in self.DEFAULT_KEYS:\n existing_fields[key] = build_config.pop(key)\n\n names = []\n for func in functions:\n named_functions[func[\"name\"]] = func\n names.append(func[\"name\"])\n\n for arg in func[\"args\"]:\n field_name = f\"{func['name']}|{arg['name']}\"\n if field_name in existing_fields:\n build_config[field_name] = existing_fields[field_name]\n continue\n\n field = MessageTextInput(\n display_name=f\"{arg['name']}: Description\",\n name=field_name,\n info=f\"Enter the description for {arg['name']}\",\n required=True,\n )\n build_config[field_name] = field.to_dict()\n build_config[\"_functions\"][\"value\"] = json.dumps(named_functions)\n build_config[\"_classes\"][\"value\"] = json.dumps(classes)\n build_config[\"tool_function\"][\"options\"] = names\n except Exception as e: # noqa: BLE001\n self.status = f\"Failed to extract names: {e}\"\n logger.debug(self.status, exc_info=True)\n build_config[\"tool_function\"][\"options\"] = [\"Failed to parse\", str(e)]\n return build_config\n\n async def build_tool(self) -> Tool:\n local_namespace = {} # type: ignore[var-annotated]\n modules = self._find_imports(self.tool_code)\n import_code = \"\"\n for module in modules[\"imports\"]:\n import_code += f\"global {module}\\nimport {module}\\n\"\n for from_module in modules[\"from_imports\"]:\n for alias in from_module.names:\n import_code += f\"global {alias.name}\\n\"\n import_code += (\n f\"from {from_module.module} import {', '.join([alias.name for alias in from_module.names])}\\n\"\n )\n exec(import_code, globals())\n exec(self.tool_code, globals(), local_namespace)\n\n class PythonCodeToolFunc:\n params: dict = {}\n\n def run(**kwargs):\n for key, arg in kwargs.items():\n if key not in PythonCodeToolFunc.params:\n PythonCodeToolFunc.params[key] = arg\n return local_namespace[self.tool_function](**PythonCodeToolFunc.params)\n\n globals_ = globals()\n local = {}\n local[self.tool_function] = PythonCodeToolFunc\n globals_.update(local)\n\n if isinstance(self.global_variables, list):\n for data in self.global_variables:\n if isinstance(data, Data):\n globals_.update(data.data)\n elif isinstance(self.global_variables, dict):\n globals_.update(self.global_variables)\n\n classes = json.loads(self._attributes[\"_classes\"])\n for class_dict in classes:\n exec(\"\\n\".join(class_dict[\"code\"]), globals_)\n\n named_functions = json.loads(self._attributes[\"_functions\"])\n schema_fields = {}\n\n for attr in self._attributes:\n if attr in self.DEFAULT_KEYS:\n continue\n\n func_name = attr.split(\"|\")[0]\n field_name = attr.split(\"|\")[1]\n func_arg = self._find_arg(named_functions, func_name, field_name)\n if func_arg is None:\n msg = f\"Failed to find arg: {field_name}\"\n raise ValueError(msg)\n\n field_annotation = func_arg[\"annotation\"]\n field_description = self._get_value(self._attributes[attr], str)\n\n if field_annotation:\n exec(f\"temp_annotation_type = {field_annotation}\", globals_)\n schema_annotation = globals_[\"temp_annotation_type\"]\n else:\n schema_annotation = Any\n schema_fields[field_name] = (\n schema_annotation,\n Field(\n default=func_arg.get(\"default\", Undefined),\n description=field_description,\n ),\n )\n\n if \"temp_annotation_type\" in globals_:\n globals_.pop(\"temp_annotation_type\")\n\n python_code_tool_schema = None\n if schema_fields:\n python_code_tool_schema = create_model(\"PythonCodeToolSchema\", **schema_fields)\n\n return StructuredTool.from_function(\n func=local[self.tool_function].run,\n args_schema=python_code_tool_schema,\n name=self.tool_name,\n description=self.tool_description,\n return_direct=self.return_direct,\n )\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 frontend_node[\"template\"] = await self.update_build_config(\n frontend_node[\"template\"],\n frontend_node[\"template\"][\"tool_code\"][\"value\"],\n \"tool_code\",\n )\n frontend_node = await super().update_frontend_node(new_frontend_node, current_frontend_node)\n for key in frontend_node[\"template\"]:\n if key in self.DEFAULT_KEYS:\n continue\n frontend_node[\"template\"] = await self.update_build_config(\n frontend_node[\"template\"], frontend_node[\"template\"][key][\"value\"], key\n )\n frontend_node = await super().update_frontend_node(new_frontend_node, current_frontend_node)\n return frontend_node\n\n def _parse_code(self, code: str) -> tuple[list[dict], list[dict]]:\n parsed_code = ast.parse(code)\n lines = code.split(\"\\n\")\n classes = []\n functions = []\n for node in parsed_code.body:\n if isinstance(node, ast.ClassDef):\n class_lines = lines[node.lineno - 1 : node.end_lineno]\n class_lines[-1] = class_lines[-1][: node.end_col_offset]\n class_lines[0] = class_lines[0][node.col_offset :]\n classes.append(\n {\n \"name\": node.name,\n \"code\": class_lines,\n }\n )\n continue\n\n if not isinstance(node, ast.FunctionDef):\n continue\n\n func = {\"name\": node.name, \"args\": []}\n for arg in node.args.args:\n if arg.lineno != arg.end_lineno:\n msg = \"Multiline arguments are not supported\"\n raise ValueError(msg)\n\n func_arg = {\n \"name\": arg.arg,\n \"annotation\": None,\n }\n\n for default in node.args.defaults:\n if (\n arg.lineno > default.lineno\n or arg.col_offset > default.col_offset\n or (\n arg.end_lineno is not None\n and default.end_lineno is not None\n and arg.end_lineno < default.end_lineno\n )\n or (\n arg.end_col_offset is not None\n and default.end_col_offset is not None\n and arg.end_col_offset < default.end_col_offset\n )\n ):\n continue\n\n if isinstance(default, ast.Name):\n func_arg[\"default\"] = default.id\n elif isinstance(default, ast.Constant):\n func_arg[\"default\"] = str(default.value) if default.value is not None else None\n\n if arg.annotation:\n annotation_line = lines[arg.annotation.lineno - 1]\n annotation_line = annotation_line[: arg.annotation.end_col_offset]\n annotation_line = annotation_line[arg.annotation.col_offset :]\n func_arg[\"annotation\"] = annotation_line\n if isinstance(func_arg[\"annotation\"], str) and func_arg[\"annotation\"].count(\"=\") > 0:\n func_arg[\"annotation\"] = \"=\".join(func_arg[\"annotation\"].split(\"=\")[:-1]).strip()\n if isinstance(func[\"args\"], list):\n func[\"args\"].append(func_arg)\n functions.append(func)\n\n return classes, functions\n\n def _find_imports(self, code: str) -> dotdict:\n imports: list[str] = []\n from_imports = []\n parsed_code = ast.parse(code)\n for node in parsed_code.body:\n if isinstance(node, ast.Import):\n imports.extend(alias.name for alias in node.names)\n elif isinstance(node, ast.ImportFrom):\n from_imports.append(node)\n return dotdict({\"imports\": imports, \"from_imports\": from_imports})\n\n def _get_value(self, value: Any, annotation: Any) -> Any:\n return value if isinstance(value, annotation) else value[\"value\"]\n\n def _find_arg(self, named_functions: dict, func_name: str, arg_name: str) -> dict | None:\n for arg in named_functions[func_name][\"args\"]:\n if arg[\"name\"] == arg_name:\n return arg\n return None\n" }, "global_variables": { "_input_type": "HandleInput", @@ -108335,7 +108502,8 @@ "dynamic": false, "info": "Enter the global variables or Create Data Component.", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -108482,7 +108650,7 @@ }, "PythonREPLTool": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -108536,14 +108704,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -108837,7 +109005,7 @@ }, "SearchAPI": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -108889,14 +109057,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -109077,7 +109245,7 @@ }, "SerpAPI": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -109132,14 +109300,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -109295,7 +109463,7 @@ }, "TavilyAISearch": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -109358,14 +109526,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -109709,7 +109877,7 @@ }, "WikidataAPI": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -109756,14 +109924,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -109840,7 +110008,7 @@ }, "WikipediaAPI": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -109883,14 +110051,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -110052,7 +110220,7 @@ }, "YahooFinanceTool": { "base_classes": [ - "Data", + "JSON", "Tool" ], "beta": false, @@ -110105,14 +110273,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "run_model", "name": "api_run_model", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, @@ -110276,7 +110444,7 @@ "icon": "TwelveLabs", "legacy": false, "metadata": { - "code_hash": "90ad7b9b59eb", + "code_hash": "2a65cbf14ce5", "dependencies": { "dependencies": [ { @@ -110330,7 +110498,8 @@ "dynamic": false, "info": "Search results from Astra DB component", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -110361,14 +110530,14 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom lfx.custom import Component\nfrom lfx.io import HandleInput, Output\nfrom lfx.schema import Data\nfrom lfx.schema.message import Message\n\n\nclass ConvertAstraToTwelveLabs(Component):\n \"\"\"Convert Astra DB search results to TwelveLabs Pegasus inputs.\"\"\"\n\n display_name = \"Convert Astra DB to Pegasus Input\"\n description = \"Converts Astra DB search results to inputs compatible with TwelveLabs Pegasus.\"\n icon = \"TwelveLabs\"\n name = \"ConvertAstraToTwelveLabs\"\n documentation = \"https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md\"\n\n inputs = [\n HandleInput(\n name=\"astra_results\",\n display_name=\"Astra DB Results\",\n input_types=[\"Data\"],\n info=\"Search results from Astra DB component\",\n required=True,\n is_list=True,\n )\n ]\n\n outputs = [\n Output(\n name=\"index_id\",\n display_name=\"Index ID\",\n type_=Message,\n method=\"get_index_id\",\n ),\n Output(\n name=\"video_id\",\n display_name=\"Video ID\",\n type_=Message,\n method=\"get_video_id\",\n ),\n ]\n\n def __init__(self, **kwargs):\n super().__init__(**kwargs)\n self._video_id = None\n self._index_id = None\n\n def build(self, **kwargs: Any) -> None: # noqa: ARG002 - Required for parent class compatibility\n \"\"\"Process the Astra DB results and extract TwelveLabs index information.\"\"\"\n if not self.astra_results:\n return\n\n # Convert to list if single item\n results = self.astra_results if isinstance(self.astra_results, list) else [self.astra_results]\n\n # Try to extract index information from metadata\n for doc in results:\n if not isinstance(doc, Data):\n continue\n\n # Get the metadata, handling the nested structure\n metadata = {}\n if hasattr(doc, \"metadata\") and isinstance(doc.metadata, dict):\n # Handle nested metadata using .get() method\n metadata = doc.metadata.get(\"metadata\", doc.metadata)\n\n # Extract index_id and video_id\n self._index_id = metadata.get(\"index_id\")\n self._video_id = metadata.get(\"video_id\")\n\n # If we found both, we can stop searching\n if self._index_id and self._video_id:\n break\n\n def get_video_id(self) -> Message:\n \"\"\"Return the extracted video ID as a Message.\"\"\"\n self.build()\n return Message(text=self._video_id if self._video_id else \"\")\n\n def get_index_id(self) -> Message:\n \"\"\"Return the extracted index ID as a Message.\"\"\"\n self.build()\n return Message(text=self._index_id if self._index_id else \"\")\n" + "value": "from typing import Any\n\nfrom lfx.custom import Component\nfrom lfx.io import HandleInput, Output\nfrom lfx.schema import Data\nfrom lfx.schema.message import Message\n\n\nclass ConvertAstraToTwelveLabs(Component):\n \"\"\"Convert Astra DB search results to TwelveLabs Pegasus inputs.\"\"\"\n\n display_name = \"Convert Astra DB to Pegasus Input\"\n description = \"Converts Astra DB search results to inputs compatible with TwelveLabs Pegasus.\"\n icon = \"TwelveLabs\"\n name = \"ConvertAstraToTwelveLabs\"\n documentation = \"https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md\"\n\n inputs = [\n HandleInput(\n name=\"astra_results\",\n display_name=\"Astra DB Results\",\n input_types=[\"Data\", \"JSON\"],\n info=\"Search results from Astra DB component\",\n required=True,\n is_list=True,\n )\n ]\n\n outputs = [\n Output(\n name=\"index_id\",\n display_name=\"Index ID\",\n type_=Message,\n method=\"get_index_id\",\n ),\n Output(\n name=\"video_id\",\n display_name=\"Video ID\",\n type_=Message,\n method=\"get_video_id\",\n ),\n ]\n\n def __init__(self, **kwargs):\n super().__init__(**kwargs)\n self._video_id = None\n self._index_id = None\n\n def build(self, **kwargs: Any) -> None: # noqa: ARG002 - Required for parent class compatibility\n \"\"\"Process the Astra DB results and extract TwelveLabs index information.\"\"\"\n if not self.astra_results:\n return\n\n # Convert to list if single item\n results = self.astra_results if isinstance(self.astra_results, list) else [self.astra_results]\n\n # Try to extract index information from metadata\n for doc in results:\n if not isinstance(doc, Data):\n continue\n\n # Get the metadata, handling the nested structure\n metadata = {}\n if hasattr(doc, \"metadata\") and isinstance(doc.metadata, dict):\n # Handle nested metadata using .get() method\n metadata = doc.metadata.get(\"metadata\", doc.metadata)\n\n # Extract index_id and video_id\n self._index_id = metadata.get(\"index_id\")\n self._video_id = metadata.get(\"video_id\")\n\n # If we found both, we can stop searching\n if self._index_id and self._video_id:\n break\n\n def get_video_id(self) -> Message:\n \"\"\"Return the extracted video ID as a Message.\"\"\"\n self.build()\n return Message(text=self._video_id if self._video_id else \"\")\n\n def get_index_id(self) -> Message:\n \"\"\"Return the extracted index ID as a Message.\"\"\"\n self.build()\n return Message(text=self._index_id if self._index_id else \"\")\n" } }, "tool_mode": false }, "SplitVideo": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -110387,7 +110556,7 @@ "icon": "TwelveLabs", "legacy": false, "metadata": { - "code_hash": "4d3a4a724aa5", + "code_hash": "56ccb4106c30", "dependencies": { "dependencies": [ { @@ -110409,10 +110578,10 @@ "group_outputs": false, "method": "process", "name": "clips", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -110456,7 +110625,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import hashlib\nimport math\nimport subprocess\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\n\nfrom lfx.custom import Component\nfrom lfx.inputs import BoolInput, DropdownInput, HandleInput, IntInput\nfrom lfx.schema import Data\nfrom lfx.template import Output\nfrom lfx.utils.validate_cloud import raise_error_if_astra_cloud_disable_component\n\ndisable_component_in_astra_cloud_msg = (\n \"Video processing is not supported in Astra cloud environment. \"\n \"Video components require local file system access for processing. \"\n \"Please use local storage mode or process videos locally before uploading.\"\n)\n\n\nclass SplitVideoComponent(Component):\n \"\"\"A component that splits a video into multiple clips of specified duration using FFmpeg.\"\"\"\n\n display_name = \"Split Video\"\n description = \"Split a video into multiple clips of specified duration.\"\n icon = \"TwelveLabs\"\n name = \"SplitVideo\"\n documentation = \"https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md\"\n\n inputs = [\n HandleInput(\n name=\"videodata\",\n display_name=\"Video Data\",\n info=\"Input video data from VideoFile component\",\n required=True,\n input_types=[\"Data\"],\n ),\n IntInput(\n name=\"clip_duration\",\n display_name=\"Clip Duration (seconds)\",\n info=\"Duration of each clip in seconds\",\n required=True,\n value=30,\n ),\n DropdownInput(\n name=\"last_clip_handling\",\n display_name=\"Last Clip Handling\",\n info=(\n \"How to handle the final clip when it would be shorter than the specified duration:\\n\"\n \"- Truncate: Skip the final clip entirely if it's shorter than the specified duration\\n\"\n \"- Overlap Previous: Start the final clip earlier to maintain full duration, \"\n \"overlapping with previous clip\\n\"\n \"- Keep Short: Keep the final clip at its natural length, even if shorter than specified duration\"\n ),\n options=[\"Truncate\", \"Overlap Previous\", \"Keep Short\"],\n value=\"Overlap Previous\",\n required=True,\n ),\n BoolInput(\n name=\"include_original\",\n display_name=\"Include Original Video\",\n info=\"Whether to include the original video in the output\",\n value=False,\n ),\n ]\n\n outputs = [\n Output(\n name=\"clips\",\n display_name=\"Video Clips\",\n method=\"process\",\n output_types=[\"Data\"],\n ),\n ]\n\n def get_video_duration(self, video_path: str) -> float:\n \"\"\"Get video duration using FFmpeg.\"\"\"\n try:\n # Validate video path to prevent shell injection\n if not isinstance(video_path, str) or any(c in video_path for c in \";&|`$(){}[]<>*?!#~\"):\n error_msg = \"Invalid video path\"\n raise ValueError(error_msg)\n\n cmd = [\n \"ffprobe\",\n \"-v\",\n \"error\",\n \"-show_entries\",\n \"format=duration\",\n \"-of\",\n \"default=noprint_wrappers=1:nokey=1\",\n video_path,\n ]\n result = subprocess.run( # noqa: S603\n cmd,\n capture_output=True,\n text=True,\n check=False,\n shell=False, # Explicitly set shell=False for security\n )\n if result.returncode != 0:\n error_msg = f\"FFprobe error: {result.stderr}\"\n raise RuntimeError(error_msg)\n return float(result.stdout.strip())\n except Exception as e:\n self.log(f\"Error getting video duration: {e!s}\", \"ERROR\")\n raise\n\n def get_output_dir(self, video_path: str) -> str:\n \"\"\"Create a unique output directory for clips based on video name and timestamp.\"\"\"\n # Get the video filename without extension\n path_obj = Path(video_path)\n base_name = path_obj.stem\n\n # Create a timestamp\n timestamp = datetime.now(tz=timezone.utc).strftime(\"%Y-%m-%d_%H-%M-%S\")\n\n # Create a unique hash from the video path\n path_hash = hashlib.sha256(video_path.encode()).hexdigest()[:8]\n\n # Create the output directory path\n output_dir = Path(path_obj.parent) / f\"clips_{base_name}_{timestamp}_{path_hash}\"\n\n # Create the directory if it doesn't exist\n output_dir.mkdir(parents=True, exist_ok=True)\n\n return str(output_dir)\n\n def process_video(self, video_path: str, clip_duration: int, *, include_original: bool) -> list[Data]:\n \"\"\"Process video and split it into clips using FFmpeg.\"\"\"\n try:\n # Get video duration\n total_duration = self.get_video_duration(video_path)\n\n # Calculate number of clips (ceiling to include partial clip)\n num_clips = math.ceil(total_duration / clip_duration)\n self.log(\n f\"Total duration: {total_duration}s, Clip duration: {clip_duration}s, Number of clips: {num_clips}\"\n )\n\n # Create output directory for clips\n output_dir = self.get_output_dir(video_path)\n\n # Get original video info\n path_obj = Path(video_path)\n original_filename = path_obj.name\n original_name = path_obj.stem\n\n # List to store all video paths (including original if requested)\n video_paths: list[Data] = []\n\n # Add original video if requested\n if include_original:\n original_data: dict[str, Any] = {\n \"text\": video_path,\n \"metadata\": {\n \"source\": video_path,\n \"type\": \"video\",\n \"clip_index\": -1, # -1 indicates original video\n \"duration\": int(total_duration), # Convert to int\n \"original_video\": {\n \"name\": original_name,\n \"filename\": original_filename,\n \"path\": video_path,\n \"duration\": int(total_duration), # Convert to int\n \"total_clips\": int(num_clips),\n \"clip_duration\": int(clip_duration),\n },\n },\n }\n video_paths.append(Data(data=original_data))\n\n # Split video into clips\n for i in range(int(num_clips)): # Convert num_clips to int for range\n start_time = float(i * clip_duration) # Convert to float for time calculations\n end_time = min(float((i + 1) * clip_duration), total_duration)\n duration = end_time - start_time\n\n # Handle last clip if it's shorter\n if i == int(num_clips) - 1 and duration < clip_duration: # Convert num_clips to int for comparison\n if self.last_clip_handling == \"Truncate\":\n # Skip if the last clip would be too short\n continue\n if self.last_clip_handling == \"Overlap Previous\" and i > 0:\n # Start from earlier to make full duration\n start_time = total_duration - clip_duration\n duration = clip_duration\n # For \"Keep Short\", we use the original start_time and duration\n\n # Skip if duration is too small (less than 1 second)\n if duration < 1:\n continue\n\n # Generate output path\n output_path = Path(output_dir) / f\"clip_{i:03d}.mp4\"\n output_path_str = str(output_path)\n\n try:\n # Use FFmpeg to split the video\n cmd = [\n \"ffmpeg\",\n \"-i\",\n video_path,\n \"-ss\",\n str(start_time),\n \"-t\",\n str(duration),\n \"-c:v\",\n \"libx264\",\n \"-c:a\",\n \"aac\",\n \"-y\", # Overwrite output file if it exists\n output_path_str,\n ]\n\n result = subprocess.run( # noqa: S603\n cmd,\n capture_output=True,\n text=True,\n check=False,\n shell=False, # Explicitly set shell=False for security\n )\n if result.returncode != 0:\n error_msg = f\"FFmpeg error: {result.stderr}\"\n raise RuntimeError(error_msg)\n\n # Create timestamp string for metadata\n start_min = int(start_time // 60)\n start_sec = int(start_time % 60)\n end_min = int(end_time // 60)\n end_sec = int(end_time % 60)\n timestamp_str = f\"{start_min:02d}:{start_sec:02d} - {end_min:02d}:{end_sec:02d}\"\n\n # Create Data object for the clip\n clip_data: dict[str, Any] = {\n \"text\": output_path_str,\n \"metadata\": {\n \"source\": video_path,\n \"type\": \"video\",\n \"clip_index\": i,\n \"start_time\": float(start_time),\n \"end_time\": float(end_time),\n \"duration\": float(duration),\n \"original_video\": {\n \"name\": original_name,\n \"filename\": original_filename,\n \"path\": video_path,\n \"duration\": int(total_duration),\n \"total_clips\": int(num_clips),\n \"clip_duration\": int(clip_duration),\n },\n \"clip\": {\n \"index\": i,\n \"total\": int(num_clips),\n \"duration\": float(duration),\n \"start_time\": float(start_time),\n \"end_time\": float(end_time),\n \"timestamp\": timestamp_str,\n },\n },\n }\n video_paths.append(Data(data=clip_data))\n\n except Exception as e:\n self.log(f\"Error processing clip {i}: {e!s}\", \"ERROR\")\n raise\n\n self.log(f\"Created {len(video_paths)} clips in {output_dir}\")\n except Exception as e:\n self.log(f\"Error processing video: {e!s}\", \"ERROR\")\n raise\n else:\n return video_paths\n\n def process(self) -> list[Data]:\n \"\"\"Process the input video and return a list of Data objects containing the clips.\"\"\"\n # Check if we're in Astra cloud environment and raise an error if we are.\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n\n try:\n # Get the input video path from the previous component\n if not hasattr(self, \"videodata\") or not isinstance(self.videodata, list) or len(self.videodata) != 1:\n error_msg = \"Please provide exactly one video\"\n raise ValueError(error_msg)\n\n video_path = self.videodata[0].data.get(\"text\")\n if not video_path or not Path(video_path).exists():\n error_msg = \"Invalid video path\"\n raise ValueError(error_msg)\n\n # Validate video path to prevent shell injection\n if not isinstance(video_path, str) or any(c in video_path for c in \";&|`$(){}[]<>*?!#~\"):\n error_msg = \"Invalid video path contains unsafe characters\"\n raise ValueError(error_msg)\n\n # Process the video\n return self.process_video(video_path, self.clip_duration, include_original=self.include_original)\n\n except Exception as e:\n self.log(f\"Error in split video component: {e!s}\", \"ERROR\")\n raise\n" + "value": "import hashlib\nimport math\nimport subprocess\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Any\n\nfrom lfx.custom import Component\nfrom lfx.inputs import BoolInput, DropdownInput, HandleInput, IntInput\nfrom lfx.schema import Data\nfrom lfx.template import Output\nfrom lfx.utils.validate_cloud import raise_error_if_astra_cloud_disable_component\n\ndisable_component_in_astra_cloud_msg = (\n \"Video processing is not supported in Astra cloud environment. \"\n \"Video components require local file system access for processing. \"\n \"Please use local storage mode or process videos locally before uploading.\"\n)\n\n\nclass SplitVideoComponent(Component):\n \"\"\"A component that splits a video into multiple clips of specified duration using FFmpeg.\"\"\"\n\n display_name = \"Split Video\"\n description = \"Split a video into multiple clips of specified duration.\"\n icon = \"TwelveLabs\"\n name = \"SplitVideo\"\n documentation = \"https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md\"\n\n inputs = [\n HandleInput(\n name=\"videodata\",\n display_name=\"Video Data\",\n info=\"Input video data from VideoFile component\",\n required=True,\n input_types=[\"Data\", \"JSON\"],\n ),\n IntInput(\n name=\"clip_duration\",\n display_name=\"Clip Duration (seconds)\",\n info=\"Duration of each clip in seconds\",\n required=True,\n value=30,\n ),\n DropdownInput(\n name=\"last_clip_handling\",\n display_name=\"Last Clip Handling\",\n info=(\n \"How to handle the final clip when it would be shorter than the specified duration:\\n\"\n \"- Truncate: Skip the final clip entirely if it's shorter than the specified duration\\n\"\n \"- Overlap Previous: Start the final clip earlier to maintain full duration, \"\n \"overlapping with previous clip\\n\"\n \"- Keep Short: Keep the final clip at its natural length, even if shorter than specified duration\"\n ),\n options=[\"Truncate\", \"Overlap Previous\", \"Keep Short\"],\n value=\"Overlap Previous\",\n required=True,\n ),\n BoolInput(\n name=\"include_original\",\n display_name=\"Include Original Video\",\n info=\"Whether to include the original video in the output\",\n value=False,\n ),\n ]\n\n outputs = [\n Output(\n name=\"clips\",\n display_name=\"Video Clips\",\n method=\"process\",\n output_types=[\"JSON\"],\n ),\n ]\n\n def get_video_duration(self, video_path: str) -> float:\n \"\"\"Get video duration using FFmpeg.\"\"\"\n try:\n # Validate video path to prevent shell injection\n if not isinstance(video_path, str) or any(c in video_path for c in \";&|`$(){}[]<>*?!#~\"):\n error_msg = \"Invalid video path\"\n raise ValueError(error_msg)\n\n cmd = [\n \"ffprobe\",\n \"-v\",\n \"error\",\n \"-show_entries\",\n \"format=duration\",\n \"-of\",\n \"default=noprint_wrappers=1:nokey=1\",\n video_path,\n ]\n result = subprocess.run( # noqa: S603\n cmd,\n capture_output=True,\n text=True,\n check=False,\n shell=False, # Explicitly set shell=False for security\n )\n if result.returncode != 0:\n error_msg = f\"FFprobe error: {result.stderr}\"\n raise RuntimeError(error_msg)\n return float(result.stdout.strip())\n except Exception as e:\n self.log(f\"Error getting video duration: {e!s}\", \"ERROR\")\n raise\n\n def get_output_dir(self, video_path: str) -> str:\n \"\"\"Create a unique output directory for clips based on video name and timestamp.\"\"\"\n # Get the video filename without extension\n path_obj = Path(video_path)\n base_name = path_obj.stem\n\n # Create a timestamp\n timestamp = datetime.now(tz=timezone.utc).strftime(\"%Y-%m-%d_%H-%M-%S\")\n\n # Create a unique hash from the video path\n path_hash = hashlib.sha256(video_path.encode()).hexdigest()[:8]\n\n # Create the output directory path\n output_dir = Path(path_obj.parent) / f\"clips_{base_name}_{timestamp}_{path_hash}\"\n\n # Create the directory if it doesn't exist\n output_dir.mkdir(parents=True, exist_ok=True)\n\n return str(output_dir)\n\n def process_video(self, video_path: str, clip_duration: int, *, include_original: bool) -> list[Data]:\n \"\"\"Process video and split it into clips using FFmpeg.\"\"\"\n try:\n # Get video duration\n total_duration = self.get_video_duration(video_path)\n\n # Calculate number of clips (ceiling to include partial clip)\n num_clips = math.ceil(total_duration / clip_duration)\n self.log(\n f\"Total duration: {total_duration}s, Clip duration: {clip_duration}s, Number of clips: {num_clips}\"\n )\n\n # Create output directory for clips\n output_dir = self.get_output_dir(video_path)\n\n # Get original video info\n path_obj = Path(video_path)\n original_filename = path_obj.name\n original_name = path_obj.stem\n\n # List to store all video paths (including original if requested)\n video_paths: list[Data] = []\n\n # Add original video if requested\n if include_original:\n original_data: dict[str, Any] = {\n \"text\": video_path,\n \"metadata\": {\n \"source\": video_path,\n \"type\": \"video\",\n \"clip_index\": -1, # -1 indicates original video\n \"duration\": int(total_duration), # Convert to int\n \"original_video\": {\n \"name\": original_name,\n \"filename\": original_filename,\n \"path\": video_path,\n \"duration\": int(total_duration), # Convert to int\n \"total_clips\": int(num_clips),\n \"clip_duration\": int(clip_duration),\n },\n },\n }\n video_paths.append(Data(data=original_data))\n\n # Split video into clips\n for i in range(int(num_clips)): # Convert num_clips to int for range\n start_time = float(i * clip_duration) # Convert to float for time calculations\n end_time = min(float((i + 1) * clip_duration), total_duration)\n duration = end_time - start_time\n\n # Handle last clip if it's shorter\n if i == int(num_clips) - 1 and duration < clip_duration: # Convert num_clips to int for comparison\n if self.last_clip_handling == \"Truncate\":\n # Skip if the last clip would be too short\n continue\n if self.last_clip_handling == \"Overlap Previous\" and i > 0:\n # Start from earlier to make full duration\n start_time = total_duration - clip_duration\n duration = clip_duration\n # For \"Keep Short\", we use the original start_time and duration\n\n # Skip if duration is too small (less than 1 second)\n if duration < 1:\n continue\n\n # Generate output path\n output_path = Path(output_dir) / f\"clip_{i:03d}.mp4\"\n output_path_str = str(output_path)\n\n try:\n # Use FFmpeg to split the video\n cmd = [\n \"ffmpeg\",\n \"-i\",\n video_path,\n \"-ss\",\n str(start_time),\n \"-t\",\n str(duration),\n \"-c:v\",\n \"libx264\",\n \"-c:a\",\n \"aac\",\n \"-y\", # Overwrite output file if it exists\n output_path_str,\n ]\n\n result = subprocess.run( # noqa: S603\n cmd,\n capture_output=True,\n text=True,\n check=False,\n shell=False, # Explicitly set shell=False for security\n )\n if result.returncode != 0:\n error_msg = f\"FFmpeg error: {result.stderr}\"\n raise RuntimeError(error_msg)\n\n # Create timestamp string for metadata\n start_min = int(start_time // 60)\n start_sec = int(start_time % 60)\n end_min = int(end_time // 60)\n end_sec = int(end_time % 60)\n timestamp_str = f\"{start_min:02d}:{start_sec:02d} - {end_min:02d}:{end_sec:02d}\"\n\n # Create Data object for the clip\n clip_data: dict[str, Any] = {\n \"text\": output_path_str,\n \"metadata\": {\n \"source\": video_path,\n \"type\": \"video\",\n \"clip_index\": i,\n \"start_time\": float(start_time),\n \"end_time\": float(end_time),\n \"duration\": float(duration),\n \"original_video\": {\n \"name\": original_name,\n \"filename\": original_filename,\n \"path\": video_path,\n \"duration\": int(total_duration),\n \"total_clips\": int(num_clips),\n \"clip_duration\": int(clip_duration),\n },\n \"clip\": {\n \"index\": i,\n \"total\": int(num_clips),\n \"duration\": float(duration),\n \"start_time\": float(start_time),\n \"end_time\": float(end_time),\n \"timestamp\": timestamp_str,\n },\n },\n }\n video_paths.append(Data(data=clip_data))\n\n except Exception as e:\n self.log(f\"Error processing clip {i}: {e!s}\", \"ERROR\")\n raise\n\n self.log(f\"Created {len(video_paths)} clips in {output_dir}\")\n except Exception as e:\n self.log(f\"Error processing video: {e!s}\", \"ERROR\")\n raise\n else:\n return video_paths\n\n def process(self) -> list[Data]:\n \"\"\"Process the input video and return a list of Data objects containing the clips.\"\"\"\n # Check if we're in Astra cloud environment and raise an error if we are.\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n\n try:\n # Get the input video path from the previous component\n if not hasattr(self, \"videodata\") or not isinstance(self.videodata, list) or len(self.videodata) != 1:\n error_msg = \"Please provide exactly one video\"\n raise ValueError(error_msg)\n\n video_path = self.videodata[0].data.get(\"text\")\n if not video_path or not Path(video_path).exists():\n error_msg = \"Invalid video path\"\n raise ValueError(error_msg)\n\n # Validate video path to prevent shell injection\n if not isinstance(video_path, str) or any(c in video_path for c in \";&|`$(){}[]<>*?!#~\"):\n error_msg = \"Invalid video path contains unsafe characters\"\n raise ValueError(error_msg)\n\n # Process the video\n return self.process_video(video_path, self.clip_duration, include_original=self.include_original)\n\n except Exception as e:\n self.log(f\"Error in split video component: {e!s}\", \"ERROR\")\n raise\n" }, "include_original": { "_input_type": "BoolInput", @@ -110513,7 +110682,8 @@ "dynamic": false, "info": "Input video data from VideoFile component", "input_types": [ - "Data" + "Data", + "JSON" ], "list": false, "list_add_label": "Add More", @@ -110809,13 +110979,14 @@ "value": "" }, "videodata": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Video Data", "dynamic": false, "info": "Video Data", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -110837,7 +111008,7 @@ }, "TwelveLabsPegasusIndexVideo": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -110857,7 +111028,7 @@ "icon": "TwelveLabs", "legacy": false, "metadata": { - "code_hash": "a2c0865be096", + "code_hash": "faa14b3d6a18", "dependencies": { "dependencies": [ { @@ -110887,10 +111058,10 @@ "group_outputs": false, "method": "index_videos", "name": "indexed_data", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -110933,7 +111104,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import time\nfrom concurrent.futures import ThreadPoolExecutor\nfrom pathlib import Path\nfrom typing import Any\n\nfrom tenacity import retry, stop_after_attempt, wait_exponential\nfrom twelvelabs import TwelveLabs\n\nfrom lfx.custom import Component\nfrom lfx.inputs import DataInput, DropdownInput, SecretStrInput, StrInput\nfrom lfx.io import Output\nfrom lfx.schema import Data\n\n\nclass TwelveLabsError(Exception):\n \"\"\"Base exception for TwelveLabs errors.\"\"\"\n\n\nclass IndexCreationError(TwelveLabsError):\n \"\"\"Error raised when there's an issue with an index.\"\"\"\n\n\nclass TaskError(TwelveLabsError):\n \"\"\"Error raised when a task fails.\"\"\"\n\n\nclass TaskTimeoutError(TwelveLabsError):\n \"\"\"Error raised when a task times out.\"\"\"\n\n\nclass PegasusIndexVideo(Component):\n \"\"\"Indexes videos using TwelveLabs Pegasus API and adds the video ID to metadata.\"\"\"\n\n display_name = \"TwelveLabs Pegasus Index Video\"\n description = \"Index videos using TwelveLabs and add the video_id to metadata.\"\n icon = \"TwelveLabs\"\n name = \"TwelveLabsPegasusIndexVideo\"\n documentation = \"https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md\"\n\n inputs = [\n DataInput(\n name=\"videodata\",\n display_name=\"Video Data\",\n info=\"Video Data objects (from VideoFile or SplitVideo)\",\n is_list=True,\n required=True,\n ),\n SecretStrInput(\n name=\"api_key\", display_name=\"TwelveLabs API Key\", info=\"Enter your TwelveLabs API Key.\", required=True\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model\",\n info=\"Pegasus model to use for indexing\",\n options=[\"pegasus1.2\"],\n value=\"pegasus1.2\",\n advanced=False,\n ),\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n info=\"Name of the index to use. If the index doesn't exist, it will be created.\",\n required=False,\n ),\n StrInput(\n name=\"index_id\",\n display_name=\"Index ID\",\n info=\"ID of an existing index to use. If provided, index_name will be ignored.\",\n required=False,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Indexed Data\", name=\"indexed_data\", method=\"index_videos\", output_types=[\"Data\"], is_list=True\n ),\n ]\n\n def _get_or_create_index(self, client: TwelveLabs) -> tuple[str, str]:\n \"\"\"Get existing index or create new one.\n\n Returns (index_id, index_name).\n \"\"\"\n # First check if index_id is provided and valid\n if hasattr(self, \"index_id\") and self.index_id:\n try:\n index = client.index.retrieve(id=self.index_id)\n except (ValueError, KeyError) as e:\n if not hasattr(self, \"index_name\") or not self.index_name:\n error_msg = \"Invalid index ID provided and no index name specified for fallback\"\n raise IndexCreationError(error_msg) from e\n else:\n return self.index_id, index.name\n\n # If index_name is provided, try to find it\n if hasattr(self, \"index_name\") and self.index_name:\n try:\n # List all indexes and find by name\n indexes = client.index.list()\n for idx in indexes:\n if idx.name == self.index_name:\n return idx.id, idx.name\n\n # If we get here, index wasn't found - create it\n index = client.index.create(\n name=self.index_name,\n models=[\n {\n \"name\": self.model_name if hasattr(self, \"model_name\") else \"pegasus1.2\",\n \"options\": [\"visual\", \"audio\"],\n }\n ],\n )\n except (ValueError, KeyError) as e:\n error_msg = f\"Error with index name {self.index_name}\"\n raise IndexCreationError(error_msg) from e\n else:\n return index.id, index.name\n\n # If we get here, neither index_id nor index_name was provided\n error_msg = \"Either index_name or index_id must be provided\"\n raise IndexCreationError(error_msg)\n\n def on_task_update(self, task: Any, video_path: str) -> None:\n \"\"\"Callback for task status updates.\n\n Updates the component status with the current task status.\n \"\"\"\n video_name = Path(video_path).name\n status_msg = f\"Indexing {video_name}... Status: {task.status}\"\n self.status = status_msg\n\n @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=5, max=60), reraise=True)\n def _check_task_status(\n self,\n client: TwelveLabs,\n task_id: str,\n video_path: str,\n ) -> Any:\n \"\"\"Check task status once.\n\n Makes a single API call to check the status of a task.\n \"\"\"\n task = client.task.retrieve(id=task_id)\n self.on_task_update(task, video_path)\n return task\n\n def _wait_for_task_completion(\n self, client: TwelveLabs, task_id: str, video_path: str, max_retries: int = 120, sleep_time: int = 10\n ) -> Any:\n \"\"\"Wait for task completion with timeout and improved error handling.\n\n Polls the task status until completion or timeout.\n \"\"\"\n retries = 0\n consecutive_errors = 0\n max_consecutive_errors = 5\n video_name = Path(video_path).name\n\n while retries < max_retries:\n try:\n self.status = f\"Checking task status for {video_name} (attempt {retries + 1})\"\n task = self._check_task_status(client, task_id, video_path)\n\n if task.status == \"ready\":\n self.status = f\"Indexing for {video_name} completed successfully!\"\n return task\n if task.status == \"failed\":\n error_msg = f\"Task failed for {video_name}: {getattr(task, 'error', 'Unknown error')}\"\n self.status = error_msg\n raise TaskError(error_msg)\n if task.status == \"error\":\n error_msg = f\"Task encountered an error for {video_name}: {getattr(task, 'error', 'Unknown error')}\"\n self.status = error_msg\n raise TaskError(error_msg)\n\n time.sleep(sleep_time)\n retries += 1\n elapsed_time = retries * sleep_time\n self.status = f\"Indexing {video_name}... {elapsed_time}s elapsed\"\n\n except (ValueError, KeyError) as e:\n consecutive_errors += 1\n error_msg = f\"Error checking task status for {video_name}: {e!s}\"\n self.status = error_msg\n\n if consecutive_errors >= max_consecutive_errors:\n too_many_errors = f\"Too many consecutive errors checking task status for {video_name}\"\n raise TaskError(too_many_errors) from e\n\n time.sleep(sleep_time * (2**consecutive_errors))\n continue\n\n timeout_msg = f\"Timeout waiting for indexing of {video_name} after {max_retries * sleep_time} seconds\"\n self.status = timeout_msg\n raise TaskTimeoutError(timeout_msg)\n\n def _upload_video(self, client: TwelveLabs, video_path: str, index_id: str) -> str:\n \"\"\"Upload a single video and return its task ID.\n\n Uploads a video file to the specified index and returns the task ID.\n \"\"\"\n video_name = Path(video_path).name\n with Path(video_path).open(\"rb\") as video_file:\n self.status = f\"Uploading {video_name} to index {index_id}...\"\n task = client.task.create(index_id=index_id, file=video_file)\n task_id = task.id\n self.status = f\"Upload complete for {video_name}. Task ID: {task_id}\"\n return task_id\n\n def index_videos(self) -> list[Data]:\n \"\"\"Indexes each video and adds the video_id to its metadata.\"\"\"\n if not self.videodata:\n self.status = \"No video data provided.\"\n return []\n\n if not self.api_key:\n error_msg = \"TwelveLabs API Key is required\"\n raise IndexCreationError(error_msg)\n\n if not (hasattr(self, \"index_name\") and self.index_name) and not (hasattr(self, \"index_id\") and self.index_id):\n error_msg = \"Either index_name or index_id must be provided\"\n raise IndexCreationError(error_msg)\n\n client = TwelveLabs(api_key=self.api_key)\n indexed_data_list: list[Data] = []\n\n # Get or create the index\n try:\n index_id, index_name = self._get_or_create_index(client)\n self.status = f\"Using index: {index_name} (ID: {index_id})\"\n except IndexCreationError as e:\n self.status = f\"Failed to get/create TwelveLabs index: {e!s}\"\n raise\n\n # First, validate all videos and create a list of valid ones\n valid_videos: list[tuple[Data, str]] = []\n for video_data_item in self.videodata:\n if not isinstance(video_data_item, Data):\n self.status = f\"Skipping invalid data item: {video_data_item}\"\n continue\n\n video_info = video_data_item.data\n if not isinstance(video_info, dict):\n self.status = f\"Skipping item with invalid data structure: {video_info}\"\n continue\n\n video_path = video_info.get(\"text\")\n if not video_path or not isinstance(video_path, str):\n self.status = f\"Skipping item with missing or invalid video path: {video_info}\"\n continue\n\n if not Path(video_path).exists():\n self.status = f\"Video file not found, skipping: {video_path}\"\n continue\n\n valid_videos.append((video_data_item, video_path))\n\n if not valid_videos:\n self.status = \"No valid videos to process.\"\n return []\n\n # Upload all videos first and collect their task IDs\n upload_tasks: list[tuple[Data, str, str]] = [] # (data_item, video_path, task_id)\n for data_item, video_path in valid_videos:\n try:\n task_id = self._upload_video(client, video_path, index_id)\n upload_tasks.append((data_item, video_path, task_id))\n except (ValueError, KeyError) as e:\n self.status = f\"Failed to upload {video_path}: {e!s}\"\n continue\n\n # Now check all tasks in parallel using a thread pool\n with ThreadPoolExecutor(max_workers=min(10, len(upload_tasks))) as executor:\n futures = []\n for data_item, video_path, task_id in upload_tasks:\n future = executor.submit(self._wait_for_task_completion, client, task_id, video_path)\n futures.append((data_item, video_path, future))\n\n # Process results as they complete\n for data_item, video_path, future in futures:\n try:\n completed_task = future.result()\n if completed_task.status == \"ready\":\n video_id = completed_task.video_id\n video_name = Path(video_path).name\n self.status = f\"Video {video_name} indexed successfully. Video ID: {video_id}\"\n\n # Add video_id to the metadata\n video_info = data_item.data\n if \"metadata\" not in video_info:\n video_info[\"metadata\"] = {}\n elif not isinstance(video_info[\"metadata\"], dict):\n self.status = f\"Warning: Overwriting non-dict metadata for {video_path}\"\n video_info[\"metadata\"] = {}\n\n video_info[\"metadata\"].update(\n {\"video_id\": video_id, \"index_id\": index_id, \"index_name\": index_name}\n )\n\n updated_data_item = Data(data=video_info)\n indexed_data_list.append(updated_data_item)\n except (TaskError, TaskTimeoutError) as e:\n self.status = f\"Failed to process {video_path}: {e!s}\"\n\n if not indexed_data_list:\n self.status = \"No videos were successfully indexed.\"\n else:\n self.status = f\"Finished indexing {len(indexed_data_list)}/{len(self.videodata)} videos.\"\n\n return indexed_data_list\n" + "value": "import time\nfrom concurrent.futures import ThreadPoolExecutor\nfrom pathlib import Path\nfrom typing import Any\n\nfrom tenacity import retry, stop_after_attempt, wait_exponential\nfrom twelvelabs import TwelveLabs\n\nfrom lfx.custom import Component\nfrom lfx.inputs import DataInput, DropdownInput, SecretStrInput, StrInput\nfrom lfx.io import Output\nfrom lfx.schema import Data\n\n\nclass TwelveLabsError(Exception):\n \"\"\"Base exception for TwelveLabs errors.\"\"\"\n\n\nclass IndexCreationError(TwelveLabsError):\n \"\"\"Error raised when there's an issue with an index.\"\"\"\n\n\nclass TaskError(TwelveLabsError):\n \"\"\"Error raised when a task fails.\"\"\"\n\n\nclass TaskTimeoutError(TwelveLabsError):\n \"\"\"Error raised when a task times out.\"\"\"\n\n\nclass PegasusIndexVideo(Component):\n \"\"\"Indexes videos using TwelveLabs Pegasus API and adds the video ID to metadata.\"\"\"\n\n display_name = \"TwelveLabs Pegasus Index Video\"\n description = \"Index videos using TwelveLabs and add the video_id to metadata.\"\n icon = \"TwelveLabs\"\n name = \"TwelveLabsPegasusIndexVideo\"\n documentation = \"https://github.com/twelvelabs-io/twelvelabs-developer-experience/blob/main/integrations/Langflow/TWELVE_LABS_COMPONENTS_README.md\"\n\n inputs = [\n DataInput(\n name=\"videodata\",\n display_name=\"Video Data\",\n info=\"Video Data objects (from VideoFile or SplitVideo)\",\n is_list=True,\n required=True,\n ),\n SecretStrInput(\n name=\"api_key\", display_name=\"TwelveLabs API Key\", info=\"Enter your TwelveLabs API Key.\", required=True\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model\",\n info=\"Pegasus model to use for indexing\",\n options=[\"pegasus1.2\"],\n value=\"pegasus1.2\",\n advanced=False,\n ),\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n info=\"Name of the index to use. If the index doesn't exist, it will be created.\",\n required=False,\n ),\n StrInput(\n name=\"index_id\",\n display_name=\"Index ID\",\n info=\"ID of an existing index to use. If provided, index_name will be ignored.\",\n required=False,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Indexed Data\", name=\"indexed_data\", method=\"index_videos\", output_types=[\"JSON\"], is_list=True\n ),\n ]\n\n def _get_or_create_index(self, client: TwelveLabs) -> tuple[str, str]:\n \"\"\"Get existing index or create new one.\n\n Returns (index_id, index_name).\n \"\"\"\n # First check if index_id is provided and valid\n if hasattr(self, \"index_id\") and self.index_id:\n try:\n index = client.index.retrieve(id=self.index_id)\n except (ValueError, KeyError) as e:\n if not hasattr(self, \"index_name\") or not self.index_name:\n error_msg = \"Invalid index ID provided and no index name specified for fallback\"\n raise IndexCreationError(error_msg) from e\n else:\n return self.index_id, index.name\n\n # If index_name is provided, try to find it\n if hasattr(self, \"index_name\") and self.index_name:\n try:\n # List all indexes and find by name\n indexes = client.index.list()\n for idx in indexes:\n if idx.name == self.index_name:\n return idx.id, idx.name\n\n # If we get here, index wasn't found - create it\n index = client.index.create(\n name=self.index_name,\n models=[\n {\n \"name\": self.model_name if hasattr(self, \"model_name\") else \"pegasus1.2\",\n \"options\": [\"visual\", \"audio\"],\n }\n ],\n )\n except (ValueError, KeyError) as e:\n error_msg = f\"Error with index name {self.index_name}\"\n raise IndexCreationError(error_msg) from e\n else:\n return index.id, index.name\n\n # If we get here, neither index_id nor index_name was provided\n error_msg = \"Either index_name or index_id must be provided\"\n raise IndexCreationError(error_msg)\n\n def on_task_update(self, task: Any, video_path: str) -> None:\n \"\"\"Callback for task status updates.\n\n Updates the component status with the current task status.\n \"\"\"\n video_name = Path(video_path).name\n status_msg = f\"Indexing {video_name}... Status: {task.status}\"\n self.status = status_msg\n\n @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=5, max=60), reraise=True)\n def _check_task_status(\n self,\n client: TwelveLabs,\n task_id: str,\n video_path: str,\n ) -> Any:\n \"\"\"Check task status once.\n\n Makes a single API call to check the status of a task.\n \"\"\"\n task = client.task.retrieve(id=task_id)\n self.on_task_update(task, video_path)\n return task\n\n def _wait_for_task_completion(\n self, client: TwelveLabs, task_id: str, video_path: str, max_retries: int = 120, sleep_time: int = 10\n ) -> Any:\n \"\"\"Wait for task completion with timeout and improved error handling.\n\n Polls the task status until completion or timeout.\n \"\"\"\n retries = 0\n consecutive_errors = 0\n max_consecutive_errors = 5\n video_name = Path(video_path).name\n\n while retries < max_retries:\n try:\n self.status = f\"Checking task status for {video_name} (attempt {retries + 1})\"\n task = self._check_task_status(client, task_id, video_path)\n\n if task.status == \"ready\":\n self.status = f\"Indexing for {video_name} completed successfully!\"\n return task\n if task.status == \"failed\":\n error_msg = f\"Task failed for {video_name}: {getattr(task, 'error', 'Unknown error')}\"\n self.status = error_msg\n raise TaskError(error_msg)\n if task.status == \"error\":\n error_msg = f\"Task encountered an error for {video_name}: {getattr(task, 'error', 'Unknown error')}\"\n self.status = error_msg\n raise TaskError(error_msg)\n\n time.sleep(sleep_time)\n retries += 1\n elapsed_time = retries * sleep_time\n self.status = f\"Indexing {video_name}... {elapsed_time}s elapsed\"\n\n except (ValueError, KeyError) as e:\n consecutive_errors += 1\n error_msg = f\"Error checking task status for {video_name}: {e!s}\"\n self.status = error_msg\n\n if consecutive_errors >= max_consecutive_errors:\n too_many_errors = f\"Too many consecutive errors checking task status for {video_name}\"\n raise TaskError(too_many_errors) from e\n\n time.sleep(sleep_time * (2**consecutive_errors))\n continue\n\n timeout_msg = f\"Timeout waiting for indexing of {video_name} after {max_retries * sleep_time} seconds\"\n self.status = timeout_msg\n raise TaskTimeoutError(timeout_msg)\n\n def _upload_video(self, client: TwelveLabs, video_path: str, index_id: str) -> str:\n \"\"\"Upload a single video and return its task ID.\n\n Uploads a video file to the specified index and returns the task ID.\n \"\"\"\n video_name = Path(video_path).name\n with Path(video_path).open(\"rb\") as video_file:\n self.status = f\"Uploading {video_name} to index {index_id}...\"\n task = client.task.create(index_id=index_id, file=video_file)\n task_id = task.id\n self.status = f\"Upload complete for {video_name}. Task ID: {task_id}\"\n return task_id\n\n def index_videos(self) -> list[Data]:\n \"\"\"Indexes each video and adds the video_id to its metadata.\"\"\"\n if not self.videodata:\n self.status = \"No video data provided.\"\n return []\n\n if not self.api_key:\n error_msg = \"TwelveLabs API Key is required\"\n raise IndexCreationError(error_msg)\n\n if not (hasattr(self, \"index_name\") and self.index_name) and not (hasattr(self, \"index_id\") and self.index_id):\n error_msg = \"Either index_name or index_id must be provided\"\n raise IndexCreationError(error_msg)\n\n client = TwelveLabs(api_key=self.api_key)\n indexed_data_list: list[Data] = []\n\n # Get or create the index\n try:\n index_id, index_name = self._get_or_create_index(client)\n self.status = f\"Using index: {index_name} (ID: {index_id})\"\n except IndexCreationError as e:\n self.status = f\"Failed to get/create TwelveLabs index: {e!s}\"\n raise\n\n # First, validate all videos and create a list of valid ones\n valid_videos: list[tuple[Data, str]] = []\n for video_data_item in self.videodata:\n if not isinstance(video_data_item, Data):\n self.status = f\"Skipping invalid data item: {video_data_item}\"\n continue\n\n video_info = video_data_item.data\n if not isinstance(video_info, dict):\n self.status = f\"Skipping item with invalid data structure: {video_info}\"\n continue\n\n video_path = video_info.get(\"text\")\n if not video_path or not isinstance(video_path, str):\n self.status = f\"Skipping item with missing or invalid video path: {video_info}\"\n continue\n\n if not Path(video_path).exists():\n self.status = f\"Video file not found, skipping: {video_path}\"\n continue\n\n valid_videos.append((video_data_item, video_path))\n\n if not valid_videos:\n self.status = \"No valid videos to process.\"\n return []\n\n # Upload all videos first and collect their task IDs\n upload_tasks: list[tuple[Data, str, str]] = [] # (data_item, video_path, task_id)\n for data_item, video_path in valid_videos:\n try:\n task_id = self._upload_video(client, video_path, index_id)\n upload_tasks.append((data_item, video_path, task_id))\n except (ValueError, KeyError) as e:\n self.status = f\"Failed to upload {video_path}: {e!s}\"\n continue\n\n # Now check all tasks in parallel using a thread pool\n with ThreadPoolExecutor(max_workers=min(10, len(upload_tasks))) as executor:\n futures = []\n for data_item, video_path, task_id in upload_tasks:\n future = executor.submit(self._wait_for_task_completion, client, task_id, video_path)\n futures.append((data_item, video_path, future))\n\n # Process results as they complete\n for data_item, video_path, future in futures:\n try:\n completed_task = future.result()\n if completed_task.status == \"ready\":\n video_id = completed_task.video_id\n video_name = Path(video_path).name\n self.status = f\"Video {video_name} indexed successfully. Video ID: {video_id}\"\n\n # Add video_id to the metadata\n video_info = data_item.data\n if \"metadata\" not in video_info:\n video_info[\"metadata\"] = {}\n elif not isinstance(video_info[\"metadata\"], dict):\n self.status = f\"Warning: Overwriting non-dict metadata for {video_path}\"\n video_info[\"metadata\"] = {}\n\n video_info[\"metadata\"].update(\n {\"video_id\": video_id, \"index_id\": index_id, \"index_name\": index_name}\n )\n\n updated_data_item = Data(data=video_info)\n indexed_data_list.append(updated_data_item)\n except (TaskError, TaskTimeoutError) as e:\n self.status = f\"Failed to process {video_path}: {e!s}\"\n\n if not indexed_data_list:\n self.status = \"No videos were successfully indexed.\"\n else:\n self.status = f\"Finished indexing {len(indexed_data_list)}/{len(self.videodata)} videos.\"\n\n return indexed_data_list\n" }, "index_id": { "_input_type": "StrInput", @@ -111004,13 +111175,14 @@ "value": "pegasus1.2" }, "videodata": { - "_input_type": "DataInput", + "_input_type": "JSONInput", "advanced": false, "display_name": "Video Data", "dynamic": false, "info": "Video Data objects (from VideoFile or SplitVideo)", "input_types": [ - "Data" + "Data", + "JSON" ], "list": true, "list_add_label": "Add More", @@ -111339,7 +111511,7 @@ }, "VideoFile": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -111377,10 +111549,10 @@ "group_outputs": false, "method": "load_files", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -111467,7 +111639,7 @@ { "Unstructured": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -111519,10 +111691,10 @@ "group_outputs": false, "method": "load_files", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -111651,6 +111823,7 @@ "info": "Data object with a 'file_path' property pointing to server file or a Message object with a path to the file. Supercedes 'Path' but supports same file types.", "input_types": [ "Data", + "JSON", "Message" ], "list": true, @@ -111834,8 +112007,8 @@ { "Upstash": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -111886,24 +112059,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -112160,7 +112333,7 @@ { "CalculatorComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -112176,7 +112349,7 @@ "icon": "calculator", "legacy": false, "metadata": { - "code_hash": "acbe2603b034", + "code_hash": "37caa1aba62c", "dependencies": { "dependencies": [ { @@ -112194,14 +112367,14 @@ { "allows_loop": false, "cache": true, - "display_name": "Data", + "display_name": "JSON", "group_outputs": false, "method": "evaluate_expression", "name": "result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -112225,7 +112398,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" + "value": "import ast\nimport operator\nfrom collections.abc import Callable\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MessageTextInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\n\nclass CalculatorComponent(Component):\n display_name = \"Calculator\"\n description = \"Perform basic arithmetic operations on a given expression.\"\n documentation: str = \"https://docs.langflow.org/calculator\"\n icon = \"calculator\"\n\n # Cache operators dictionary as a class variable\n OPERATORS: dict[type[ast.operator], Callable] = {\n ast.Add: operator.add,\n ast.Sub: operator.sub,\n ast.Mult: operator.mul,\n ast.Div: operator.truediv,\n ast.Pow: operator.pow,\n }\n\n inputs = [\n MessageTextInput(\n name=\"expression\",\n display_name=\"Expression\",\n info=\"The arithmetic expression to evaluate (e.g., '4*4*(33/22)+12-20').\",\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"JSON\", name=\"result\", type_=Data, method=\"evaluate_expression\"),\n ]\n\n def _eval_expr(self, node: ast.AST) -> float:\n \"\"\"Evaluate an AST node recursively.\"\"\"\n if isinstance(node, ast.Constant):\n if isinstance(node.value, int | float):\n return float(node.value)\n error_msg = f\"Unsupported constant type: {type(node.value).__name__}\"\n raise TypeError(error_msg)\n if isinstance(node, ast.Num): # For backwards compatibility\n if isinstance(node.n, int | float):\n return float(node.n)\n error_msg = f\"Unsupported number type: {type(node.n).__name__}\"\n raise TypeError(error_msg)\n\n if isinstance(node, ast.BinOp):\n op_type = type(node.op)\n if op_type not in self.OPERATORS:\n error_msg = f\"Unsupported binary operator: {op_type.__name__}\"\n raise TypeError(error_msg)\n\n left = self._eval_expr(node.left)\n right = self._eval_expr(node.right)\n return self.OPERATORS[op_type](left, right)\n\n error_msg = f\"Unsupported operation or expression type: {type(node).__name__}\"\n raise TypeError(error_msg)\n\n def evaluate_expression(self) -> Data:\n \"\"\"Evaluate the mathematical expression and return the result.\"\"\"\n try:\n tree = ast.parse(self.expression, mode=\"eval\")\n result = self._eval_expr(tree.body)\n\n formatted_result = f\"{float(result):.6f}\".rstrip(\"0\").rstrip(\".\")\n self.log(f\"Calculation result: {formatted_result}\")\n\n self.status = formatted_result\n return Data(data={\"result\": formatted_result})\n\n except ZeroDivisionError:\n error_message = \"Error: Division by zero\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n except (SyntaxError, TypeError, KeyError, ValueError, AttributeError, OverflowError) as e:\n error_message = f\"Invalid expression: {e!s}\"\n self.status = error_message\n return Data(data={\"error\": error_message, \"input\": self.expression})\n\n def build(self):\n \"\"\"Return the main evaluation function.\"\"\"\n return self.evaluate_expression\n" }, "expression": { "_input_type": "MessageTextInput", @@ -112456,7 +112629,7 @@ }, "PythonREPLComponent": { "base_classes": [ - "Data" + "JSON" ], "beta": false, "conditional_paths": [], @@ -112499,10 +112672,10 @@ "group_outputs": false, "method": "run_python_repl", "name": "results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -112588,8 +112761,8 @@ { "Vectara": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -112638,24 +112811,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -113276,7 +113449,7 @@ { "LocalDB": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -113302,7 +113475,7 @@ "icon": "database", "legacy": true, "metadata": { - "code_hash": "767988a11f4a", + "code_hash": "457b336fd756", "dependencies": { "dependencies": [ { @@ -113328,14 +113501,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "perform_search", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -113379,7 +113552,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from copy import deepcopy\nfrom pathlib import Path\n\nfrom langchain_chroma import Chroma\nfrom typing_extensions import override\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.base.vectorstores.utils import chroma_collection_to_data\nfrom lfx.inputs.inputs import MultilineInput\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, TabInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\nfrom lfx.utils.validate_cloud import raise_error_if_astra_cloud_disable_component\n\ndisable_component_in_astra_cloud_msg = (\n \"Local vector stores are not supported in S3/cloud mode. \"\n \"Local vector stores require local file system access for persistence. \"\n \"Please use cloud-based vector stores (Pinecone, Weaviate, etc.) or local storage mode.\"\n)\n\n\nclass LocalDBComponent(LCVectorStoreComponent):\n \"\"\"Chroma Vector Store with search capabilities.\"\"\"\n\n display_name: str = \"Local DB\"\n description: str = \"Local Vector Store with search capabilities\"\n name = \"LocalDB\"\n icon = \"database\"\n legacy = True\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Ingest\", \"Retrieve\"],\n info=\"Select the operation mode\",\n value=\"Ingest\",\n real_time_refresh=True,\n show=True,\n ),\n MessageTextInput(\n name=\"collection_name\",\n display_name=\"Collection Name\",\n value=\"langflow\",\n required=True,\n ),\n MessageTextInput(\n name=\"persist_directory\",\n display_name=\"Persist Directory\",\n info=(\n \"Custom base directory to save the vector store. \"\n \"Collections will be stored under '{directory}/vector_stores/{collection_name}'. \"\n \"If not specified, it will use your system's cache folder.\"\n ),\n advanced=True,\n ),\n DropdownInput(\n name=\"existing_collections\",\n display_name=\"Existing Collections\",\n options=[], # Will be populated dynamically\n info=\"Select a previously created collection to search through its stored data.\",\n show=False,\n combobox=True,\n ),\n HandleInput(name=\"embedding\", display_name=\"Embedding\", required=True, input_types=[\"Embeddings\"]),\n BoolInput(\n name=\"allow_duplicates\",\n display_name=\"Allow Duplicates\",\n advanced=True,\n info=\"If false, will not add documents that are already in the Vector Store.\",\n ),\n DropdownInput(\n name=\"search_type\",\n display_name=\"Search Type\",\n options=[\"Similarity\", \"MMR\"],\n value=\"Similarity\",\n advanced=True,\n ),\n HandleInput(\n name=\"ingest_data\",\n display_name=\"Ingest Data\",\n input_types=[\"Data\", \"DataFrame\"],\n is_list=True,\n info=\"Data to store. It will be embedded and indexed for semantic search.\",\n show=True,\n ),\n MultilineInput(\n name=\"search_query\",\n display_name=\"Search Query\",\n tool_mode=True,\n info=\"Enter text to search for similar content in the selected collection.\",\n show=False,\n ),\n IntInput(\n name=\"number_of_results\",\n display_name=\"Number of Results\",\n info=\"Number of results to return.\",\n advanced=True,\n value=10,\n ),\n IntInput(\n name=\"limit\",\n display_name=\"Limit\",\n advanced=True,\n info=\"Limit the number of records to compare when Allow Duplicates is False.\",\n ),\n ]\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"perform_search\"),\n ]\n\n def get_vector_store_directory(self, base_dir: str | Path) -> Path:\n \"\"\"Get the full directory path for a collection.\"\"\"\n # Ensure base_dir is a Path object\n base_dir = Path(base_dir)\n # Create the full path: base_dir/vector_stores/collection_name\n full_path = base_dir / \"vector_stores\" / self.collection_name\n # Create the directory if it doesn't exist\n full_path.mkdir(parents=True, exist_ok=True)\n return full_path\n\n def get_default_persist_dir(self) -> str:\n \"\"\"Get the default persist directory from cache.\"\"\"\n from lfx.services.cache.utils import CACHE_DIR\n\n return str(self.get_vector_store_directory(CACHE_DIR))\n\n def list_existing_collections(self) -> list[str]:\n \"\"\"List existing vector store collections from the persist directory.\"\"\"\n from lfx.services.cache.utils import CACHE_DIR\n\n # Get the base directory (either custom or cache)\n base_dir = Path(self.persist_directory) if self.persist_directory else Path(CACHE_DIR)\n # Get the vector_stores subdirectory\n vector_stores_dir = base_dir / \"vector_stores\"\n if not vector_stores_dir.exists():\n return []\n\n return [d.name for d in vector_stores_dir.iterdir() if d.is_dir()]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n \"\"\"Update the build configuration when the mode changes.\"\"\"\n if field_name == \"mode\":\n # Hide all dynamic fields by default\n dynamic_fields = [\n \"ingest_data\",\n \"search_query\",\n \"search_type\",\n \"number_of_results\",\n \"existing_collections\",\n \"collection_name\",\n \"embedding\",\n \"allow_duplicates\",\n \"limit\",\n ]\n for field in dynamic_fields:\n if field in build_config:\n build_config[field][\"show\"] = False\n\n # Show/hide fields based on selected mode\n if field_value == \"Ingest\":\n if \"ingest_data\" in build_config:\n build_config[\"ingest_data\"][\"show\"] = True\n if \"collection_name\" in build_config:\n build_config[\"collection_name\"][\"show\"] = True\n build_config[\"collection_name\"][\"display_name\"] = \"Name Your Collection\"\n if \"persist\" in build_config:\n build_config[\"persist\"][\"show\"] = True\n if \"persist_directory\" in build_config:\n build_config[\"persist_directory\"][\"show\"] = True\n if \"embedding\" in build_config:\n build_config[\"embedding\"][\"show\"] = True\n if \"allow_duplicates\" in build_config:\n build_config[\"allow_duplicates\"][\"show\"] = True\n if \"limit\" in build_config:\n build_config[\"limit\"][\"show\"] = True\n elif field_value == \"Retrieve\":\n if \"persist\" in build_config:\n build_config[\"persist\"][\"show\"] = False\n build_config[\"search_query\"][\"show\"] = True\n build_config[\"search_type\"][\"show\"] = True\n build_config[\"number_of_results\"][\"show\"] = True\n build_config[\"embedding\"][\"show\"] = True\n build_config[\"collection_name\"][\"show\"] = False\n # Show existing collections dropdown and update its options\n if \"existing_collections\" in build_config:\n build_config[\"existing_collections\"][\"show\"] = True\n build_config[\"existing_collections\"][\"options\"] = self.list_existing_collections()\n # Hide collection_name in Retrieve mode since we use existing_collections\n elif field_name == \"existing_collections\":\n # Update collection_name when an existing collection is selected\n if \"collection_name\" in build_config:\n build_config[\"collection_name\"][\"value\"] = field_value\n\n return build_config\n\n @override\n @check_cached_vector_store\n def build_vector_store(self) -> Chroma:\n \"\"\"Builds the Chroma object.\"\"\"\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n\n try:\n from langchain_chroma import Chroma\n except ImportError as e:\n msg = \"Could not import Chroma integration package. Please install it with `pip install langchain-chroma`.\"\n raise ImportError(msg) from e\n # Chroma settings\n # chroma_settings = None\n if self.existing_collections:\n self.collection_name = self.existing_collections\n\n # Use user-provided directory or default cache directory\n if self.persist_directory:\n base_dir = self.resolve_path(self.persist_directory)\n persist_directory = str(self.get_vector_store_directory(base_dir))\n logger.debug(f\"Using custom persist directory: {persist_directory}\")\n else:\n persist_directory = self.get_default_persist_dir()\n logger.debug(f\"Using default persist directory: {persist_directory}\")\n\n chroma = Chroma(\n persist_directory=persist_directory,\n client=None,\n embedding_function=self.embedding,\n collection_name=self.collection_name,\n )\n\n self._add_documents_to_vector_store(chroma)\n self.status = chroma_collection_to_data(chroma.get(limit=self.limit))\n return chroma\n\n def _add_documents_to_vector_store(self, vector_store: \"Chroma\") -> None:\n \"\"\"Adds documents to the Vector Store.\"\"\"\n ingest_data: list | Data | DataFrame = self.ingest_data\n if not ingest_data:\n self.status = \"\"\n return\n\n # Convert DataFrame to Data if needed using parent's method\n ingest_data = self._prepare_ingest_data()\n\n stored_documents_without_id = []\n if self.allow_duplicates:\n stored_data = []\n else:\n stored_data = chroma_collection_to_data(vector_store.get(limit=self.limit))\n for value in deepcopy(stored_data):\n del value.id\n stored_documents_without_id.append(value)\n\n documents = []\n for _input in ingest_data or []:\n if isinstance(_input, Data):\n if _input not in stored_documents_without_id:\n documents.append(_input.to_lc_document())\n else:\n msg = \"Vector Store Inputs must be Data objects.\"\n raise TypeError(msg)\n\n if documents and self.embedding is not None:\n self.log(f\"Adding {len(documents)} documents to the Vector Store.\")\n vector_store.add_documents(documents)\n else:\n self.log(\"No documents to add to the Vector Store.\")\n\n def perform_search(self) -> DataFrame:\n return DataFrame(self.search_documents())\n" + "value": "from copy import deepcopy\nfrom pathlib import Path\n\nfrom langchain_chroma import Chroma\nfrom typing_extensions import override\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.base.vectorstores.utils import chroma_collection_to_data\nfrom lfx.inputs.inputs import MultilineInput\nfrom lfx.io import BoolInput, DropdownInput, HandleInput, IntInput, MessageTextInput, TabInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\nfrom lfx.utils.validate_cloud import raise_error_if_astra_cloud_disable_component\n\ndisable_component_in_astra_cloud_msg = (\n \"Local vector stores are not supported in S3/cloud mode. \"\n \"Local vector stores require local file system access for persistence. \"\n \"Please use cloud-based vector stores (Pinecone, Weaviate, etc.) or local storage mode.\"\n)\n\n\nclass LocalDBComponent(LCVectorStoreComponent):\n \"\"\"Chroma Vector Store with search capabilities.\"\"\"\n\n display_name: str = \"Local DB\"\n description: str = \"Local Vector Store with search capabilities\"\n name = \"LocalDB\"\n icon = \"database\"\n legacy = True\n\n inputs = [\n TabInput(\n name=\"mode\",\n display_name=\"Mode\",\n options=[\"Ingest\", \"Retrieve\"],\n info=\"Select the operation mode\",\n value=\"Ingest\",\n real_time_refresh=True,\n show=True,\n ),\n MessageTextInput(\n name=\"collection_name\",\n display_name=\"Collection Name\",\n value=\"langflow\",\n required=True,\n ),\n MessageTextInput(\n name=\"persist_directory\",\n display_name=\"Persist Directory\",\n info=(\n \"Custom base directory to save the vector store. \"\n \"Collections will be stored under '{directory}/vector_stores/{collection_name}'. \"\n \"If not specified, it will use your system's cache folder.\"\n ),\n advanced=True,\n ),\n DropdownInput(\n name=\"existing_collections\",\n display_name=\"Existing Collections\",\n options=[], # Will be populated dynamically\n info=\"Select a previously created collection to search through its stored data.\",\n show=False,\n combobox=True,\n ),\n HandleInput(name=\"embedding\", display_name=\"Embedding\", required=True, input_types=[\"Embeddings\"]),\n BoolInput(\n name=\"allow_duplicates\",\n display_name=\"Allow Duplicates\",\n advanced=True,\n info=\"If false, will not add documents that are already in the Vector Store.\",\n ),\n DropdownInput(\n name=\"search_type\",\n display_name=\"Search Type\",\n options=[\"Similarity\", \"MMR\"],\n value=\"Similarity\",\n advanced=True,\n ),\n HandleInput(\n name=\"ingest_data\",\n display_name=\"Ingest Data\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\"],\n is_list=True,\n info=\"Data to store. It will be embedded and indexed for semantic search.\",\n show=True,\n ),\n MultilineInput(\n name=\"search_query\",\n display_name=\"Search Query\",\n tool_mode=True,\n info=\"Enter text to search for similar content in the selected collection.\",\n show=False,\n ),\n IntInput(\n name=\"number_of_results\",\n display_name=\"Number of Results\",\n info=\"Number of results to return.\",\n advanced=True,\n value=10,\n ),\n IntInput(\n name=\"limit\",\n display_name=\"Limit\",\n advanced=True,\n info=\"Limit the number of records to compare when Allow Duplicates is False.\",\n ),\n ]\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"perform_search\"),\n ]\n\n def get_vector_store_directory(self, base_dir: str | Path) -> Path:\n \"\"\"Get the full directory path for a collection.\"\"\"\n # Ensure base_dir is a Path object\n base_dir = Path(base_dir)\n # Create the full path: base_dir/vector_stores/collection_name\n full_path = base_dir / \"vector_stores\" / self.collection_name\n # Create the directory if it doesn't exist\n full_path.mkdir(parents=True, exist_ok=True)\n return full_path\n\n def get_default_persist_dir(self) -> str:\n \"\"\"Get the default persist directory from cache.\"\"\"\n from lfx.services.cache.utils import CACHE_DIR\n\n return str(self.get_vector_store_directory(CACHE_DIR))\n\n def list_existing_collections(self) -> list[str]:\n \"\"\"List existing vector store collections from the persist directory.\"\"\"\n from lfx.services.cache.utils import CACHE_DIR\n\n # Get the base directory (either custom or cache)\n base_dir = Path(self.persist_directory) if self.persist_directory else Path(CACHE_DIR)\n # Get the vector_stores subdirectory\n vector_stores_dir = base_dir / \"vector_stores\"\n if not vector_stores_dir.exists():\n return []\n\n return [d.name for d in vector_stores_dir.iterdir() if d.is_dir()]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict:\n \"\"\"Update the build configuration when the mode changes.\"\"\"\n if field_name == \"mode\":\n # Hide all dynamic fields by default\n dynamic_fields = [\n \"ingest_data\",\n \"search_query\",\n \"search_type\",\n \"number_of_results\",\n \"existing_collections\",\n \"collection_name\",\n \"embedding\",\n \"allow_duplicates\",\n \"limit\",\n ]\n for field in dynamic_fields:\n if field in build_config:\n build_config[field][\"show\"] = False\n\n # Show/hide fields based on selected mode\n if field_value == \"Ingest\":\n if \"ingest_data\" in build_config:\n build_config[\"ingest_data\"][\"show\"] = True\n if \"collection_name\" in build_config:\n build_config[\"collection_name\"][\"show\"] = True\n build_config[\"collection_name\"][\"display_name\"] = \"Name Your Collection\"\n if \"persist\" in build_config:\n build_config[\"persist\"][\"show\"] = True\n if \"persist_directory\" in build_config:\n build_config[\"persist_directory\"][\"show\"] = True\n if \"embedding\" in build_config:\n build_config[\"embedding\"][\"show\"] = True\n if \"allow_duplicates\" in build_config:\n build_config[\"allow_duplicates\"][\"show\"] = True\n if \"limit\" in build_config:\n build_config[\"limit\"][\"show\"] = True\n elif field_value == \"Retrieve\":\n if \"persist\" in build_config:\n build_config[\"persist\"][\"show\"] = False\n build_config[\"search_query\"][\"show\"] = True\n build_config[\"search_type\"][\"show\"] = True\n build_config[\"number_of_results\"][\"show\"] = True\n build_config[\"embedding\"][\"show\"] = True\n build_config[\"collection_name\"][\"show\"] = False\n # Show existing collections dropdown and update its options\n if \"existing_collections\" in build_config:\n build_config[\"existing_collections\"][\"show\"] = True\n build_config[\"existing_collections\"][\"options\"] = self.list_existing_collections()\n # Hide collection_name in Retrieve mode since we use existing_collections\n elif field_name == \"existing_collections\":\n # Update collection_name when an existing collection is selected\n if \"collection_name\" in build_config:\n build_config[\"collection_name\"][\"value\"] = field_value\n\n return build_config\n\n @override\n @check_cached_vector_store\n def build_vector_store(self) -> Chroma:\n \"\"\"Builds the Chroma object.\"\"\"\n raise_error_if_astra_cloud_disable_component(disable_component_in_astra_cloud_msg)\n\n try:\n from langchain_chroma import Chroma\n except ImportError as e:\n msg = \"Could not import Chroma integration package. Please install it with `pip install langchain-chroma`.\"\n raise ImportError(msg) from e\n # Chroma settings\n # chroma_settings = None\n if self.existing_collections:\n self.collection_name = self.existing_collections\n\n # Use user-provided directory or default cache directory\n if self.persist_directory:\n base_dir = self.resolve_path(self.persist_directory)\n persist_directory = str(self.get_vector_store_directory(base_dir))\n logger.debug(f\"Using custom persist directory: {persist_directory}\")\n else:\n persist_directory = self.get_default_persist_dir()\n logger.debug(f\"Using default persist directory: {persist_directory}\")\n\n chroma = Chroma(\n persist_directory=persist_directory,\n client=None,\n embedding_function=self.embedding,\n collection_name=self.collection_name,\n )\n\n self._add_documents_to_vector_store(chroma)\n self.status = chroma_collection_to_data(chroma.get(limit=self.limit))\n return chroma\n\n def _add_documents_to_vector_store(self, vector_store: \"Chroma\") -> None:\n \"\"\"Adds documents to the Vector Store.\"\"\"\n ingest_data: list | Data | DataFrame = self.ingest_data\n if not ingest_data:\n self.status = \"\"\n return\n\n # Convert DataFrame to Data if needed using parent's method\n ingest_data = self._prepare_ingest_data()\n\n stored_documents_without_id = []\n if self.allow_duplicates:\n stored_data = []\n else:\n stored_data = chroma_collection_to_data(vector_store.get(limit=self.limit))\n for value in deepcopy(stored_data):\n del value.id\n stored_documents_without_id.append(value)\n\n documents = []\n for _input in ingest_data or []:\n if isinstance(_input, Data):\n if _input not in stored_documents_without_id:\n documents.append(_input.to_lc_document())\n else:\n msg = \"Vector Store Inputs must be Data objects.\"\n raise TypeError(msg)\n\n if documents and self.embedding is not None:\n self.log(f\"Adding {len(documents)} documents to the Vector Store.\")\n vector_store.add_documents(documents)\n else:\n self.log(\"No documents to add to the Vector Store.\")\n\n def perform_search(self) -> DataFrame:\n return DataFrame(self.search_documents())\n" }, "collection_name": { "_input_type": "MessageTextInput", @@ -113460,7 +113633,9 @@ "info": "Data to store. It will be embedded and indexed for semantic search.", "input_types": [ "Data", - "DataFrame" + "JSON", + "DataFrame", + "Table" ], "list": true, "list_add_label": "Add More", @@ -115165,7 +115340,7 @@ { "VLMRunTranscription": { "base_classes": [ - "Data" + "JSON" ], "beta": true, "conditional_paths": [], @@ -115216,10 +115391,10 @@ "group_outputs": false, "method": "process_media", "name": "result", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -115411,8 +115586,8 @@ { "Weaviate": { "base_classes": [ - "Data", - "DataFrame" + "JSON", + "Table" ], "beta": false, "conditional_paths": [], @@ -115467,24 +115642,24 @@ "group_outputs": false, "method": "search_documents", "name": "search_results", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" }, { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "as_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -115732,7 +115907,7 @@ { "WikidataComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -115748,7 +115923,7 @@ "icon": "Wikipedia", "legacy": false, "metadata": { - "code_hash": "59df9d399440", + "code_hash": "5f6398d72116", "dependencies": { "dependencies": [ { @@ -115774,14 +115949,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -115805,7 +115980,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import httpx\nfrom httpx import HTTPError\nfrom langchain_core.tools import ToolException\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MultilineInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass WikidataComponent(Component):\n display_name = \"Wikidata\"\n description = \"Performs a search using the Wikidata API.\"\n icon = \"Wikipedia\"\n\n inputs = [\n MultilineInput(\n name=\"query\",\n display_name=\"Query\",\n info=\"The text query for similarity search on Wikidata.\",\n required=True,\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n try:\n # Define request parameters for Wikidata API\n params = {\n \"action\": \"wbsearchentities\",\n \"format\": \"json\",\n \"search\": self.query,\n \"language\": \"en\",\n }\n\n # Send request to Wikidata API\n wikidata_api_url = \"https://www.wikidata.org/w/api.php\"\n response = httpx.get(wikidata_api_url, params=params)\n response.raise_for_status()\n response_json = response.json()\n\n # Extract search results\n results = response_json.get(\"search\", [])\n\n if not results:\n return [Data(data={\"error\": \"No search results found for the given query.\"})]\n\n # Transform the API response into Data objects\n data = [\n Data(\n text=f\"{result['label']}: {result.get('description', '')}\",\n data={\n \"label\": result[\"label\"],\n \"id\": result.get(\"id\"),\n \"url\": result.get(\"url\"),\n \"description\": result.get(\"description\", \"\"),\n \"concepturi\": result.get(\"concepturi\"),\n },\n )\n for result in results\n ]\n\n self.status = data\n except HTTPError as e:\n error_message = f\"HTTP Error in Wikidata Search API: {e!s}\"\n raise ToolException(error_message) from None\n except KeyError as e:\n error_message = f\"Data parsing error in Wikidata API response: {e!s}\"\n raise ToolException(error_message) from None\n except ValueError as e:\n error_message = f\"Value error in Wikidata API: {e!s}\"\n raise ToolException(error_message) from None\n else:\n return data\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import httpx\nfrom httpx import HTTPError\nfrom langchain_core.tools import ToolException\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import MultilineInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.template.field.base import Output\n\n\nclass WikidataComponent(Component):\n display_name = \"Wikidata\"\n description = \"Performs a search using the Wikidata API.\"\n icon = \"Wikipedia\"\n\n inputs = [\n MultilineInput(\n name=\"query\",\n display_name=\"Query\",\n info=\"The text query for similarity search on Wikidata.\",\n required=True,\n tool_mode=True,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n try:\n # Define request parameters for Wikidata API\n params = {\n \"action\": \"wbsearchentities\",\n \"format\": \"json\",\n \"search\": self.query,\n \"language\": \"en\",\n }\n\n # Send request to Wikidata API\n wikidata_api_url = \"https://www.wikidata.org/w/api.php\"\n response = httpx.get(wikidata_api_url, params=params)\n response.raise_for_status()\n response_json = response.json()\n\n # Extract search results\n results = response_json.get(\"search\", [])\n\n if not results:\n return [Data(data={\"error\": \"No search results found for the given query.\"})]\n\n # Transform the API response into Data objects\n data = [\n Data(\n text=f\"{result['label']}: {result.get('description', '')}\",\n data={\n \"label\": result[\"label\"],\n \"id\": result.get(\"id\"),\n \"url\": result.get(\"url\"),\n \"description\": result.get(\"description\", \"\"),\n \"concepturi\": result.get(\"concepturi\"),\n },\n )\n for result in results\n ]\n\n self.status = data\n except HTTPError as e:\n error_message = f\"HTTP Error in Wikidata Search API: {e!s}\"\n raise ToolException(error_message) from None\n except KeyError as e:\n error_message = f\"Data parsing error in Wikidata API response: {e!s}\"\n raise ToolException(error_message) from None\n except ValueError as e:\n error_message = f\"Value error in Wikidata API: {e!s}\"\n raise ToolException(error_message) from None\n else:\n return data\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "query": { "_input_type": "MultilineInput", @@ -115841,7 +116016,7 @@ }, "WikipediaComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -115861,7 +116036,7 @@ "icon": "Wikipedia", "legacy": false, "metadata": { - "code_hash": "cc13e26c79c4", + "code_hash": "e1b58c8ac595", "dependencies": { "dependencies": [ { @@ -115883,14 +116058,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -115914,7 +116089,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from langchain_community.utilities.wikipedia import WikipediaAPIWrapper\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, IntInput, MessageTextInput, MultilineInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass WikipediaComponent(Component):\n display_name = \"Wikipedia\"\n description = \"Call Wikipedia API.\"\n icon = \"Wikipedia\"\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n MessageTextInput(name=\"lang\", display_name=\"Language\", value=\"en\"),\n IntInput(name=\"k\", display_name=\"Number of results\", value=4, required=True),\n BoolInput(name=\"load_all_available_meta\", display_name=\"Load all available meta\", value=False, advanced=True),\n IntInput(\n name=\"doc_content_chars_max\", display_name=\"Document content characters max\", value=4000, advanced=True\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def _build_wrapper(self) -> WikipediaAPIWrapper:\n return WikipediaAPIWrapper(\n top_k_results=self.k,\n lang=self.lang,\n load_all_available_meta=self.load_all_available_meta,\n doc_content_chars_max=self.doc_content_chars_max,\n )\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n docs = wrapper.load(self.input_value)\n data = [Data.from_document(doc) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "from langchain_community.utilities.wikipedia import WikipediaAPIWrapper\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import BoolInput, IntInput, MessageTextInput, MultilineInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass WikipediaComponent(Component):\n display_name = \"Wikipedia\"\n description = \"Call Wikipedia API.\"\n icon = \"Wikipedia\"\n\n inputs = [\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n MessageTextInput(name=\"lang\", display_name=\"Language\", value=\"en\"),\n IntInput(name=\"k\", display_name=\"Number of results\", value=4, required=True),\n BoolInput(name=\"load_all_available_meta\", display_name=\"Load all available meta\", value=False, advanced=True),\n IntInput(\n name=\"doc_content_chars_max\", display_name=\"Document content characters max\", value=4000, advanced=True\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def _build_wrapper(self) -> WikipediaAPIWrapper:\n return WikipediaAPIWrapper(\n top_k_results=self.k,\n lang=self.lang,\n load_all_available_meta=self.load_all_available_meta,\n doc_content_chars_max=self.doc_content_chars_max,\n )\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n docs = wrapper.load(self.input_value)\n data = [Data.from_document(doc) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "doc_content_chars_max": { "_input_type": "IntInput", @@ -116040,7 +116215,7 @@ { "WolframAlphaAPI": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -116057,7 +116232,7 @@ "icon": "WolframAlphaAPI", "legacy": false, "metadata": { - "code_hash": "86caf22224ad", + "code_hash": "44e79cb8a924", "dependencies": { "dependencies": [ { @@ -116079,14 +116254,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -116129,7 +116304,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from langchain_community.utilities.wolfram_alpha import WolframAlphaAPIWrapper\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import MultilineInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass WolframAlphaAPIComponent(LCToolComponent):\n display_name = \"WolframAlpha API\"\n description = \"\"\"Enables queries to WolframAlpha for computational data, facts, and calculations across various \\\ntopics, delivering structured responses.\"\"\"\n name = \"WolframAlphaAPI\"\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n inputs = [\n MultilineInput(\n name=\"input_value\", display_name=\"Input Query\", info=\"Example query: 'What is the population of France?'\"\n ),\n SecretStrInput(name=\"app_id\", display_name=\"WolframAlpha App ID\", required=True),\n ]\n\n icon = \"WolframAlphaAPI\"\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def build_tool(self) -> Tool:\n wrapper = self._build_wrapper()\n return Tool(name=\"wolfram_alpha_api\", description=\"Answers mathematical questions.\", func=wrapper.run)\n\n def _build_wrapper(self) -> WolframAlphaAPIWrapper:\n return WolframAlphaAPIWrapper(wolfram_alpha_appid=self.app_id)\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n result_str = wrapper.run(self.input_value)\n data = [Data(text=result_str)]\n self.status = data\n return data\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the WolframAlpha results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the query results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "from langchain_community.utilities.wolfram_alpha import WolframAlphaAPIWrapper\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import MultilineInput, SecretStrInput\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass WolframAlphaAPIComponent(LCToolComponent):\n display_name = \"WolframAlpha API\"\n description = \"\"\"Enables queries to WolframAlpha for computational data, facts, and calculations across various \\\ntopics, delivering structured responses.\"\"\"\n name = \"WolframAlphaAPI\"\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n inputs = [\n MultilineInput(\n name=\"input_value\", display_name=\"Input Query\", info=\"Example query: 'What is the population of France?'\"\n ),\n SecretStrInput(name=\"app_id\", display_name=\"WolframAlpha App ID\", required=True),\n ]\n\n icon = \"WolframAlphaAPI\"\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def build_tool(self) -> Tool:\n wrapper = self._build_wrapper()\n return Tool(name=\"wolfram_alpha_api\", description=\"Answers mathematical questions.\", func=wrapper.run)\n\n def _build_wrapper(self) -> WolframAlphaAPIWrapper:\n return WolframAlphaAPIWrapper(wolfram_alpha_appid=self.app_id)\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n result_str = wrapper.run(self.input_value)\n data = [Data(text=result_str)]\n self.status = data\n return data\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the WolframAlpha results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the query results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n" }, "input_value": { "_input_type": "MultilineInput", @@ -116559,7 +116734,7 @@ { "YfinanceComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -116577,7 +116752,7 @@ "icon": "trending-up", "legacy": false, "metadata": { - "code_hash": "d6bf628ab821", + "code_hash": "14ca8af63c82", "dependencies": { "dependencies": [ { @@ -116607,14 +116782,14 @@ { "allows_loop": false, "cache": true, - "display_name": "DataFrame", + "display_name": "Table", "group_outputs": false, "method": "fetch_content_dataframe", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -116638,7 +116813,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import ast\nimport pprint\nfrom enum import Enum\n\nimport yfinance as yf\nfrom langchain_core.tools import ToolException\nfrom pydantic import BaseModel, Field\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DropdownInput, IntInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass YahooFinanceMethod(Enum):\n GET_INFO = \"get_info\"\n GET_NEWS = \"get_news\"\n GET_ACTIONS = \"get_actions\"\n GET_ANALYSIS = \"get_analysis\"\n GET_BALANCE_SHEET = \"get_balance_sheet\"\n GET_CALENDAR = \"get_calendar\"\n GET_CASHFLOW = \"get_cashflow\"\n GET_INSTITUTIONAL_HOLDERS = \"get_institutional_holders\"\n GET_RECOMMENDATIONS = \"get_recommendations\"\n GET_SUSTAINABILITY = \"get_sustainability\"\n GET_MAJOR_HOLDERS = \"get_major_holders\"\n GET_MUTUALFUND_HOLDERS = \"get_mutualfund_holders\"\n GET_INSIDER_PURCHASES = \"get_insider_purchases\"\n GET_INSIDER_TRANSACTIONS = \"get_insider_transactions\"\n GET_INSIDER_ROSTER_HOLDERS = \"get_insider_roster_holders\"\n GET_DIVIDENDS = \"get_dividends\"\n GET_CAPITAL_GAINS = \"get_capital_gains\"\n GET_SPLITS = \"get_splits\"\n GET_SHARES = \"get_shares\"\n GET_FAST_INFO = \"get_fast_info\"\n GET_SEC_FILINGS = \"get_sec_filings\"\n GET_RECOMMENDATIONS_SUMMARY = \"get_recommendations_summary\"\n GET_UPGRADES_DOWNGRADES = \"get_upgrades_downgrades\"\n GET_EARNINGS = \"get_earnings\"\n GET_INCOME_STMT = \"get_income_stmt\"\n\n\nclass YahooFinanceSchema(BaseModel):\n symbol: str = Field(..., description=\"The stock symbol to retrieve data for.\")\n method: YahooFinanceMethod = Field(YahooFinanceMethod.GET_INFO, description=\"The type of data to retrieve.\")\n num_news: int | None = Field(5, description=\"The number of news articles to retrieve.\")\n\n\nclass YfinanceComponent(Component):\n display_name = \"Yahoo! Finance\"\n description = \"\"\"Uses [yfinance](https://pypi.org/project/yfinance/) (unofficial package) \\\nto access financial data and market information from Yahoo! Finance.\"\"\"\n icon = \"trending-up\"\n\n inputs = [\n MessageTextInput(\n name=\"symbol\",\n display_name=\"Stock Symbol\",\n info=\"The stock symbol to retrieve data for (e.g., AAPL, GOOG).\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Data Method\",\n info=\"The type of data to retrieve.\",\n options=list(YahooFinanceMethod),\n value=\"get_news\",\n ),\n IntInput(\n name=\"num_news\",\n display_name=\"Number of News\",\n info=\"The number of news articles to retrieve (only applicable for get_news).\",\n value=5,\n ),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def _fetch_yfinance_data(self, ticker: yf.Ticker, method: YahooFinanceMethod, num_news: int | None) -> str:\n try:\n if method == YahooFinanceMethod.GET_INFO:\n result = ticker.info\n elif method == YahooFinanceMethod.GET_NEWS:\n result = ticker.news[:num_news]\n else:\n result = getattr(ticker, method.value)()\n return pprint.pformat(result)\n except Exception as e:\n error_message = f\"Error retrieving data: {e}\"\n logger.debug(error_message)\n self.status = error_message\n raise ToolException(error_message) from e\n\n def fetch_content(self) -> list[Data]:\n try:\n return self._yahoo_finance_tool(\n self.symbol,\n YahooFinanceMethod(self.method),\n self.num_news,\n )\n except ToolException:\n raise\n except Exception as e:\n error_message = f\"Unexpected error: {e}\"\n logger.debug(error_message)\n self.status = error_message\n raise ToolException(error_message) from e\n\n def _yahoo_finance_tool(\n self,\n symbol: str,\n method: YahooFinanceMethod,\n num_news: int | None = 5,\n ) -> list[Data]:\n ticker = yf.Ticker(symbol)\n result = self._fetch_yfinance_data(ticker, method, num_news)\n\n if method == YahooFinanceMethod.GET_NEWS:\n data_list = [\n Data(text=f\"{article['title']}: {article['link']}\", data=article)\n for article in ast.literal_eval(result)\n ]\n else:\n data_list = [Data(text=result, data={\"result\": result})]\n\n return data_list\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" + "value": "import ast\nimport pprint\nfrom enum import Enum\n\nimport yfinance as yf\nfrom langchain_core.tools import ToolException\nfrom pydantic import BaseModel, Field\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs.inputs import DropdownInput, IntInput, MessageTextInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass YahooFinanceMethod(Enum):\n GET_INFO = \"get_info\"\n GET_NEWS = \"get_news\"\n GET_ACTIONS = \"get_actions\"\n GET_ANALYSIS = \"get_analysis\"\n GET_BALANCE_SHEET = \"get_balance_sheet\"\n GET_CALENDAR = \"get_calendar\"\n GET_CASHFLOW = \"get_cashflow\"\n GET_INSTITUTIONAL_HOLDERS = \"get_institutional_holders\"\n GET_RECOMMENDATIONS = \"get_recommendations\"\n GET_SUSTAINABILITY = \"get_sustainability\"\n GET_MAJOR_HOLDERS = \"get_major_holders\"\n GET_MUTUALFUND_HOLDERS = \"get_mutualfund_holders\"\n GET_INSIDER_PURCHASES = \"get_insider_purchases\"\n GET_INSIDER_TRANSACTIONS = \"get_insider_transactions\"\n GET_INSIDER_ROSTER_HOLDERS = \"get_insider_roster_holders\"\n GET_DIVIDENDS = \"get_dividends\"\n GET_CAPITAL_GAINS = \"get_capital_gains\"\n GET_SPLITS = \"get_splits\"\n GET_SHARES = \"get_shares\"\n GET_FAST_INFO = \"get_fast_info\"\n GET_SEC_FILINGS = \"get_sec_filings\"\n GET_RECOMMENDATIONS_SUMMARY = \"get_recommendations_summary\"\n GET_UPGRADES_DOWNGRADES = \"get_upgrades_downgrades\"\n GET_EARNINGS = \"get_earnings\"\n GET_INCOME_STMT = \"get_income_stmt\"\n\n\nclass YahooFinanceSchema(BaseModel):\n symbol: str = Field(..., description=\"The stock symbol to retrieve data for.\")\n method: YahooFinanceMethod = Field(YahooFinanceMethod.GET_INFO, description=\"The type of data to retrieve.\")\n num_news: int | None = Field(5, description=\"The number of news articles to retrieve.\")\n\n\nclass YfinanceComponent(Component):\n display_name = \"Yahoo! Finance\"\n description = \"\"\"Uses [yfinance](https://pypi.org/project/yfinance/) (unofficial package) \\\nto access financial data and market information from Yahoo! Finance.\"\"\"\n icon = \"trending-up\"\n\n inputs = [\n MessageTextInput(\n name=\"symbol\",\n display_name=\"Stock Symbol\",\n info=\"The stock symbol to retrieve data for (e.g., AAPL, GOOG).\",\n tool_mode=True,\n ),\n DropdownInput(\n name=\"method\",\n display_name=\"Data Method\",\n info=\"The type of data to retrieve.\",\n options=list(YahooFinanceMethod),\n value=\"get_news\",\n ),\n IntInput(\n name=\"num_news\",\n display_name=\"Number of News\",\n info=\"The number of news articles to retrieve (only applicable for get_news).\",\n value=5,\n ),\n ]\n\n outputs = [\n Output(display_name=\"Table\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def _fetch_yfinance_data(self, ticker: yf.Ticker, method: YahooFinanceMethod, num_news: int | None) -> str:\n try:\n if method == YahooFinanceMethod.GET_INFO:\n result = ticker.info\n elif method == YahooFinanceMethod.GET_NEWS:\n result = ticker.news[:num_news]\n else:\n result = getattr(ticker, method.value)()\n return pprint.pformat(result)\n except Exception as e:\n error_message = f\"Error retrieving data: {e}\"\n logger.debug(error_message)\n self.status = error_message\n raise ToolException(error_message) from e\n\n def fetch_content(self) -> list[Data]:\n try:\n return self._yahoo_finance_tool(\n self.symbol,\n YahooFinanceMethod(self.method),\n self.num_news,\n )\n except ToolException:\n raise\n except Exception as e:\n error_message = f\"Unexpected error: {e}\"\n logger.debug(error_message)\n self.status = error_message\n raise ToolException(error_message) from e\n\n def _yahoo_finance_tool(\n self,\n symbol: str,\n method: YahooFinanceMethod,\n num_news: int | None = 5,\n ) -> list[Data]:\n ticker = yf.Ticker(symbol)\n result = self._fetch_yfinance_data(ticker, method, num_news)\n\n if method == YahooFinanceMethod.GET_NEWS:\n data_list = [\n Data(text=f\"{article['title']}: {article['link']}\", data=article)\n for article in ast.literal_eval(result)\n ]\n else:\n data_list = [Data(text=result, data={\"result\": result})]\n\n return data_list\n\n def fetch_content_dataframe(self) -> DataFrame:\n data = self.fetch_content()\n return DataFrame(data)\n" }, "method": { "_input_type": "DropdownInput", @@ -116745,7 +116920,7 @@ { "YouTubeChannelComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -116795,10 +116970,10 @@ "group_outputs": false, "method": "get_channel_info", "name": "channel_df", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -116933,7 +117108,7 @@ }, "YouTubeCommentsComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -116984,10 +117159,10 @@ "group_outputs": false, "method": "get_video_comments", "name": "comments", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -117149,7 +117324,7 @@ }, "YouTubePlaylistComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -117191,10 +117366,10 @@ "group_outputs": false, "method": "extract_video_urls", "name": "video_urls", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -117250,7 +117425,7 @@ }, "YouTubeSearchComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -117300,10 +117475,10 @@ "group_outputs": false, "method": "search_videos", "name": "results", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -117448,9 +117623,9 @@ }, "YouTubeTranscripts": { "base_classes": [ - "Data", - "DataFrame", - "Message" + "JSON", + "Message", + "Table" ], "beta": false, "conditional_paths": [], @@ -117498,10 +117673,10 @@ "group_outputs": false, "method": "get_dataframe_output", "name": "dataframe", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" }, @@ -117526,10 +117701,10 @@ "group_outputs": false, "method": "get_data_output", "name": "data_output", - "selected": "Data", + "selected": "JSON", "tool_mode": true, "types": [ - "Data" + "JSON" ], "value": "__UNDEFINED__" } @@ -117647,7 +117822,7 @@ }, "YouTubeTrendingComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -117699,10 +117874,10 @@ "group_outputs": false, "method": "get_trending_videos", "name": "trending_videos", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -117915,7 +118090,7 @@ }, "YouTubeVideoDetailsComponent": { "base_classes": [ - "DataFrame" + "Table" ], "beta": false, "conditional_paths": [], @@ -117966,10 +118141,10 @@ "group_outputs": false, "method": "get_video_details", "name": "video_data", - "selected": "DataFrame", + "selected": "Table", "tool_mode": true, "types": [ - "DataFrame" + "Table" ], "value": "__UNDEFINED__" } @@ -118312,6 +118487,6 @@ "num_components": 359, "num_modules": 97 }, - "sha256": "b4ad15cdd78b0f55e26ed49b475c2df2547310bf8ea4550ed9f02d0795d642c7", + "sha256": "da998986ea45f043e65e13a6b57bdc130bcdcb8d6548d92857d6817dc929e802", "version": "0.3.0" } \ No newline at end of file diff --git a/src/lfx/src/lfx/_assets/stable_hash_history.json b/src/lfx/src/lfx/_assets/stable_hash_history.json index da23a6808e8a..d3a89fc9445d 100644 --- a/src/lfx/src/lfx/_assets/stable_hash_history.json +++ b/src/lfx/src/lfx/_assets/stable_hash_history.json @@ -46,7 +46,7 @@ }, "AgentQL": { "versions": { - "0.3.0": "37de3210aed9" + "0.3.0": "3737ac221d7d" } }, "AIMLModel": { @@ -81,7 +81,7 @@ }, "s3bucketuploader": { "versions": { - "0.3.0": "6e4ba2dafc3c" + "0.3.0": "119c89b6bd40" } }, "AnthropicModel": { @@ -96,7 +96,7 @@ }, "ArXivComponent": { "versions": { - "0.3.0": "219239ee2b48" + "0.3.0": "2d892beaf98b" } }, "AssemblyAIGetSubtitles": { @@ -141,7 +141,7 @@ }, "BingSearchAPI": { "versions": { - "0.3.0": "84334607b325" + "0.3.0": "21008f6682b9" } }, "Cassandra": { @@ -526,7 +526,7 @@ }, "Confluence": { "versions": { - "0.3.0": "8a7ef34b66e4" + "0.3.0": "d669f422824e" } }, "Couchbase": { @@ -576,17 +576,17 @@ }, "APIRequest": { "versions": { - "0.3.0": "f102aadfb328" + "0.3.0": "2af407885294" } }, "CSVtoData": { "versions": { - "0.3.0": "85c7d6df7473" + "0.3.0": "049e2eeb6901" } }, "JSONtoData": { "versions": { - "0.3.0": "0d9d78d496a2" + "0.3.0": "e8d050bde0d0" } }, "MockDataGenerator": { @@ -611,7 +611,7 @@ }, "URLComponent": { "versions": { - "0.3.0": "f773f55e3820" + "0.3.0": "7c2b0b18854e" } }, "UnifiedWebSearch": { @@ -706,7 +706,7 @@ }, "ChunkDoclingDocument": { "versions": { - "0.3.0": "49d762d97039" + "0.3.0": "7775393185fe" } }, "DoclingInline": { @@ -721,12 +721,12 @@ }, "ExportDoclingDocument": { "versions": { - "0.3.0": "32577a7e396b" + "0.3.0": "24cc033dcec6" } }, "DuckDuckGoSearchComponent": { "versions": { - "0.3.0": "2e522a5a4389" + "0.3.0": "2b8d1e2e8317" } }, "Elasticsearch": { @@ -736,12 +736,12 @@ }, "OpenSearchVectorStoreComponent": { "versions": { - "0.3.0": "4968b4d34fad" + "0.3.0": "f4dfc3668475" } }, "OpenSearchVectorStoreComponentMultimodalMultiEmbedding": { "versions": { - "0.3.0": "6a3df45b55c5" + "0.3.0": "24abb9020048" } }, "EmbeddingSimilarityComponent": { @@ -781,27 +781,27 @@ }, "SaveToFile": { "versions": { - "0.3.0": "6d0e4842271e" + "0.3.0": "f8b6df3c93c0" } }, "FirecrawlCrawlApi": { "versions": { - "0.3.0": "22fd75efce27" + "0.3.0": "21b4965f8b53" } }, "FirecrawlExtractApi": { "versions": { - "0.3.0": "8083782c2c28" + "0.3.0": "1363b7da7bf7" } }, "FirecrawlMapApi": { "versions": { - "0.3.0": "e326e840049b" + "0.3.0": "31e75312e67e" } }, "FirecrawlScrapeApi": { "versions": { - "0.3.0": "857b7da04207" + "0.3.0": "a56c999d7a42" } }, "ConditionalRouter": { @@ -821,17 +821,17 @@ }, "Listen": { "versions": { - "0.3.0": "93fc11377c96" + "0.3.0": "7f4e3f36b7e2" } }, "LoopComponent": { "versions": { - "0.3.0": "e516ea99611c" + "0.3.0": "f789817c7cd3" } }, "Notify": { "versions": { - "0.3.0": "03d68ba28530" + "0.3.0": "a22284d4b01e" } }, "Pass": { @@ -851,7 +851,7 @@ }, "GitLoaderComponent": { "versions": { - "0.3.0": "ac5de0564a4f" + "0.3.0": "7797832cc23c" } }, "GitExtractorComponent": { @@ -861,12 +861,12 @@ }, "GleanSearchAPIComponent": { "versions": { - "0.3.0": "493ca281d420" + "0.3.0": "469618609b03" } }, "GmailLoaderComponent": { "versions": { - "0.3.0": "b973c5a1987b" + "0.3.0": "6ef945902cfd" } }, "BigQueryExecutor": { @@ -881,7 +881,7 @@ }, "GoogleDriveSearchComponent": { "versions": { - "0.3.0": "8f8dbdf04aaf" + "0.3.0": "35528aa332d1" } }, "GoogleGenerativeAIModel": { @@ -956,7 +956,7 @@ }, "ChatOutput": { "versions": { - "0.3.0": "8c87e536cca4" + "0.3.0": "c312c84b1777" } }, "TextInput": { @@ -971,7 +971,7 @@ }, "Webhook": { "versions": { - "0.3.0": "eb561ef21f3d" + "0.3.0": "e99e2452d56e" } }, "JigsawStackAIScraper": { @@ -1031,7 +1031,7 @@ }, "CharacterTextSplitter": { "versions": { - "0.3.0": "995b35c5296c" + "0.3.0": "ea7c81772b05" } }, "ConversationChain": { @@ -1051,7 +1051,7 @@ }, "HtmlLinkExtractor": { "versions": { - "0.3.0": "13cc6457f84c" + "0.3.0": "7acefd9ece13" } }, "JsonAgent": { @@ -1066,12 +1066,12 @@ }, "LanguageRecursiveTextSplitter": { "versions": { - "0.3.0": "207a88b20a7e" + "0.3.0": "ee28cc4c2001" } }, "SemanticTextSplitter": { "versions": { - "0.3.0": "e9178757dea0" + "0.3.0": "8a7e7a5a39ed" } }, "LLMCheckerChain": { @@ -1086,7 +1086,7 @@ }, "NaturalLanguageTextSplitter": { "versions": { - "0.3.0": "aed1e0bb411e" + "0.3.0": "6483da1155b8" } }, "OpenAIToolsAgent": { @@ -1101,7 +1101,7 @@ }, "RecursiveCharacterTextSplitter": { "versions": { - "0.3.0": "9ed58a212804" + "0.3.0": "1cad6dd9957a" } }, "RetrievalQA": { @@ -1116,7 +1116,7 @@ }, "SelfQueryRetriever": { "versions": { - "0.3.0": "a18169f36371" + "0.3.0": "3b647e2416be" } }, "SpiderTool": { @@ -1166,12 +1166,12 @@ }, "BatchRunComponent": { "versions": { - "0.3.0": "8b1ec3b03475" + "0.3.0": "f20d52a329ad" } }, "Smart Transform": { "versions": { - "0.3.0": "9912fe8c7f1b" + "0.3.0": "4fb127dc371c" } }, "SmartRouter": { @@ -1206,7 +1206,7 @@ }, "mem0_chat_memory": { "versions": { - "0.3.0": "b6addbcccf9a" + "0.3.0": "309abc9375f4" } }, "Milvus": { @@ -1246,7 +1246,7 @@ }, "Memory": { "versions": { - "0.3.0": "efd064ef48ff" + "0.3.0": "460243b16a3a" } }, "Prompt Template": { @@ -1306,7 +1306,7 @@ }, "OllamaModel": { "versions": { - "0.3.0": "cd3dc38272a7" + "0.3.0": "2aa7e6ecf48c" } }, "OllamaEmbeddings": { @@ -1346,7 +1346,7 @@ }, "AlterMetadata": { "versions": { - "0.3.0": "0b2fe62eaec4" + "0.3.0": "a209b85f75c1" } }, "CombineText": { @@ -1356,37 +1356,37 @@ }, "TypeConverterComponent": { "versions": { - "0.3.0": "be7797f8df1c" + "0.3.0": "6ce26e994c2d" } }, "CreateData": { "versions": { - "0.3.0": "3e313525090d" + "0.3.0": "10b0eae5a063" } }, "CreateList": { "versions": { - "0.3.0": "9ec770d03310" + "0.3.0": "565738357961" } }, "DataOperations": { "versions": { - "0.3.0": "1e5bfda1706b" + "0.3.0": "957fe86b2c4f" } }, "DataToDataFrame": { "versions": { - "0.3.0": "57b9f79028e9" + "0.3.0": "edcdf6feefd2" } }, "DataFrameOperations": { "versions": { - "0.3.0": "e2b4323d4ed5" + "0.3.0": "3a3aca2d9d1f" } }, "DynamicCreateData": { "versions": { - "0.3.0": "0457c4acdf45" + "0.3.0": "8af479187c18" } }, "ExtractaKey": { @@ -1396,12 +1396,12 @@ }, "FilterData": { "versions": { - "0.3.0": "04c50937216d" + "0.3.0": "5f364efb79fc" } }, "FilterDataValues": { "versions": { - "0.3.0": "847522549c67" + "0.3.0": "274c9e3a6e7e" } }, "JSONCleaner": { @@ -1411,12 +1411,12 @@ }, "MergeDataComponent": { "versions": { - "0.3.0": "a2ecb813aac5" + "0.3.0": "3d8c0fa8f47c" } }, "MessagetoData": { "versions": { - "0.3.0": "d0af1222aeaf" + "0.3.0": "cc86df1d6415" } }, "OutputParser": { @@ -1426,37 +1426,37 @@ }, "ParseData": { "versions": { - "0.3.0": "3fac44a9bb37" + "0.3.0": "73e818f86943" } }, "ParseDataFrame": { "versions": { - "0.3.0": "9d4b05cf1564" + "0.3.0": "af6b7e66d77e" } }, "ParseJSONData": { "versions": { - "0.3.0": "5268ca4c42d6" + "0.3.0": "2ad980f8bac3" } }, "ParserComponent": { "versions": { - "0.3.0": "3cda25c3f7b5" + "0.3.0": "cda7b997a730" } }, "RegexExtractorComponent": { "versions": { - "0.3.0": "f67d7bd7f65e" + "0.3.0": "6e5d844f29b3" } }, "SelectData": { "versions": { - "0.3.0": "0512bd98ce4d" + "0.3.0": "943bab86d962" } }, "SplitText": { "versions": { - "0.3.0": "29ae597d2d86" + "0.3.0": "859adebdf672" } }, "StoreMessage": { @@ -1466,17 +1466,17 @@ }, "TextOperations": { "versions": { - "0.3.0": "2c2991ef0a37" + "0.3.0": "008b8a7b612e" } }, "UpdateData": { "versions": { - "0.3.0": "d0790af3ac9b" + "0.3.0": "7d171034c729" } }, "PythonFunction": { "versions": { - "0.3.0": "7da7d856a545" + "0.3.0": "55dc87cf0979" } }, "QdrantVectorStoreComponent": { @@ -1501,27 +1501,27 @@ }, "ScrapeGraphMarkdownifyApi": { "versions": { - "0.3.0": "0f5f12091af6" + "0.3.0": "c17524dbca7a" } }, "ScrapeGraphSearchApi": { "versions": { - "0.3.0": "002d2af653ef" + "0.3.0": "4caa0e09ea85" } }, "ScrapeGraphSmartScraperApi": { "versions": { - "0.3.0": "cb419bec02ed" + "0.3.0": "229446ce1e37" } }, "SearchComponent": { "versions": { - "0.3.0": "625d1f5b3290" + "0.3.0": "766aee1dff00" } }, "Serp": { "versions": { - "0.3.0": "dcc2ecb44ff6" + "0.3.0": "85a6736d5bb3" } }, "SupabaseVectorStore": { @@ -1531,12 +1531,12 @@ }, "TavilyExtractComponent": { "versions": { - "0.3.0": "fec95e2181d8" + "0.3.0": "86266b25a045" } }, "TavilySearchComponent": { "versions": { - "0.3.0": "e602eaec8316" + "0.3.0": "5638a305a99c" } }, "CalculatorTool": { @@ -1556,7 +1556,7 @@ }, "PythonCodeStructuredTool": { "versions": { - "0.3.0": "99f294af525b" + "0.3.0": "3f913a303e47" } }, "PythonREPLTool": { @@ -1601,17 +1601,17 @@ }, "ConvertAstraToTwelveLabs": { "versions": { - "0.3.0": "90ad7b9b59eb" + "0.3.0": "2a65cbf14ce5" } }, "TwelveLabsPegasusIndexVideo": { "versions": { - "0.3.0": "a2c0865be096" + "0.3.0": "faa14b3d6a18" } }, "SplitVideo": { "versions": { - "0.3.0": "4d3a4a724aa5" + "0.3.0": "56ccb4106c30" } }, "TwelveLabsTextEmbeddings": { @@ -1646,7 +1646,7 @@ }, "CalculatorComponent": { "versions": { - "0.3.0": "acbe2603b034" + "0.3.0": "37caa1aba62c" } }, "CurrentDate": { @@ -1676,7 +1676,7 @@ }, "LocalDB": { "versions": { - "0.3.0": "767988a11f4a" + "0.3.0": "457b336fd756" } }, "VertexAiModel": { @@ -1711,17 +1711,17 @@ }, "WikidataComponent": { "versions": { - "0.3.0": "59df9d399440" + "0.3.0": "5f6398d72116" } }, "WikipediaComponent": { "versions": { - "0.3.0": "cc13e26c79c4" + "0.3.0": "e1b58c8ac595" } }, "WolframAlphaAPI": { "versions": { - "0.3.0": "86caf22224ad" + "0.3.0": "44e79cb8a924" } }, "xAIModel": { @@ -1731,7 +1731,7 @@ }, "YfinanceComponent": { "versions": { - "0.3.0": "d6bf628ab821" + "0.3.0": "14ca8af63c82" } }, "YouTubeChannelComponent": { @@ -1786,17 +1786,17 @@ }, "SemanticAggregator": { "versions": { - "0.3.0": "4e631c501d33" + "0.3.0": "080199fa8b09" } }, "SemanticMap": { "versions": { - "0.3.0": "9fe34c926467" + "0.3.0": "ab1e08451407" } }, "SyntheticDataGenerator": { "versions": { - "0.3.0": "efd180878996" + "0.3.0": "677579fcf15f" } }, "KnowledgeBase": { diff --git a/src/lfx/src/lfx/base/composio/composio_base.py b/src/lfx/src/lfx/base/composio/composio_base.py index cf2e74df491d..a8ad2a5f9fb8 100644 --- a/src/lfx/src/lfx/base/composio/composio_base.py +++ b/src/lfx/src/lfx/base/composio/composio_base.py @@ -358,7 +358,7 @@ def get_all_auth_field_names(cls) -> set[str]: return cls._all_auth_field_names outputs = [ - Output(name="dataFrame", display_name="DataFrame", method="as_dataframe"), + Output(name="dataFrame", display_name="Table", method="as_dataframe"), ] inputs = list(_base_inputs) diff --git a/src/lfx/src/lfx/base/compressors/model.py b/src/lfx/src/lfx/base/compressors/model.py index 47c22f88b3d3..4fbbb126f499 100644 --- a/src/lfx/src/lfx/base/compressors/model.py +++ b/src/lfx/src/lfx/base/compressors/model.py @@ -26,12 +26,12 @@ class LCCompressorComponent(Component): outputs = [ Output( - display_name="Data", + display_name="JSON", name="compressed_documents", method="Compressed Documents", ), Output( - display_name="DataFrame", + display_name="Table", name="compressed_documents_as_dataframe", method="Compressed Documents as DataFrame", ), diff --git a/src/lfx/src/lfx/base/data/base_file.py b/src/lfx/src/lfx/base/data/base_file.py index f0e2170f8f87..852c2221569e 100644 --- a/src/lfx/src/lfx/base/data/base_file.py +++ b/src/lfx/src/lfx/base/data/base_file.py @@ -146,7 +146,7 @@ def __init__(self, *args, **kwargs): " or a Message object with a path to the file. Supercedes 'Path' but supports same file types." ), required=False, - input_types=["Data", "Message"], + input_types=["Data", "JSON", "Message"], is_list=True, advanced=True, ), diff --git a/src/lfx/src/lfx/base/document_transformers/model.py b/src/lfx/src/lfx/base/document_transformers/model.py index 9a83ef86e776..9eca326c8dca 100644 --- a/src/lfx/src/lfx/base/document_transformers/model.py +++ b/src/lfx/src/lfx/base/document_transformers/model.py @@ -12,7 +12,7 @@ class LCDocumentTransformerComponent(Component): trace_type = "document_transformer" outputs = [ - Output(display_name="Data", name="data", method="transform_data"), + Output(display_name="JSON", name="data", method="transform_data"), ] def transform_data(self) -> list[Data]: diff --git a/src/lfx/src/lfx/base/langchain_utilities/model.py b/src/lfx/src/lfx/base/langchain_utilities/model.py index 323ce460abf3..0f538396c993 100644 --- a/src/lfx/src/lfx/base/langchain_utilities/model.py +++ b/src/lfx/src/lfx/base/langchain_utilities/model.py @@ -11,7 +11,7 @@ class LCToolComponent(Component): trace_type = "tool" outputs = [ - Output(name="api_run_model", display_name="Data", method="run_model"), + Output(name="api_run_model", display_name="JSON", method="run_model"), Output(name="api_build_tool", display_name="Tool", method="build_tool"), ] diff --git a/src/lfx/src/lfx/base/vectorstores/model.py b/src/lfx/src/lfx/base/vectorstores/model.py index 67d12d5dde08..711c81088538 100644 --- a/src/lfx/src/lfx/base/vectorstores/model.py +++ b/src/lfx/src/lfx/base/vectorstores/model.py @@ -86,7 +86,7 @@ def __init_subclass__(cls, **kwargs): name="search_results", method="search_documents", ), - Output(display_name="DataFrame", name="dataframe", method="as_dataframe"), + Output(display_name="Table", name="dataframe", method="as_dataframe"), ] def _validate_outputs(self) -> None: diff --git a/src/lfx/src/lfx/components/agentics/semantic_aggregator.py b/src/lfx/src/lfx/components/agentics/semantic_aggregator.py index 90fc80a91a01..40588d948700 100644 --- a/src/lfx/src/lfx/components/agentics/semantic_aggregator.py +++ b/src/lfx/src/lfx/components/agentics/semantic_aggregator.py @@ -49,7 +49,7 @@ class SemanticAggregator(BaseAgenticComponent): *get_model_provider_inputs(), DataFrameInput( name="source", - display_name="Input DataFrame", + display_name="Input Table", info="Input DataFrame to aggregate. The schema is automatically inferred from column names and types.", required=True, ), @@ -75,7 +75,7 @@ class SemanticAggregator(BaseAgenticComponent): Output( name="states", method="aReduce", - display_name="Output DataFrame", + display_name="Output Table", info="Aggregated DataFrame generated by the LLM following the specified output schema.", tool_mode=True, ), diff --git a/src/lfx/src/lfx/components/agentics/semantic_map.py b/src/lfx/src/lfx/components/agentics/semantic_map.py index 08b1211b78b6..309616346450 100644 --- a/src/lfx/src/lfx/components/agentics/semantic_map.py +++ b/src/lfx/src/lfx/components/agentics/semantic_map.py @@ -49,7 +49,7 @@ class SemanticMap(BaseAgenticComponent): *get_model_provider_inputs(), DataFrameInput( name="source", - display_name="Input DataFrame", + display_name="Input Table", info=("Input DataFrame to transform. The schema is automatically inferred from column names and types."), ), get_generated_fields_input(), @@ -84,7 +84,7 @@ class SemanticMap(BaseAgenticComponent): outputs = [ Output( name="states", - display_name="Output DataFrame", + display_name="Output Table", info="Transformed DataFrame resulting from semantic mapping.", method="aMap", tool_mode=True, diff --git a/src/lfx/src/lfx/components/agentics/synthetic_data_generator.py b/src/lfx/src/lfx/components/agentics/synthetic_data_generator.py index 1ccccec7da98..4e12fba35863 100644 --- a/src/lfx/src/lfx/components/agentics/synthetic_data_generator.py +++ b/src/lfx/src/lfx/components/agentics/synthetic_data_generator.py @@ -49,7 +49,7 @@ class SyntheticDataGenerator(BaseAgenticComponent): ), DataFrameInput( name="source", - display_name="Input DataFrame", + display_name="Input Table", info=( "Provide example DataFrame to learn from and generate similar data. " "Only the first 50 rows will be used as examples." @@ -77,7 +77,7 @@ class SyntheticDataGenerator(BaseAgenticComponent): outputs = [ Output( name="states", - display_name="Output DataFrame", + display_name="Output Table", info="Synthetic DataFrame generated by the LLM based on the schema or example data.", method="aGenerate", tool_mode=True, diff --git a/src/lfx/src/lfx/components/agentql/agentql_api.py b/src/lfx/src/lfx/components/agentql/agentql_api.py index 85740a4aa075..8395c3eee49e 100644 --- a/src/lfx/src/lfx/components/agentql/agentql_api.py +++ b/src/lfx/src/lfx/components/agentql/agentql_api.py @@ -90,7 +90,7 @@ class AgentQL(Component): ] outputs = [ - Output(display_name="Data", name="data", method="build_output"), + Output(display_name="JSON", name="data", method="build_output"), ] def build_output(self) -> Data: diff --git a/src/lfx/src/lfx/components/amazon/s3_bucket_uploader.py b/src/lfx/src/lfx/components/amazon/s3_bucket_uploader.py index 4d3701fe6754..9217357c1198 100644 --- a/src/lfx/src/lfx/components/amazon/s3_bucket_uploader.py +++ b/src/lfx/src/lfx/components/amazon/s3_bucket_uploader.py @@ -88,7 +88,7 @@ class S3BucketUploaderComponent(Component): name="data_inputs", display_name="Data Inputs", info="The data to split.", - input_types=["Data"], + input_types=["Data", "JSON"], is_list=True, required=True, ), diff --git a/src/lfx/src/lfx/components/arxiv/arxiv.py b/src/lfx/src/lfx/components/arxiv/arxiv.py index eb37b34b3886..6bb782337d6f 100644 --- a/src/lfx/src/lfx/components/arxiv/arxiv.py +++ b/src/lfx/src/lfx/components/arxiv/arxiv.py @@ -38,7 +38,7 @@ class ArXivComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="search_papers_dataframe"), + Output(display_name="Table", name="dataframe", method="search_papers_dataframe"), ] def build_query_url(self) -> str: diff --git a/src/lfx/src/lfx/components/bing/bing_search_api.py b/src/lfx/src/lfx/components/bing/bing_search_api.py index 1f7d06181e28..ffc2cadc34d6 100644 --- a/src/lfx/src/lfx/components/bing/bing_search_api.py +++ b/src/lfx/src/lfx/components/bing/bing_search_api.py @@ -28,7 +28,7 @@ class BingSearchAPIComponent(LCToolComponent): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), Output(display_name="Tool", name="tool", method="build_tool"), ] diff --git a/src/lfx/src/lfx/components/confluence/confluence.py b/src/lfx/src/lfx/components/confluence/confluence.py index 57f934366360..ce62870de0a5 100644 --- a/src/lfx/src/lfx/components/confluence/confluence.py +++ b/src/lfx/src/lfx/components/confluence/confluence.py @@ -61,7 +61,7 @@ class ConfluenceComponent(Component): ] outputs = [ - Output(name="data", display_name="Data", method="load_documents"), + Output(name="data", display_name="JSON", method="load_documents"), ] def build_confluence(self) -> ConfluenceLoader: diff --git a/src/lfx/src/lfx/components/data_source/api_request.py b/src/lfx/src/lfx/components/data_source/api_request.py index a3d601097d7f..e599d6a4062f 100644 --- a/src/lfx/src/lfx/components/data_source/api_request.py +++ b/src/lfx/src/lfx/components/data_source/api_request.py @@ -109,7 +109,7 @@ class APIRequestComponent(Component): }, ], value=[], - input_types=["Data"], + input_types=["Data", "JSON"], advanced=True, real_time_refresh=True, ), @@ -133,7 +133,7 @@ class APIRequestComponent(Component): ], value=[{"key": "User-Agent", "value": "Langflow/1.0"}], advanced=True, - input_types=["Data"], + input_types=["Data", "JSON"], real_time_refresh=True, ), IntInput( diff --git a/src/lfx/src/lfx/components/data_source/csv_to_data.py b/src/lfx/src/lfx/components/data_source/csv_to_data.py index 8e0eeb10dd9f..d31f5ac43c8a 100644 --- a/src/lfx/src/lfx/components/data_source/csv_to_data.py +++ b/src/lfx/src/lfx/components/data_source/csv_to_data.py @@ -43,7 +43,7 @@ class CSVToDataComponent(Component): ] outputs = [ - Output(name="data_list", display_name="Data List", method="load_csv_to_data"), + Output(name="data_list", display_name="JSON List", method="load_csv_to_data"), ] def load_csv_to_data(self) -> list[Data]: diff --git a/src/lfx/src/lfx/components/data_source/json_to_data.py b/src/lfx/src/lfx/components/data_source/json_to_data.py index fc9500d6b7d5..387fbb3602d7 100644 --- a/src/lfx/src/lfx/components/data_source/json_to_data.py +++ b/src/lfx/src/lfx/components/data_source/json_to_data.py @@ -40,7 +40,7 @@ class JSONToDataComponent(Component): ] outputs = [ - Output(name="data", display_name="Data", method="convert_json_to_data"), + Output(name="data", display_name="JSON", method="convert_json_to_data"), ] def convert_json_to_data(self) -> Data | list[Data]: diff --git a/src/lfx/src/lfx/components/data_source/url.py b/src/lfx/src/lfx/components/data_source/url.py index 9bd66e71d50c..888c7d8cb0c9 100644 --- a/src/lfx/src/lfx/components/data_source/url.py +++ b/src/lfx/src/lfx/components/data_source/url.py @@ -146,7 +146,7 @@ class URLComponent(Component): ], value=[{"key": "User-Agent", "value": USER_AGENT}], advanced=True, - input_types=["DataFrame"], + input_types=["DataFrame", "Table"], ), BoolInput( name="filter_text_html", diff --git a/src/lfx/src/lfx/components/deactivated/ingestion.py b/src/lfx/src/lfx/components/deactivated/ingestion.py index beb8ad2d328e..abbb90c54782 100644 --- a/src/lfx/src/lfx/components/deactivated/ingestion.py +++ b/src/lfx/src/lfx/components/deactivated/ingestion.py @@ -147,7 +147,7 @@ class NewKnowledgeBaseInput: "Accepts Message, Data, or DataFrame. If Message or Data is provided, " "it is converted to a DataFrame automatically." ), - input_types=["Message", "Data", "DataFrame"], + input_types=["Message", "Data", "JSON", "DataFrame", "Table"], required=True, ), TableInput( diff --git a/src/lfx/src/lfx/components/deactivated/split_text.py b/src/lfx/src/lfx/components/deactivated/split_text.py index e925331f6853..0571e3ad5114 100644 --- a/src/lfx/src/lfx/components/deactivated/split_text.py +++ b/src/lfx/src/lfx/components/deactivated/split_text.py @@ -17,7 +17,7 @@ class SplitTextComponent(Component): name="data_inputs", display_name="Data Inputs", info="The data to split.", - input_types=["Data"], + input_types=["Data", "JSON"], is_list=True, ), IntInput( diff --git a/src/lfx/src/lfx/components/docling/chunk_docling_document.py b/src/lfx/src/lfx/components/docling/chunk_docling_document.py index 5142179f693a..4137bf75f39b 100644 --- a/src/lfx/src/lfx/components/docling/chunk_docling_document.py +++ b/src/lfx/src/lfx/components/docling/chunk_docling_document.py @@ -20,9 +20,9 @@ class ChunkDoclingDocumentComponent(Component): inputs = [ HandleInput( name="data_inputs", - display_name="Data or DataFrame", + display_name="JSON or Table", info="The data with documents to split in chunks.", - input_types=["Data", "DataFrame"], + input_types=["Data", "JSON", "DataFrame", "Table"], required=True, ), DropdownInput( @@ -103,7 +103,7 @@ class ChunkDoclingDocumentComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="chunk_documents"), + Output(display_name="Table", name="dataframe", method="chunk_documents"), ] def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None) -> dict: diff --git a/src/lfx/src/lfx/components/docling/export_docling_document.py b/src/lfx/src/lfx/components/docling/export_docling_document.py index 2149d8c9e0de..2e6574174880 100644 --- a/src/lfx/src/lfx/components/docling/export_docling_document.py +++ b/src/lfx/src/lfx/components/docling/export_docling_document.py @@ -18,9 +18,9 @@ class ExportDoclingDocumentComponent(Component): inputs = [ HandleInput( name="data_inputs", - display_name="Data or DataFrame", + display_name="JSON or Table", info="The data with documents to export.", - input_types=["Data", "DataFrame"], + input_types=["Data", "JSON", "DataFrame", "Table"], required=True, ), DropdownInput( @@ -66,7 +66,7 @@ class ExportDoclingDocumentComponent(Component): outputs = [ Output(display_name="Exported data", name="data", method="export_document"), - Output(display_name="DataFrame", name="dataframe", method="as_dataframe"), + Output(display_name="Table", name="dataframe", method="as_dataframe"), ] def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: diff --git a/src/lfx/src/lfx/components/duckduckgo/duck_duck_go_search_run.py b/src/lfx/src/lfx/components/duckduckgo/duck_duck_go_search_run.py index 2f84d754f236..b08c2f4d1a4e 100644 --- a/src/lfx/src/lfx/components/duckduckgo/duck_duck_go_search_run.py +++ b/src/lfx/src/lfx/components/duckduckgo/duck_duck_go_search_run.py @@ -42,7 +42,7 @@ class DuckDuckGoSearchComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), ] def _build_wrapper(self) -> DuckDuckGoSearchRun: diff --git a/src/lfx/src/lfx/components/elastic/opensearch.py b/src/lfx/src/lfx/components/elastic/opensearch.py index 3a0753c160c8..4ed4dce45e41 100644 --- a/src/lfx/src/lfx/components/elastic/opensearch.py +++ b/src/lfx/src/lfx/components/elastic/opensearch.py @@ -84,7 +84,7 @@ class OpenSearchVectorStoreComponent(LCVectorStoreComponent): }, ], value=[], - input_types=["Data"], + input_types=["Data", "JSON"], ), StrInput( name="opensearch_url", diff --git a/src/lfx/src/lfx/components/elastic/opensearch_multimodal.py b/src/lfx/src/lfx/components/elastic/opensearch_multimodal.py index 7fc7f4707631..fe54effc8c27 100644 --- a/src/lfx/src/lfx/components/elastic/opensearch_multimodal.py +++ b/src/lfx/src/lfx/components/elastic/opensearch_multimodal.py @@ -154,7 +154,7 @@ class OpenSearchVectorStoreComponentMultimodalMultiEmbedding(LCVectorStoreCompon }, ], value=[], - input_types=["Data"], + input_types=["Data", "JSON"], ), StrInput( name="opensearch_url", diff --git a/src/lfx/src/lfx/components/files_and_knowledge/save_file.py b/src/lfx/src/lfx/components/files_and_knowledge/save_file.py index e6c0661fcaf5..1d3b9dfcbb0e 100644 --- a/src/lfx/src/lfx/components/files_and_knowledge/save_file.py +++ b/src/lfx/src/lfx/components/files_and_knowledge/save_file.py @@ -70,7 +70,7 @@ class SaveToFileComponent(Component): display_name="File Content", info="The input to save.", dynamic=True, - input_types=["Data", "DataFrame", "Message"], + input_types=["Data", "JSON", "DataFrame", "Table", "Message"], required=True, ), StrInput( diff --git a/src/lfx/src/lfx/components/firecrawl/firecrawl_crawl_api.py b/src/lfx/src/lfx/components/firecrawl/firecrawl_crawl_api.py index a72fe27e18f0..a1315c964cd4 100644 --- a/src/lfx/src/lfx/components/firecrawl/firecrawl_crawl_api.py +++ b/src/lfx/src/lfx/components/firecrawl/firecrawl_crawl_api.py @@ -50,7 +50,7 @@ class FirecrawlCrawlApi(Component): ] outputs = [ - Output(display_name="Data", name="data", method="crawl"), + Output(display_name="JSON", name="data", method="crawl"), ] idempotency_key: str | None = None diff --git a/src/lfx/src/lfx/components/firecrawl/firecrawl_extract_api.py b/src/lfx/src/lfx/components/firecrawl/firecrawl_extract_api.py index fb5a0ed471fc..107c6cbc654a 100644 --- a/src/lfx/src/lfx/components/firecrawl/firecrawl_extract_api.py +++ b/src/lfx/src/lfx/components/firecrawl/firecrawl_extract_api.py @@ -65,7 +65,7 @@ class FirecrawlExtractApi(Component): ] outputs = [ - Output(display_name="Data", name="data", method="extract"), + Output(display_name="JSON", name="data", method="extract"), ] def extract(self) -> Data: diff --git a/src/lfx/src/lfx/components/firecrawl/firecrawl_map_api.py b/src/lfx/src/lfx/components/firecrawl/firecrawl_map_api.py index 225ab6bc329f..9f33514487a5 100644 --- a/src/lfx/src/lfx/components/firecrawl/firecrawl_map_api.py +++ b/src/lfx/src/lfx/components/firecrawl/firecrawl_map_api.py @@ -48,7 +48,7 @@ class FirecrawlMapApi(Component): ] outputs = [ - Output(display_name="Data", name="data", method="map"), + Output(display_name="JSON", name="data", method="map"), ] def map(self) -> Data: diff --git a/src/lfx/src/lfx/components/firecrawl/firecrawl_scrape_api.py b/src/lfx/src/lfx/components/firecrawl/firecrawl_scrape_api.py index c7fdc813b00b..718146ea6d56 100644 --- a/src/lfx/src/lfx/components/firecrawl/firecrawl_scrape_api.py +++ b/src/lfx/src/lfx/components/firecrawl/firecrawl_scrape_api.py @@ -49,7 +49,7 @@ class FirecrawlScrapeApi(Component): ] outputs = [ - Output(display_name="Data", name="data", method="scrape"), + Output(display_name="JSON", name="data", method="scrape"), ] def scrape(self) -> Data: diff --git a/src/lfx/src/lfx/components/flow_controls/listen.py b/src/lfx/src/lfx/components/flow_controls/listen.py index 6a30b7b29f1a..de1406546bc9 100644 --- a/src/lfx/src/lfx/components/flow_controls/listen.py +++ b/src/lfx/src/lfx/components/flow_controls/listen.py @@ -19,7 +19,7 @@ class ListenComponent(Component): ) ] - outputs = [Output(name="data", display_name="Data", method="listen_for_data", cache=False)] + outputs = [Output(name="data", display_name="JSON", method="listen_for_data", cache=False)] def listen_for_data(self) -> Data: """Retrieves a Data object from the component context using the provided context key. diff --git a/src/lfx/src/lfx/components/flow_controls/loop.py b/src/lfx/src/lfx/components/flow_controls/loop.py index 50ed249cc94d..f01d4f3d2ae6 100644 --- a/src/lfx/src/lfx/components/flow_controls/loop.py +++ b/src/lfx/src/lfx/components/flow_controls/loop.py @@ -29,7 +29,7 @@ class LoopComponent(Component): name="data", display_name="Inputs", info="The initial DataFrame to iterate over.", - input_types=["DataFrame"], + input_types=["DataFrame", "Table"], ), ] diff --git a/src/lfx/src/lfx/components/flow_controls/notify.py b/src/lfx/src/lfx/components/flow_controls/notify.py index b722eff54734..eb66f482db9d 100644 --- a/src/lfx/src/lfx/components/flow_controls/notify.py +++ b/src/lfx/src/lfx/components/flow_controls/notify.py @@ -24,7 +24,7 @@ class NotifyComponent(Component): display_name="Input Data", info="The data to store.", required=False, - input_types=["Data", "Message", "DataFrame"], + input_types=["Data", "JSON", "Message", "DataFrame", "Table"], ), BoolInput( name="append", @@ -37,7 +37,7 @@ class NotifyComponent(Component): outputs = [ Output( - display_name="Data", + display_name="JSON", name="result", method="notify_components", cache=False, diff --git a/src/lfx/src/lfx/components/git/git.py b/src/lfx/src/lfx/components/git/git.py index 512843703de7..778be7ea6ba4 100644 --- a/src/lfx/src/lfx/components/git/git.py +++ b/src/lfx/src/lfx/components/git/git.py @@ -75,7 +75,7 @@ class GitLoaderComponent(Component): ] outputs = [ - Output(name="data", display_name="Data", method="load_documents"), + Output(name="data", display_name="JSON", method="load_documents"), ] @staticmethod diff --git a/src/lfx/src/lfx/components/glean/glean_search_api.py b/src/lfx/src/lfx/components/glean/glean_search_api.py index 2729654b53b6..4c2c5268ea5a 100644 --- a/src/lfx/src/lfx/components/glean/glean_search_api.py +++ b/src/lfx/src/lfx/components/glean/glean_search_api.py @@ -105,7 +105,7 @@ class GleanSearchAPIComponent(LCToolComponent): icon: str = "Glean" outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), ] inputs = [ diff --git a/src/lfx/src/lfx/components/google/gmail.py b/src/lfx/src/lfx/components/google/gmail.py index 4fbd16f8c512..adc9708bf402 100644 --- a/src/lfx/src/lfx/components/google/gmail.py +++ b/src/lfx/src/lfx/components/google/gmail.py @@ -64,7 +64,7 @@ class GmailLoaderComponent(Component): ] outputs = [ - Output(display_name="Data", name="data", method="load_emails"), + Output(display_name="JSON", name="data", method="load_emails"), ] def load_emails(self) -> Data: diff --git a/src/lfx/src/lfx/components/google/google_drive_search.py b/src/lfx/src/lfx/components/google/google_drive_search.py index 01aff16e7cc9..a622223c3518 100644 --- a/src/lfx/src/lfx/components/google/google_drive_search.py +++ b/src/lfx/src/lfx/components/google/google_drive_search.py @@ -73,7 +73,7 @@ class GoogleDriveSearchComponent(Component): Output(display_name="Document URLs", name="doc_urls", method="search_doc_urls"), Output(display_name="Document IDs", name="doc_ids", method="search_doc_ids"), Output(display_name="Document Titles", name="doc_titles", method="search_doc_titles"), - Output(display_name="Data", name="Data", method="search_data"), + Output(display_name="JSON", name="Data", method="search_data"), ] def generate_query_string(self) -> str: diff --git a/src/lfx/src/lfx/components/input_output/chat_output.py b/src/lfx/src/lfx/components/input_output/chat_output.py index 4e9efa2247db..f8c354d48fd3 100644 --- a/src/lfx/src/lfx/components/input_output/chat_output.py +++ b/src/lfx/src/lfx/components/input_output/chat_output.py @@ -32,7 +32,7 @@ class ChatOutput(ChatComponent): name="input_value", display_name="Inputs", info="Message to be passed as output.", - input_types=["Data", "DataFrame", "Message"], + input_types=["Data", "JSON", "DataFrame", "Table", "Message"], required=True, ), BoolInput( diff --git a/src/lfx/src/lfx/components/input_output/webhook.py b/src/lfx/src/lfx/components/input_output/webhook.py index 2293eaae8afd..1873bc307f81 100644 --- a/src/lfx/src/lfx/components/input_output/webhook.py +++ b/src/lfx/src/lfx/components/input_output/webhook.py @@ -35,7 +35,7 @@ class WebhookComponent(Component): ), ] outputs = [ - Output(display_name="Data", name="output_data", method="build_data"), + Output(display_name="JSON", name="output_data", method="build_data"), ] def build_data(self) -> Data: diff --git a/src/lfx/src/lfx/components/langchain_utilities/character.py b/src/lfx/src/lfx/components/langchain_utilities/character.py index 61003fe776e4..247a04eb662b 100644 --- a/src/lfx/src/lfx/components/langchain_utilities/character.py +++ b/src/lfx/src/lfx/components/langchain_utilities/character.py @@ -31,7 +31,7 @@ class CharacterTextSplitterComponent(LCTextSplitterComponent): name="data_input", display_name="Input", info="The texts to split.", - input_types=["Document", "Data"], + input_types=["Document", "Data", "JSON"], required=True, ), MessageTextInput( diff --git a/src/lfx/src/lfx/components/langchain_utilities/html_link_extractor.py b/src/lfx/src/lfx/components/langchain_utilities/html_link_extractor.py index e1e3b9fce9ff..d24991fe233c 100644 --- a/src/lfx/src/lfx/components/langchain_utilities/html_link_extractor.py +++ b/src/lfx/src/lfx/components/langchain_utilities/html_link_extractor.py @@ -21,7 +21,7 @@ class HtmlLinkExtractorComponent(LCDocumentTransformerComponent): name="data_input", display_name="Input", info="The texts from which to extract links.", - input_types=["Document", "Data"], + input_types=["Document", "Data", "JSON"], required=True, ), ] diff --git a/src/lfx/src/lfx/components/langchain_utilities/language_recursive.py b/src/lfx/src/lfx/components/langchain_utilities/language_recursive.py index 71f53f75388f..cd0a28fd1697 100644 --- a/src/lfx/src/lfx/components/langchain_utilities/language_recursive.py +++ b/src/lfx/src/lfx/components/langchain_utilities/language_recursive.py @@ -30,7 +30,7 @@ class LanguageRecursiveTextSplitterComponent(LCTextSplitterComponent): name="data_input", display_name="Input", info="The texts to split.", - input_types=["Document", "Data"], + input_types=["Document", "Data", "JSON"], required=True, ), DropdownInput( diff --git a/src/lfx/src/lfx/components/langchain_utilities/language_semantic.py b/src/lfx/src/lfx/components/langchain_utilities/language_semantic.py index 5982344c5401..93f5021767b3 100644 --- a/src/lfx/src/lfx/components/langchain_utilities/language_semantic.py +++ b/src/lfx/src/lfx/components/langchain_utilities/language_semantic.py @@ -28,7 +28,7 @@ class SemanticTextSplitterComponent(LCTextSplitterComponent): name="data_inputs", display_name="Data Inputs", info="List of Data objects containing text and metadata to split.", - input_types=["Data"], + input_types=["Data", "JSON"], is_list=True, required=True, ), diff --git a/src/lfx/src/lfx/components/langchain_utilities/natural_language.py b/src/lfx/src/lfx/components/langchain_utilities/natural_language.py index f8f3b21f0f3e..57a2221d7dbe 100644 --- a/src/lfx/src/lfx/components/langchain_utilities/natural_language.py +++ b/src/lfx/src/lfx/components/langchain_utilities/natural_language.py @@ -32,7 +32,7 @@ class NaturalLanguageTextSplitterComponent(LCTextSplitterComponent): name="data_input", display_name="Input", info="The text data to be split.", - input_types=["Document", "Data"], + input_types=["Document", "Data", "JSON"], required=True, ), MessageTextInput( diff --git a/src/lfx/src/lfx/components/langchain_utilities/recursive_character.py b/src/lfx/src/lfx/components/langchain_utilities/recursive_character.py index 772b7142bf3c..93f6cc8b8de5 100644 --- a/src/lfx/src/lfx/components/langchain_utilities/recursive_character.py +++ b/src/lfx/src/lfx/components/langchain_utilities/recursive_character.py @@ -31,7 +31,7 @@ class RecursiveCharacterTextSplitterComponent(LCTextSplitterComponent): name="data_input", display_name="Input", info="The texts to split.", - input_types=["Document", "Data"], + input_types=["Document", "Data", "JSON"], required=True, ), MessageTextInput( diff --git a/src/lfx/src/lfx/components/langchain_utilities/self_query.py b/src/lfx/src/lfx/components/langchain_utilities/self_query.py index 509b94762c2d..ee626f28b0d1 100644 --- a/src/lfx/src/lfx/components/langchain_utilities/self_query.py +++ b/src/lfx/src/lfx/components/langchain_utilities/self_query.py @@ -32,7 +32,7 @@ class SelfQueryRetrieverComponent(Component): name="attribute_infos", display_name="Metadata Field Info", info="Metadata Field Info to be passed as input.", - input_types=["Data"], + input_types=["Data", "JSON"], is_list=True, ), MessageTextInput( diff --git a/src/lfx/src/lfx/components/llm_operations/batch_run.py b/src/lfx/src/lfx/components/llm_operations/batch_run.py index c732576407cb..e9a643e2748f 100644 --- a/src/lfx/src/lfx/components/llm_operations/batch_run.py +++ b/src/lfx/src/lfx/components/llm_operations/batch_run.py @@ -47,7 +47,7 @@ class BatchRunComponent(Component): ), DataFrameInput( name="df", - display_name="DataFrame", + display_name="Table", info="The DataFrame whose column (specified by 'column_name') we'll treat as text messages.", required=True, ), diff --git a/src/lfx/src/lfx/components/llm_operations/lambda_filter.py b/src/lfx/src/lfx/components/llm_operations/lambda_filter.py index ee25c398ec05..9524180f72d6 100644 --- a/src/lfx/src/lfx/components/llm_operations/lambda_filter.py +++ b/src/lfx/src/lfx/components/llm_operations/lambda_filter.py @@ -49,9 +49,9 @@ class LambdaFilterComponent(Component): inputs = [ DataInput( name="data", - display_name="Data", + display_name="JSON", info="The structured data or text messages to filter or transform using a lambda function.", - input_types=["Data", "DataFrame", "Message"], + input_types=["Data", "JSON", "DataFrame", "Table", "Message"], is_list=True, required=True, ), diff --git a/src/lfx/src/lfx/components/mem0/mem0_chat_memory.py b/src/lfx/src/lfx/components/mem0/mem0_chat_memory.py index 05d32fafe02d..40d31a21d3d3 100644 --- a/src/lfx/src/lfx/components/mem0/mem0_chat_memory.py +++ b/src/lfx/src/lfx/components/mem0/mem0_chat_memory.py @@ -36,7 +36,7 @@ class Mem0MemoryComponent(LCChatMemoryComponent): }, "version": "v1.1" }""", - input_types=["Data"], + input_types=["Data", "JSON"], ), MessageTextInput( name="ingest_message", diff --git a/src/lfx/src/lfx/components/models_and_agents/memory.py b/src/lfx/src/lfx/components/models_and_agents/memory.py index 051c530a3ade..dcb826491d50 100644 --- a/src/lfx/src/lfx/components/models_and_agents/memory.py +++ b/src/lfx/src/lfx/components/models_and_agents/memory.py @@ -116,7 +116,7 @@ class MemoryComponent(Component): outputs = [ Output(display_name="Message", name="messages_text", method="retrieve_messages_as_text", dynamic=True), - Output(display_name="Dataframe", name="dataframe", method="retrieve_messages_dataframe", dynamic=True), + Output(display_name="Table", name="dataframe", method="retrieve_messages_dataframe", dynamic=True), ] def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict: @@ -139,9 +139,7 @@ def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) Output( display_name="Messages", name="messages_text", method="retrieve_messages_as_text", dynamic=True ), - Output( - display_name="Dataframe", name="dataframe", method="retrieve_messages_dataframe", dynamic=True - ), + Output(display_name="Table", name="dataframe", method="retrieve_messages_dataframe", dynamic=True), ] return frontend_node diff --git a/src/lfx/src/lfx/components/ollama/ollama.py b/src/lfx/src/lfx/components/ollama/ollama.py index f1ff3d54f12c..70d5a54d848f 100644 --- a/src/lfx/src/lfx/components/ollama/ollama.py +++ b/src/lfx/src/lfx/components/ollama/ollama.py @@ -231,8 +231,8 @@ class ChatOllamaComponent(LCModelComponent): outputs = [ Output(display_name="Text", name="text_output", method="text_response"), Output(display_name="Language Model", name="model_output", method="build_model"), - Output(display_name="Data", name="data_output", method="build_data_output"), - Output(display_name="DataFrame", name="dataframe_output", method="build_dataframe_output"), + Output(display_name="JSON", name="data_output", method="build_data_output"), + Output(display_name="Table", name="dataframe_output", method="build_dataframe_output"), ] def build_model(self) -> LanguageModel: # type: ignore[type-var] diff --git a/src/lfx/src/lfx/components/processing/alter_metadata.py b/src/lfx/src/lfx/components/processing/alter_metadata.py index de90ef36d632..76f9eecbd1e9 100644 --- a/src/lfx/src/lfx/components/processing/alter_metadata.py +++ b/src/lfx/src/lfx/components/processing/alter_metadata.py @@ -19,7 +19,7 @@ class AlterMetadataComponent(Component): display_name="Input", info="Object(s) to which Metadata should be added", required=False, - input_types=["Message", "Data"], + input_types=["Message", "Data", "JSON"], is_list=True, ), StrInput( @@ -32,7 +32,7 @@ class AlterMetadataComponent(Component): name="metadata", display_name="Metadata", info="Metadata to add to each object", - input_types=["Data"], + input_types=["Data", "JSON"], required=True, ), MessageTextInput( @@ -47,12 +47,12 @@ class AlterMetadataComponent(Component): outputs = [ Output( name="data", - display_name="Data", + display_name="JSON", info="List of Input objects each with added Metadata", method="process_output", ), Output( - display_name="DataFrame", + display_name="Table", name="dataframe", info="Data objects as a DataFrame, with metadata as columns", method="as_dataframe", diff --git a/src/lfx/src/lfx/components/processing/converter.py b/src/lfx/src/lfx/components/processing/converter.py index ba72e7f6f35d..912555ea2e9b 100644 --- a/src/lfx/src/lfx/components/processing/converter.py +++ b/src/lfx/src/lfx/components/processing/converter.py @@ -4,6 +4,8 @@ from lfx.custom import Component from lfx.io import BoolInput, HandleInput, Output, TabInput from lfx.schema import Data, DataFrame, Message +from lfx.schema.data import JSON +from lfx.schema.dataframe import Table MIN_CSV_LINES = 2 @@ -20,15 +22,15 @@ def convert_to_message(v) -> Message: return v if isinstance(v, Message) else v.to_message() -def convert_to_data(v: DataFrame | Data | Message | dict, *, auto_parse: bool) -> Data: - """Convert input to Data type. +def convert_to_data(v: Table | Data | Message | dict, *, auto_parse: bool) -> JSON: + """Convert input to JSON type. Args: - v: Input to convert (Message, Data, DataFrame, or dict) + v: Input to convert (Message, Data, Table, or dict) auto_parse: Enable automatic parsing of structured data (JSON/CSV) Returns: - Data: Converted Data object + JSON: Converted JSON object """ if isinstance(v, dict): return Data(v) @@ -39,15 +41,15 @@ def convert_to_data(v: DataFrame | Data | Message | dict, *, auto_parse: bool) - return v if isinstance(v, Data) else v.to_data() -def convert_to_dataframe(v: DataFrame | Data | Message | dict, *, auto_parse: bool) -> DataFrame: - """Convert input to DataFrame type. +def convert_to_dataframe(v: Table | Data | Message | dict, *, auto_parse: bool) -> Table: + """Convert input to Table type. Args: - v: Input to convert (Message, Data, DataFrame, or dict) + v: Input to convert (Message, Data, Table, or dict) auto_parse: Enable automatic parsing of structured data (JSON/CSV) Returns: - DataFrame: Converted DataFrame object + Table: Converted Table object """ import pandas as pd @@ -67,14 +69,14 @@ def convert_to_dataframe(v: DataFrame | Data | Message | dict, *, auto_parse: bo return v.to_dataframe() -def parse_structured_data(data: Data) -> Data: - """Parse structured data (JSON, CSV) from Data's text field. +def parse_structured_data(data: JSON) -> JSON: + """Parse structured data (JSON, CSV) from JSON's text field. Args: - data: Data object with text content to parse + data: JSON object with text content to parse Returns: - Data: Modified Data object with parsed content or original if parsing fails + JSON: Modified JSON object with parsed content or original if parsing fails """ raw_text = data.get_text() or "" text = raw_text.lstrip("\ufeff").strip() @@ -96,8 +98,8 @@ def parse_structured_data(data: Data) -> Data: return data -def _try_parse_json(text: str) -> Data | None: - """Try to parse text as JSON and return Data object.""" +def _try_parse_json(text: str) -> JSON | None: + """Try to parse text as JSON and return JSON object.""" try: parsed = json.loads(text) @@ -105,7 +107,7 @@ def _try_parse_json(text: str) -> Data | None: # Single JSON object return Data(data=parsed) if isinstance(parsed, list) and all(isinstance(item, dict) for item in parsed): - # Array of JSON objects - create Data with the list + # Array of JSON objects - create JSON with the list return Data(data={"records": parsed}) except (json.JSONDecodeError, ValueError): @@ -124,8 +126,8 @@ def _looks_like_csv(text: str) -> bool: return "," in header_line and len(lines) > 1 -def _parse_csv_to_data(text: str) -> Data: - """Parse CSV text and return Data object.""" +def _parse_csv_to_data(text: str) -> JSON: + """Parse CSV text and return JSON object.""" from io import StringIO import pandas as pd @@ -139,7 +141,7 @@ def _parse_csv_to_data(text: str) -> Data: class TypeConverterComponent(Component): display_name = "Type Convert" - description = "Convert between different types (Message, Data, DataFrame)" + description = "Convert between different types (Message, JSON, Table)" documentation: str = "https://docs.langflow.org/type-convert" icon = "repeat" @@ -147,8 +149,8 @@ class TypeConverterComponent(Component): HandleInput( name="input_data", display_name="Input", - input_types=["Message", "Data", "DataFrame"], - info="Accept Message, Data or DataFrame as input", + input_types=["Message", "Data", "JSON", "DataFrame", "Table"], + info="Accept Message, JSON or Table as input", required=True, ), BoolInput( @@ -162,50 +164,79 @@ class TypeConverterComponent(Component): TabInput( name="output_type", display_name="Output Type", - options=["Message", "Data", "DataFrame"], + options=["Message", "JSON", "Table"], info="Select the desired output data type", real_time_refresh=True, value="Message", ), ] + # Define ALL outputs so they exist during validation + # update_frontend_node will filter to show only the selected one outputs = [ Output( display_name="Message Output", name="message_output", method="convert_to_message", - ) + types=["Message"], + ), + Output( + display_name="JSON Output", + name="data_output", + method="convert_to_data", + types=["JSON"], + ), + Output( + display_name="Table Output", + name="dataframe_output", + method="convert_to_dataframe", + types=["Table"], + ), ] + async def update_frontend_node(self, new_frontend_node: dict, current_frontend_node: dict): + """Ensure outputs are synced with output_type when component is loaded.""" + # Call parent implementation first + await super().update_frontend_node(new_frontend_node, current_frontend_node) + + # Then sync outputs with current output_type value + output_type = new_frontend_node.get("template", {}).get("output_type", {}).get("value", "Message") + self.update_outputs(new_frontend_node, "output_type", output_type) + + return new_frontend_node + def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict: """Dynamically show only the relevant output based on the selected output type.""" if field_name == "output_type": # Start with empty outputs frontend_node["outputs"] = [] - # Add only the selected output type + # Add only the selected output type WITH TYPES SPECIFIED if field_value == "Message": frontend_node["outputs"].append( Output( display_name="Message Output", name="message_output", method="convert_to_message", + types=["Message"], ).to_dict() ) - elif field_value == "Data": + elif field_value in ("Data", "JSON"): frontend_node["outputs"].append( Output( - display_name="Data Output", + display_name="JSON Output", name="data_output", method="convert_to_data", + types=["JSON"], ).to_dict() ) - elif field_value == "DataFrame": + elif field_value in ("DataFrame", "Table"): frontend_node["outputs"].append( Output( - display_name="DataFrame Output", + display_name="Table Output", name="dataframe_output", method="convert_to_dataframe", + types=["Table"], ).to_dict() ) @@ -223,8 +254,8 @@ def convert_to_message(self) -> Message: self.status = result return result - def convert_to_data(self) -> Data: - """Convert input to Data type.""" + def convert_to_data(self) -> JSON: + """Convert input to JSON type.""" input_value = self.input_data[0] if isinstance(self.input_data, list) else self.input_data # Handle string input by converting to Message first @@ -235,8 +266,8 @@ def convert_to_data(self) -> Data: self.status = result return result - def convert_to_dataframe(self) -> DataFrame: - """Convert input to DataFrame type.""" + def convert_to_dataframe(self) -> Table: + """Convert input to Table type.""" input_value = self.input_data[0] if isinstance(self.input_data, list) else self.input_data # Handle string input by converting to Message first diff --git a/src/lfx/src/lfx/components/processing/create_data.py b/src/lfx/src/lfx/components/processing/create_data.py index 846475047c45..5265b939ee80 100644 --- a/src/lfx/src/lfx/components/processing/create_data.py +++ b/src/lfx/src/lfx/components/processing/create_data.py @@ -41,7 +41,7 @@ class CreateDataComponent(Component): ] outputs = [ - Output(display_name="Data", name="data", method="build_data"), + Output(display_name="JSON", name="data", method="build_data"), ] def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None): @@ -75,7 +75,7 @@ def update_build_config(self, build_config: dotdict, field_value: Any, field_nam display_name=f"Field {i}", name=key, info=f"Key for field {i}.", - input_types=["Message", "Data"], + input_types=["Message", "Data", "JSON"], ) build_config[field.name] = field.to_dict() diff --git a/src/lfx/src/lfx/components/processing/create_list.py b/src/lfx/src/lfx/components/processing/create_list.py index 3d31164d694d..e2013a34bc19 100644 --- a/src/lfx/src/lfx/components/processing/create_list.py +++ b/src/lfx/src/lfx/components/processing/create_list.py @@ -22,8 +22,8 @@ class CreateListComponent(Component): ] outputs = [ - Output(display_name="Data List", name="list", method="create_list"), - Output(display_name="DataFrame", name="dataframe", method="as_dataframe"), + Output(display_name="JSON List", name="list", method="create_list"), + Output(display_name="Table", name="dataframe", method="as_dataframe"), ] def create_list(self) -> list[Data]: diff --git a/src/lfx/src/lfx/components/processing/data_operations.py b/src/lfx/src/lfx/components/processing/data_operations.py index 9c6b9fdd76f7..20b74f7be1ec 100644 --- a/src/lfx/src/lfx/components/processing/data_operations.py +++ b/src/lfx/src/lfx/components/processing/data_operations.py @@ -37,14 +37,15 @@ class DataOperationsComponent(Component): - display_name = "Data Operations" - description = "Perform various operations on a Data object." + display_name = "JSON Operations" + description = "Perform various operations on a JSON object." icon = "file-json" name = "DataOperations" default_keys = ["operations", "data"] metadata = { "keywords": [ "data", + "json", "operations", "filter values", "Append or Update", @@ -59,6 +60,7 @@ class DataOperationsComponent(Component): "remove", "rename", "data operations", + "json operations", "data manipulation", "data transformation", "data filtering", @@ -133,7 +135,7 @@ def rename_keys_recursive(obj, rename_map): return obj inputs = [ - DataInput(name="data", display_name="Data", info="Data object to filter.", required=True, is_list=True), + DataInput(name="data", display_name="JSON", info="Data object to filter.", required=True, is_list=True), SortableListInput( name="operations", display_name="Operations", @@ -261,7 +263,7 @@ def rename_keys_recursive(obj, rename_map): } outputs = [ - Output(display_name="Data", name="data_output", method="as_data"), + Output(display_name="JSON", name="data_output", method="as_data"), ] # Helper methods for data operations diff --git a/src/lfx/src/lfx/components/processing/data_to_dataframe.py b/src/lfx/src/lfx/components/processing/data_to_dataframe.py index ef73e1903ff3..18b88946b9a7 100644 --- a/src/lfx/src/lfx/components/processing/data_to_dataframe.py +++ b/src/lfx/src/lfx/components/processing/data_to_dataframe.py @@ -27,7 +27,7 @@ class DataToDataFrameComponent(Component): outputs = [ Output( - display_name="DataFrame", + display_name="Table", name="dataframe", method="build_dataframe", info="A DataFrame built from each Data object's fields plus a 'text' column.", diff --git a/src/lfx/src/lfx/components/processing/dataframe_operations.py b/src/lfx/src/lfx/components/processing/dataframe_operations.py index 747f2acd9e86..ffc8c5298b93 100644 --- a/src/lfx/src/lfx/components/processing/dataframe_operations.py +++ b/src/lfx/src/lfx/components/processing/dataframe_operations.py @@ -8,11 +8,29 @@ class DataFrameOperationsComponent(Component): - display_name = "DataFrame Operations" - description = "Perform various operations on a DataFrame." + display_name = "Table Operations" + description = "Perform various operations on a Table." documentation: str = "https://docs.langflow.org/dataframe-operations" icon = "table" name = "DataFrameOperations" + metadata = { + "keywords": [ + "dataframe", + "dataframe operations", + "table", + "table operations", + "filter", + "sort", + "merge", + "concatenate", + "drop column", + "rename column", + "add column", + "select columns", + "replace value", + "drop duplicates", + ], + } OPERATION_CHOICES = [ "Add Column", @@ -32,7 +50,7 @@ class DataFrameOperationsComponent(Component): inputs = [ DataFrameInput( name="df", - display_name="DataFrame", + display_name="Table", info="The input DataFrame to operate on. Connect multiple DataFrames for merge or concatenate operations.", required=True, is_list=True, @@ -163,7 +181,7 @@ class DataFrameOperationsComponent(Component): outputs = [ Output( - display_name="DataFrame", + display_name="Table", name="output", method="perform_operation", info="The resulting DataFrame after the operation.", diff --git a/src/lfx/src/lfx/components/processing/dynamic_create_data.py b/src/lfx/src/lfx/components/processing/dynamic_create_data.py index fbb1015c560e..7134beb7c045 100644 --- a/src/lfx/src/lfx/components/processing/dynamic_create_data.py +++ b/src/lfx/src/lfx/components/processing/dynamic_create_data.py @@ -62,7 +62,7 @@ def __init__(self, **kwargs): ] outputs = [ - Output(display_name="Data", name="form_data", method="process_form"), + Output(display_name="JSON", name="form_data", method="process_form"), Output(display_name="Message", name="message", method="get_message"), ] diff --git a/src/lfx/src/lfx/components/processing/filter_data.py b/src/lfx/src/lfx/components/processing/filter_data.py index 83cdfe19e774..5b45df7a5bda 100644 --- a/src/lfx/src/lfx/components/processing/filter_data.py +++ b/src/lfx/src/lfx/components/processing/filter_data.py @@ -15,7 +15,7 @@ class FilterDataComponent(Component): inputs = [ DataInput( name="data", - display_name="Data", + display_name="JSON", info="Data object to filter.", ), MessageTextInput( diff --git a/src/lfx/src/lfx/components/processing/filter_data_values.py b/src/lfx/src/lfx/components/processing/filter_data_values.py index 6ef5fac22b0d..c481c29e3226 100644 --- a/src/lfx/src/lfx/components/processing/filter_data_values.py +++ b/src/lfx/src/lfx/components/processing/filter_data_values.py @@ -24,14 +24,14 @@ class DataFilterComponent(Component): display_name="Filter Key", info="The key to filter on (e.g., 'route').", value="route", - input_types=["Data"], + input_types=["Data", "JSON"], ), MessageTextInput( name="filter_value", display_name="Filter Value", info="The value to filter by (e.g., 'CMIP').", value="CMIP", - input_types=["Data"], + input_types=["Data", "JSON"], ), DropdownInput( name="operator", diff --git a/src/lfx/src/lfx/components/processing/merge_data.py b/src/lfx/src/lfx/components/processing/merge_data.py index c5c4e92a5f45..12d2dadaa32f 100644 --- a/src/lfx/src/lfx/components/processing/merge_data.py +++ b/src/lfx/src/lfx/components/processing/merge_data.py @@ -31,7 +31,7 @@ class MergeDataComponent(Component): value=DataOperation.CONCATENATE.value, ), ] - outputs = [Output(display_name="DataFrame", name="combined_data", method="combine_data")] + outputs = [Output(display_name="Table", name="combined_data", method="combine_data")] def combine_data(self) -> DataFrame: if not self.data_inputs or len(self.data_inputs) < self.MIN_INPUTS_REQUIRED: diff --git a/src/lfx/src/lfx/components/processing/message_to_data.py b/src/lfx/src/lfx/components/processing/message_to_data.py index dbdadabc35f4..53ae8a94e0d5 100644 --- a/src/lfx/src/lfx/components/processing/message_to_data.py +++ b/src/lfx/src/lfx/components/processing/message_to_data.py @@ -22,7 +22,7 @@ class MessageToDataComponent(Component): ] outputs = [ - Output(display_name="Data", name="data", method="convert_message_to_data"), + Output(display_name="JSON", name="data", method="convert_message_to_data"), ] def convert_message_to_data(self) -> Data: diff --git a/src/lfx/src/lfx/components/processing/parse_data.py b/src/lfx/src/lfx/components/processing/parse_data.py index dad78d38d335..7d196d838172 100644 --- a/src/lfx/src/lfx/components/processing/parse_data.py +++ b/src/lfx/src/lfx/components/processing/parse_data.py @@ -19,7 +19,7 @@ class ParseDataComponent(Component): inputs = [ DataInput( name="data", - display_name="Data", + display_name="JSON", info="The data to convert to text.", is_list=True, required=True, @@ -43,7 +43,7 @@ class ParseDataComponent(Component): method="parse_data", ), Output( - display_name="Data List", + display_name="JSON List", name="data_list", info="Data as a list of new Data, each having `text` formatted by Template", method="parse_data_as_list", diff --git a/src/lfx/src/lfx/components/processing/parse_dataframe.py b/src/lfx/src/lfx/components/processing/parse_dataframe.py index 1b1f3399b765..c4b02332dc73 100644 --- a/src/lfx/src/lfx/components/processing/parse_dataframe.py +++ b/src/lfx/src/lfx/components/processing/parse_dataframe.py @@ -15,7 +15,7 @@ class ParseDataFrameComponent(Component): replacement = ["processing.DataFrameOperations", "processing.TypeConverterComponent"] inputs = [ - DataFrameInput(name="df", display_name="DataFrame", info="The DataFrame to convert to text rows."), + DataFrameInput(name="df", display_name="Table", info="The DataFrame to convert to text rows."), MultilineInput( name="template", display_name="Template", diff --git a/src/lfx/src/lfx/components/processing/parse_json_data.py b/src/lfx/src/lfx/components/processing/parse_json_data.py index aa9c37a2334e..28aa03eafd02 100644 --- a/src/lfx/src/lfx/components/processing/parse_json_data.py +++ b/src/lfx/src/lfx/components/processing/parse_json_data.py @@ -26,7 +26,7 @@ class ParseJSONDataComponent(Component): display_name="Input", info="Data object to filter.", required=True, - input_types=["Message", "Data"], + input_types=["Message", "Data", "JSON"], ), MessageTextInput( name="query", diff --git a/src/lfx/src/lfx/components/processing/parser.py b/src/lfx/src/lfx/components/processing/parser.py index f3f124caba00..625065c2d610 100644 --- a/src/lfx/src/lfx/components/processing/parser.py +++ b/src/lfx/src/lfx/components/processing/parser.py @@ -16,8 +16,8 @@ class ParserComponent(Component): inputs = [ HandleInput( name="input_data", - display_name="Data or DataFrame", - input_types=["DataFrame", "Data"], + display_name="JSON or Table", + input_types=["DataFrame", "Table", "Data", "JSON"], info="Accepts either a DataFrame or a Data object.", required=True, ), diff --git a/src/lfx/src/lfx/components/processing/regex.py b/src/lfx/src/lfx/components/processing/regex.py index 77ea2975488b..12a52cd40bba 100644 --- a/src/lfx/src/lfx/components/processing/regex.py +++ b/src/lfx/src/lfx/components/processing/regex.py @@ -31,7 +31,7 @@ class RegexExtractorComponent(Component): ] outputs = [ - Output(display_name="Data", name="data", method="extract_matches"), + Output(display_name="JSON", name="data", method="extract_matches"), Output(display_name="Message", name="text", method="get_matches_text"), ] diff --git a/src/lfx/src/lfx/components/processing/select_data.py b/src/lfx/src/lfx/components/processing/select_data.py index f98b93d00ba0..f077859abe37 100644 --- a/src/lfx/src/lfx/components/processing/select_data.py +++ b/src/lfx/src/lfx/components/processing/select_data.py @@ -16,7 +16,7 @@ class SelectDataComponent(Component): inputs = [ DataInput( name="data_list", - display_name="Data List", + display_name="JSON List", info="List of data to select from.", is_list=True, # Specify that this input takes a list of Data objects ), diff --git a/src/lfx/src/lfx/components/processing/split_text.py b/src/lfx/src/lfx/components/processing/split_text.py index 5fe8718f4a4f..b501ff657c2c 100644 --- a/src/lfx/src/lfx/components/processing/split_text.py +++ b/src/lfx/src/lfx/components/processing/split_text.py @@ -20,7 +20,7 @@ class SplitTextComponent(Component): name="data_inputs", display_name="Input", info="The data with texts to split in chunks.", - input_types=["Data", "DataFrame", "Message"], + input_types=["Data", "JSON", "DataFrame", "Table", "Message"], required=True, ), IntInput( diff --git a/src/lfx/src/lfx/components/processing/text_operations.py b/src/lfx/src/lfx/components/processing/text_operations.py index cd925daaf3ac..bd830c8d267c 100644 --- a/src/lfx/src/lfx/components/processing/text_operations.py +++ b/src/lfx/src/lfx/components/processing/text_operations.py @@ -286,9 +286,9 @@ def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) operation_name = self._extract_operation_name(field_value) if operation_name == "Word Count": - frontend_node["outputs"].append(Output(display_name="Data", name="data", method="get_data")) + frontend_node["outputs"].append(Output(display_name="JSON", name="data", method="get_data")) elif operation_name == "Text to DataFrame": - frontend_node["outputs"].append(Output(display_name="DataFrame", name="dataframe", method="get_dataframe")) + frontend_node["outputs"].append(Output(display_name="Table", name="dataframe", method="get_dataframe")) elif operation_name == "Text Join": frontend_node["outputs"].append(Output(display_name="Text", name="text", method="get_text")) frontend_node["outputs"].append(Output(display_name="Message", name="message", method="get_message")) diff --git a/src/lfx/src/lfx/components/processing/update_data.py b/src/lfx/src/lfx/components/processing/update_data.py index 99d8bc9edf9c..18ffff2fd5a7 100644 --- a/src/lfx/src/lfx/components/processing/update_data.py +++ b/src/lfx/src/lfx/components/processing/update_data.py @@ -26,7 +26,7 @@ class UpdateDataComponent(Component): inputs = [ DataInput( name="old_data", - display_name="Data", + display_name="JSON", info="The record to update.", is_list=True, # Changed to True to handle list of Data objects required=True, @@ -54,7 +54,7 @@ class UpdateDataComponent(Component): ] outputs = [ - Output(display_name="Data", name="data", method="build_data"), + Output(display_name="JSON", name="data", method="build_data"), ] def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None): @@ -100,7 +100,7 @@ def update_build_config(self, build_config: dotdict, field_value: Any, field_nam display_name=f"Field {i}", name=key, info=f"Key for field {i}.", - input_types=["Message", "Data"], + input_types=["Message", "Data", "JSON"], ) build_config[field.name] = field.to_dict() diff --git a/src/lfx/src/lfx/components/prototypes/python_function.py b/src/lfx/src/lfx/components/prototypes/python_function.py index 82c3581e472d..892c63c2d3b7 100644 --- a/src/lfx/src/lfx/components/prototypes/python_function.py +++ b/src/lfx/src/lfx/components/prototypes/python_function.py @@ -32,7 +32,7 @@ class PythonFunctionComponent(Component): ), Output( name="function_output_data", - display_name="Function Output (Data)", + display_name="Function Output (JSON)", method="execute_function_data", ), Output( diff --git a/src/lfx/src/lfx/components/scrapegraph/scrapegraph_markdownify_api.py b/src/lfx/src/lfx/components/scrapegraph/scrapegraph_markdownify_api.py index dcdfce01af0b..9ed97f6ac9a7 100644 --- a/src/lfx/src/lfx/components/scrapegraph/scrapegraph_markdownify_api.py +++ b/src/lfx/src/lfx/components/scrapegraph/scrapegraph_markdownify_api.py @@ -32,7 +32,7 @@ class ScrapeGraphMarkdownifyApi(Component): ] outputs = [ - Output(display_name="Data", name="data", method="scrape"), + Output(display_name="JSON", name="data", method="scrape"), ] def scrape(self) -> list[Data]: diff --git a/src/lfx/src/lfx/components/scrapegraph/scrapegraph_search_api.py b/src/lfx/src/lfx/components/scrapegraph/scrapegraph_search_api.py index 19e29be58fb2..cc32ee7c2fd4 100644 --- a/src/lfx/src/lfx/components/scrapegraph/scrapegraph_search_api.py +++ b/src/lfx/src/lfx/components/scrapegraph/scrapegraph_search_api.py @@ -32,7 +32,7 @@ class ScrapeGraphSearchApi(Component): ] outputs = [ - Output(display_name="Data", name="data", method="search"), + Output(display_name="JSON", name="data", method="search"), ] def search(self) -> list[Data]: diff --git a/src/lfx/src/lfx/components/scrapegraph/scrapegraph_smart_scraper_api.py b/src/lfx/src/lfx/components/scrapegraph/scrapegraph_smart_scraper_api.py index fe3af9301c8c..380f878dc69e 100644 --- a/src/lfx/src/lfx/components/scrapegraph/scrapegraph_smart_scraper_api.py +++ b/src/lfx/src/lfx/components/scrapegraph/scrapegraph_smart_scraper_api.py @@ -38,7 +38,7 @@ class ScrapeGraphSmartScraperApi(Component): ] outputs = [ - Output(display_name="Data", name="data", method="scrape"), + Output(display_name="JSON", name="data", method="scrape"), ] def scrape(self) -> list[Data]: diff --git a/src/lfx/src/lfx/components/searchapi/search.py b/src/lfx/src/lfx/components/searchapi/search.py index 0ce529d2b8de..fb86cb66c0f0 100644 --- a/src/lfx/src/lfx/components/searchapi/search.py +++ b/src/lfx/src/lfx/components/searchapi/search.py @@ -29,7 +29,7 @@ class SearchComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), ] def _build_wrapper(self): diff --git a/src/lfx/src/lfx/components/serpapi/serp.py b/src/lfx/src/lfx/components/serpapi/serp.py index 7a940e4573e9..57e5625696f9 100644 --- a/src/lfx/src/lfx/components/serpapi/serp.py +++ b/src/lfx/src/lfx/components/serpapi/serp.py @@ -48,7 +48,7 @@ class SerpComponent(Component): ] outputs = [ - Output(display_name="Data", name="data", method="fetch_content"), + Output(display_name="JSON", name="data", method="fetch_content"), Output(display_name="Text", name="text", method="fetch_content_text"), ] diff --git a/src/lfx/src/lfx/components/tavily/tavily_extract.py b/src/lfx/src/lfx/components/tavily/tavily_extract.py index 10c5b9279883..2e71f34ab5b1 100644 --- a/src/lfx/src/lfx/components/tavily/tavily_extract.py +++ b/src/lfx/src/lfx/components/tavily/tavily_extract.py @@ -45,7 +45,7 @@ class TavilyExtractComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content"), + Output(display_name="Table", name="dataframe", method="fetch_content"), ] def run_model(self) -> DataFrame: diff --git a/src/lfx/src/lfx/components/tavily/tavily_search.py b/src/lfx/src/lfx/components/tavily/tavily_search.py index 3115324affc7..c0315a91fc9a 100644 --- a/src/lfx/src/lfx/components/tavily/tavily_search.py +++ b/src/lfx/src/lfx/components/tavily/tavily_search.py @@ -108,7 +108,7 @@ class TavilySearchComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), ] def fetch_content(self) -> list[Data]: diff --git a/src/lfx/src/lfx/components/tools/python_code_structured_tool.py b/src/lfx/src/lfx/components/tools/python_code_structured_tool.py index a3eba9f71fc4..566544bb4294 100644 --- a/src/lfx/src/lfx/components/tools/python_code_structured_tool.py +++ b/src/lfx/src/lfx/components/tools/python_code_structured_tool.py @@ -79,7 +79,7 @@ class PythonCodeStructuredTool(LCToolComponent): name="global_variables", display_name="Global Variables", info="Enter the global variables or Create Data Component.", - input_types=["Data"], + input_types=["Data", "JSON"], field_type=FieldTypes.DICT, is_list=True, ), diff --git a/src/lfx/src/lfx/components/twelvelabs/convert_astra_results.py b/src/lfx/src/lfx/components/twelvelabs/convert_astra_results.py index d4262a6f50bd..716feeae5cf8 100644 --- a/src/lfx/src/lfx/components/twelvelabs/convert_astra_results.py +++ b/src/lfx/src/lfx/components/twelvelabs/convert_astra_results.py @@ -19,7 +19,7 @@ class ConvertAstraToTwelveLabs(Component): HandleInput( name="astra_results", display_name="Astra DB Results", - input_types=["Data"], + input_types=["Data", "JSON"], info="Search results from Astra DB component", required=True, is_list=True, diff --git a/src/lfx/src/lfx/components/twelvelabs/pegasus_index.py b/src/lfx/src/lfx/components/twelvelabs/pegasus_index.py index 32347f9a82e8..e234d2c5fa2c 100644 --- a/src/lfx/src/lfx/components/twelvelabs/pegasus_index.py +++ b/src/lfx/src/lfx/components/twelvelabs/pegasus_index.py @@ -72,7 +72,7 @@ class PegasusIndexVideo(Component): outputs = [ Output( - display_name="Indexed Data", name="indexed_data", method="index_videos", output_types=["Data"], is_list=True + display_name="Indexed Data", name="indexed_data", method="index_videos", output_types=["JSON"], is_list=True ), ] diff --git a/src/lfx/src/lfx/components/twelvelabs/split_video.py b/src/lfx/src/lfx/components/twelvelabs/split_video.py index 849f41eceff6..5c2b80bdea31 100644 --- a/src/lfx/src/lfx/components/twelvelabs/split_video.py +++ b/src/lfx/src/lfx/components/twelvelabs/split_video.py @@ -33,7 +33,7 @@ class SplitVideoComponent(Component): display_name="Video Data", info="Input video data from VideoFile component", required=True, - input_types=["Data"], + input_types=["Data", "JSON"], ), IntInput( name="clip_duration", @@ -69,7 +69,7 @@ class SplitVideoComponent(Component): name="clips", display_name="Video Clips", method="process", - output_types=["Data"], + output_types=["JSON"], ), ] diff --git a/src/lfx/src/lfx/components/utilities/calculator_core.py b/src/lfx/src/lfx/components/utilities/calculator_core.py index 777358f29dfe..7f31d8b34f8c 100644 --- a/src/lfx/src/lfx/components/utilities/calculator_core.py +++ b/src/lfx/src/lfx/components/utilities/calculator_core.py @@ -33,7 +33,7 @@ class CalculatorComponent(Component): ] outputs = [ - Output(display_name="Data", name="result", type_=Data, method="evaluate_expression"), + Output(display_name="JSON", name="result", type_=Data, method="evaluate_expression"), ] def _eval_expr(self, node: ast.AST) -> float: diff --git a/src/lfx/src/lfx/components/vectorstores/local_db.py b/src/lfx/src/lfx/components/vectorstores/local_db.py index 71f35cdeebd8..0fa8dc9f801d 100644 --- a/src/lfx/src/lfx/components/vectorstores/local_db.py +++ b/src/lfx/src/lfx/components/vectorstores/local_db.py @@ -81,7 +81,7 @@ class LocalDBComponent(LCVectorStoreComponent): HandleInput( name="ingest_data", display_name="Ingest Data", - input_types=["Data", "DataFrame"], + input_types=["Data", "JSON", "DataFrame", "Table"], is_list=True, info="Data to store. It will be embedded and indexed for semantic search.", show=True, @@ -108,7 +108,7 @@ class LocalDBComponent(LCVectorStoreComponent): ), ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="perform_search"), + Output(display_name="Table", name="dataframe", method="perform_search"), ] def get_vector_store_directory(self, base_dir: str | Path) -> Path: diff --git a/src/lfx/src/lfx/components/wikipedia/wikidata.py b/src/lfx/src/lfx/components/wikipedia/wikidata.py index 937bbcceb885..9285173574c9 100644 --- a/src/lfx/src/lfx/components/wikipedia/wikidata.py +++ b/src/lfx/src/lfx/components/wikipedia/wikidata.py @@ -25,7 +25,7 @@ class WikidataComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), ] def run_model(self) -> DataFrame: diff --git a/src/lfx/src/lfx/components/wikipedia/wikipedia.py b/src/lfx/src/lfx/components/wikipedia/wikipedia.py index d105503b758b..d540bcf9c1a9 100644 --- a/src/lfx/src/lfx/components/wikipedia/wikipedia.py +++ b/src/lfx/src/lfx/components/wikipedia/wikipedia.py @@ -27,7 +27,7 @@ class WikipediaComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), ] def run_model(self) -> DataFrame: diff --git a/src/lfx/src/lfx/components/wolframalpha/wolfram_alpha_api.py b/src/lfx/src/lfx/components/wolframalpha/wolfram_alpha_api.py index 03908857ac6f..f917fe7923aa 100644 --- a/src/lfx/src/lfx/components/wolframalpha/wolfram_alpha_api.py +++ b/src/lfx/src/lfx/components/wolframalpha/wolfram_alpha_api.py @@ -15,7 +15,7 @@ class WolframAlphaAPIComponent(LCToolComponent): name = "WolframAlphaAPI" outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), ] inputs = [ diff --git a/src/lfx/src/lfx/components/yahoosearch/yahoo.py b/src/lfx/src/lfx/components/yahoosearch/yahoo.py index 3567c3397f9f..76802d17ee9b 100644 --- a/src/lfx/src/lfx/components/yahoosearch/yahoo.py +++ b/src/lfx/src/lfx/components/yahoosearch/yahoo.py @@ -77,7 +77,7 @@ class YfinanceComponent(Component): ] outputs = [ - Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"), + Output(display_name="Table", name="dataframe", method="fetch_content_dataframe"), ] def run_model(self) -> DataFrame: diff --git a/src/lfx/src/lfx/field_typing/constants.py b/src/lfx/src/lfx/field_typing/constants.py index 1d0ad3796606..b337aae78efb 100644 --- a/src/lfx/src/lfx/field_typing/constants.py +++ b/src/lfx/src/lfx/field_typing/constants.py @@ -96,7 +96,8 @@ class TextSplitter: # Import lfx schema types (avoid circular deps) -from lfx.schema.data import Data +from lfx.schema.data import JSON, Data +from lfx.schema.dataframe import DataFrame, Table # Type aliases NestedDict: TypeAlias = dict[str, str | dict] @@ -154,6 +155,9 @@ class Code: **LANGCHAIN_BASE_TYPES, "NestedDict": NestedDict, "Data": Data, + "JSON": JSON, + "DataFrame": DataFrame, + "Table": Table, "Text": Text, # noqa: UP019 "Object": Object, "Callable": Callable, @@ -194,6 +198,7 @@ class Code: FloatInput, HandleInput, IntInput, + JSONInput, LinkInput, MessageInput, MessageTextInput, @@ -208,8 +213,8 @@ class Code: StrInput, TableInput, ) -from lfx.schema.data import Data -from lfx.schema.dataframe import DataFrame +from lfx.schema.data import JSON, Data +from lfx.schema.dataframe import DataFrame, Table """ if importlib.util.find_spec("langchain") is not None: diff --git a/src/lfx/src/lfx/graph/edge/base.py b/src/lfx/src/lfx/graph/edge/base.py index a053148c90e2..873740f3f524 100644 --- a/src/lfx/src/lfx/graph/edge/base.py +++ b/src/lfx/src/lfx/graph/edge/base.py @@ -9,6 +9,37 @@ if TYPE_CHECKING: from lfx.graph.vertex.base import Vertex +# Type migrations for backward compatibility +# Maps old type names to new type names +TYPE_MIGRATIONS: dict[str, str] = { + "Data": "JSON", + "DataFrame": "Table", +} + + +def types_compatible(output_types: list[str], input_types: list[str]) -> bool: + """Check if output types are compatible with input types, considering type migrations. + + Args: + output_types: List of output type names from source handle + input_types: List of input type names from target handle + + Returns: + True if any output type matches any input type (directly or via migration) + """ + for output_type in output_types: + # Get the migrated version of the output type + migrated_output = TYPE_MIGRATIONS.get(output_type, output_type) + + for input_type in input_types: + # Get the migrated version of the input type + migrated_input = TYPE_MIGRATIONS.get(input_type, input_type) + + # Check all possible combinations using sets for cleaner comparison + if input_type in {output_type, migrated_output} or migrated_input in {output_type, migrated_output}: + return True + return False + class Edge: def __init__(self, source: Vertex, target: Vertex, edge: EdgeData): @@ -82,7 +113,10 @@ def validate_handles(self, source, target) -> None: def _validate_handles(self, source, target) -> None: if self.target_handle.input_types is None: - self.valid_handles = self.target_handle.type in self.source_handle.output_types + # Backward compatibility: old flows may have Data/DataFrame types + self.valid_handles = types_compatible( + self.source_handle.output_types, [self.target_handle.type] if self.target_handle.type else [] + ) elif self.target_handle.type is None: # ! This is not a good solution # This is a loop edge @@ -92,13 +126,15 @@ def _validate_handles(self, source, target) -> None: # is in the target_handle.input_types self.valid_handles = bool(self.source_handle.output_types) and ( not self.target_handle.input_types - or any(output_type in self.target_handle.input_types for output_type in self.source_handle.output_types) + or types_compatible(self.source_handle.output_types, self.target_handle.input_types) ) elif self.source_handle.output_types is not None: - self.valid_handles = ( - any(output_type in self.target_handle.input_types for output_type in self.source_handle.output_types) - or self.target_handle.type in self.source_handle.output_types + # Backward compatibility: old flows may have Data/DataFrame types + self.valid_handles = types_compatible( + self.source_handle.output_types, self.target_handle.input_types + ) or types_compatible( + self.source_handle.output_types, [self.target_handle.type] if self.target_handle.type else [] ) if not self.valid_handles: @@ -109,11 +145,16 @@ def _validate_handles(self, source, target) -> None: def _legacy_validate_handles(self, source, target) -> None: if self.target_handle.input_types is None: - self.valid_handles = self.target_handle.type in self.source_handle.base_classes + # Backward compatibility: old flows may have Data/DataFrame types + self.valid_handles = types_compatible( + self.source_handle.base_classes, [self.target_handle.type] if self.target_handle.type else [] + ) else: - self.valid_handles = ( - any(baseClass in self.target_handle.input_types for baseClass in self.source_handle.base_classes) - or self.target_handle.type in self.source_handle.base_classes + # Backward compatibility: old flows may have Data/DataFrame types + self.valid_handles = types_compatible( + self.source_handle.base_classes, self.target_handle.input_types + ) or types_compatible( + self.source_handle.base_classes, [self.target_handle.type] if self.target_handle.type else [] ) if not self.valid_handles: logger.debug(self.source_handle) @@ -158,16 +199,15 @@ def _validate_edge(self, source, target) -> None: # For loop inputs, use the configured input_types # (which already includes original type + loop_types from frontend) loop_input_types = list(self.target_handle.input_types) - self.valid = any( - any(output_type in loop_input_types for output_type in output["types"]) for output in self.source_types - ) - # Find the first matching type + # Backward compatibility: old flows may have Data/DataFrame types + self.valid = any(types_compatible(output["types"], loop_input_types) for output in self.source_types) + # Find the first matching type (considering migrations) self.matched_type = next( ( output_type for output in self.source_types for output_type in output["types"] - if output_type in loop_input_types + if types_compatible([output_type], loop_input_types) ), None, ) @@ -177,19 +217,15 @@ def _validate_edge(self, source, target) -> None: # Both lists contain strings and sometimes a string contains the value we are # looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"] # so we need to check if any of the strings in source_types is in target_reqs - self.valid = any( - any(output_type in target_req for output_type in output["types"]) - for output in self.source_types - for target_req in self.target_reqs - ) - # Update the matched type to be the first found match + # Backward compatibility: old flows may have Data/DataFrame types + self.valid = any(types_compatible(output["types"], self.target_reqs) for output in self.source_types) + # Update the matched type to be the first found match (considering migrations) self.matched_type = next( ( output_type for output in self.source_types for output_type in output["types"] - for target_req in self.target_reqs - if output_type in target_req + if types_compatible([output_type], self.target_reqs) ), None, ) @@ -209,11 +245,12 @@ def _legacy_validate_edge(self, source, target) -> None: # Both lists contain strings and sometimes a string contains the value we are # looking for e.g. comgin_out=["Chain"] and target_reqs=["LLMChain"] # so we need to check if any of the strings in source_types is in target_reqs - self.valid = any(output in target_req for output in self.source_types for target_req in self.target_reqs) - # Get what type of input the target node is expecting + # Use types_compatible for migration support + self.valid = types_compatible(self.source_types, self.target_reqs) + # Get what type of input the target node is expecting (considering migrations) self.matched_type = next( - (output for output in self.source_types if output in self.target_reqs), + (output for output in self.source_types if types_compatible([output], self.target_reqs)), None, ) no_matched_type = self.matched_type is None diff --git a/src/lfx/src/lfx/inputs/__init__.py b/src/lfx/src/lfx/inputs/__init__.py index a85043325844..bb5ca341751f 100644 --- a/src/lfx/src/lfx/inputs/__init__.py +++ b/src/lfx/src/lfx/inputs/__init__.py @@ -13,6 +13,7 @@ HandleInput, Input, IntInput, + JSONInput, LinkInput, McpInput, MessageInput, @@ -49,6 +50,7 @@ "HandleInput", "Input", "IntInput", + "JSONInput", "LinkInput", "McpInput", "MessageInput", diff --git a/src/lfx/src/lfx/inputs/inputs.py b/src/lfx/src/lfx/inputs/inputs.py index 9db3c31e0a9f..8e3392778f27 100644 --- a/src/lfx/src/lfx/inputs/inputs.py +++ b/src/lfx/src/lfx/inputs/inputs.py @@ -38,6 +38,7 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMixin, ToolModeMixin): field_type: SerializableFieldTypes = FieldTypes.TABLE is_list: bool = True + input_types: list[str] = ["DataFrame", "Table"] @field_validator("value") @classmethod @@ -102,18 +103,34 @@ class ToolsInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, ToolMod real_time_refresh: bool = True -class DataInput(HandleInput, InputTraceMixin, ListableInputMixin, ToolModeMixin): - """Represents an Input that has a Handle that receives a Data object. +class JSONInput(HandleInput, InputTraceMixin, ListableInputMixin, ToolModeMixin): + """Represents an Input that has a Handle that receives a JSON object. + + This is the new standard input for Langflow data structures. + DataInput is maintained as an alias for backwards compatibility. Attributes: - input_types (list[str]): A list of input types supported by this data input. + input_types (list[str]): A list of input types supported by this JSON input. """ - input_types: list[str] = ["Data"] + input_types: list[str] = ["Data", "JSON"] + + +# DataInput is maintained for backwards compatibility - it is now an alias to JSONInput +DataInput = JSONInput class DataFrameInput(HandleInput, InputTraceMixin, ListableInputMixin, ToolModeMixin): - input_types: list[str] = ["DataFrame"] + """Represents an Input that has a Handle that receives a Table (DataFrame) object. + + Note: This accepts DataFrame and Table types. For visual table inputs in the UI, + use TableInput instead (which has field_type: FieldTypes.TABLE). + + Attributes: + input_types (list[str]): A list of input types supported by this input. + """ + + input_types: list[str] = ["DataFrame", "Table"] class PromptInput(BaseInputMixin, ListableInputMixin, InputTraceMixin, ToolModeMixin): @@ -848,6 +865,7 @@ class DefaultPromptField(Input): | QueryInput | DefaultPromptField | BoolInput + | JSONInput | DataInput | DictInput | DropdownInput @@ -879,6 +897,9 @@ class DefaultPromptField(Input): ) InputTypesMap: dict[str, type[InputTypes]] = {t.__name__: t for t in get_args(InputTypes)} +# DataInput is an alias for JSONInput, so its __name__ is "JSONInput". +# Add explicit entry so serialized configs using "DataInput" still deserialize correctly. +InputTypesMap["DataInput"] = JSONInput def instantiate_input(input_type: str, data: dict) -> InputTypes: diff --git a/src/lfx/src/lfx/io/__init__.py b/src/lfx/src/lfx/io/__init__.py index 8b35dae60f4b..fafbdce3ba40 100644 --- a/src/lfx/src/lfx/io/__init__.py +++ b/src/lfx/src/lfx/io/__init__.py @@ -10,6 +10,7 @@ FloatInput, HandleInput, IntInput, + JSONInput, LinkInput, McpInput, MessageInput, @@ -43,6 +44,7 @@ "FloatInput", "HandleInput", "IntInput", + "JSONInput", "LinkInput", "LinkInput", "McpInput", diff --git a/src/lfx/src/lfx/schema/data.py b/src/lfx/src/lfx/schema/data.py index b413dbf8cd3c..66099fc1d570 100644 --- a/src/lfx/src/lfx/schema/data.py +++ b/src/lfx/src/lfx/schema/data.py @@ -1,4 +1,8 @@ -"""Lightweight Data class for lfx package - contains only methods with no langflow dependencies.""" +"""Lightweight JSON class for lfx package - contains only methods with no langflow dependencies. + +This module provides the JSON class (formerly Data) as the base type for Langflow data structures. +Data is maintained as an alias for backwards compatibility. +""" from __future__ import annotations @@ -19,13 +23,37 @@ from lfx.utils.image import create_image_content_dict if TYPE_CHECKING: - from lfx.schema.dataframe import DataFrame + from lfx.schema.dataframe import Table from lfx.schema.message import Message -class Data(CrossModuleModel): +def custom_serializer(obj): + if isinstance(obj, datetime): + utc_date = obj.replace(tzinfo=timezone.utc) + return utc_date.strftime("%Y-%m-%d %H:%M:%S %Z") + if isinstance(obj, Decimal): + return float(obj) + if isinstance(obj, UUID): + return str(obj) + if isinstance(obj, BaseModel): + return obj.model_dump() + if isinstance(obj, bytes): + return obj.decode("utf-8", errors="replace") + # Add more custom serialization rules as needed + msg = f"Type {type(obj)} not serializable" + raise TypeError(msg) + + +def serialize_data(data): + return json.dumps(data, indent=4, default=custom_serializer) + + +class JSON(CrossModuleModel): """Represents a record with text and optional data. + This is the base type for Langflow data structures, replacing the legacy Data class. + Data is maintained as an alias for backwards compatibility. + Attributes: data (dict, optional): Additional data associated with the record. """ @@ -90,39 +118,39 @@ def set_text(self, text: str | None) -> str: return new_text @classmethod - def from_document(cls, document: Document) -> Data: - """Converts a Document to a Data. + def from_document(cls, document: Document) -> JSON: + """Converts a Document to a JSON. Args: document (Document): The Document to convert. Returns: - Data: The converted Data. + JSON: The converted JSON. """ data = document.metadata data["text"] = document.page_content return cls(data=data, text_key="text") @classmethod - def from_lc_message(cls, message: BaseMessage) -> Data: - """Converts a BaseMessage to a Data. + def from_lc_message(cls, message: BaseMessage) -> JSON: + """Converts a BaseMessage to a JSON. Args: message (BaseMessage): The BaseMessage to convert. Returns: - Data: The converted Data. + JSON: The converted JSON. """ data: dict = {"text": message.content} data["metadata"] = cast("dict", message.to_json()) return cls(data=data, text_key="text") - def __add__(self, other: Data) -> Data: - """Combines the data of two data by attempting to add values for overlapping keys. + def __add__(self, other: JSON) -> JSON: + """Combines the data of two JSON objects by attempting to add values for overlapping keys. - Combines the data of two data by attempting to add values for overlapping keys + Combines the data of two JSON objects by attempting to add values for overlapping keys for all types that support the addition operation. Falls back to the value from 'other' - record when addition is not supported. + when addition is not supported. """ combined_data = self.data.copy() for key, value in other.data.items(): @@ -131,13 +159,13 @@ def __add__(self, other: Data) -> Data: try: combined_data[key] += value except TypeError: - # Fallback: Use the value from 'other' record if addition is not supported + # Fallback: Use the value from 'other' if addition is not supported combined_data[key] = value else: - # If the key is not in the first record, simply add it + # If the key is not in the first object, simply add it combined_data[key] = value - return Data(data=combined_data) + return JSON(data=combined_data) def to_lc_document(self) -> Document: """Converts the Data to a Document. @@ -154,18 +182,18 @@ def to_lc_document(self) -> Document: def to_lc_message( self, ) -> BaseMessage: - """Converts the Data to a BaseMessage. + """Converts the JSON to a BaseMessage. Returns: BaseMessage: The converted BaseMessage. """ - # The idea of this function is to be a helper to convert a Data to a BaseMessage + # The idea of this function is to be a helper to convert a JSON to a BaseMessage # It will use the "sender" key to determine if the message is Human or AI # If the key is not present, it will default to AI # But first we check if all required keys are present in the data dictionary # they are: "text", "sender" if not all(key in self.data for key in ["text", "sender"]): - msg = f"Missing required keys ('text', 'sender') in Data: {self.data}" + msg = f"Missing required keys ('text', 'sender') in JSON: {self.data}" raise ValueError(msg) sender = self.data.get("sender", MESSAGE_SENDER_AI) text = self.data.get("text", "") @@ -223,37 +251,37 @@ def __delattr__(self, key) -> None: del self.data[key] def __deepcopy__(self, memo): - """Custom deepcopy implementation to handle copying of the Data object.""" - # Create a new Data object with a deep copy of the data dictionary - return Data(data=copy.deepcopy(self.data, memo), text_key=self.text_key, default_value=self.default_value) + """Custom deepcopy implementation to handle copying of the JSON object.""" + # Create a new JSON object with a deep copy of the data dictionary + return JSON(data=copy.deepcopy(self.data, memo), text_key=self.text_key, default_value=self.default_value) - # check which attributes the Data has by checking the keys in the data dictionary + # check which attributes the JSON has by checking the keys in the data dictionary def __dir__(self): return super().__dir__() + list(self.data.keys()) def __str__(self) -> str: - # return a JSON string representation of the Data atributes + # return a JSON string representation of the JSON attributes try: data = {k: v.to_json() if hasattr(v, "to_json") else v for k, v in self.data.items()} return serialize_data(data) # use the custom serializer except Exception: # noqa: BLE001 - logger.debug("Error converting Data to JSON", exc_info=True) + logger.debug("Error converting JSON to string", exc_info=True) return str(self.data) def __contains__(self, key) -> bool: return key in self.data def __eq__(self, /, other): - return isinstance(other, Data) and self.data == other.data + return isinstance(other, JSON) and self.data == other.data - def filter_data(self, filter_str: str) -> Data: + def filter_data(self, filter_str: str) -> JSON: """Filters the data dictionary based on the filter string. Args: filter_str (str): The filter string to apply to the data dictionary. Returns: - Data: The filtered Data. + JSON: The filtered JSON. """ from lfx.template.utils import apply_json_filter @@ -266,7 +294,7 @@ def to_message(self) -> Message: return Message(text=self.get_text()) return Message(text=str(self.data)) - def to_dataframe(self) -> DataFrame: + def to_dataframe(self) -> Table: from lfx.schema.dataframe import DataFrame # Local import to avoid circular import data_dict = self.data @@ -280,30 +308,14 @@ def to_dataframe(self) -> DataFrame: return DataFrame(data=[self]) def __repr__(self) -> str: - """Return string representation of the Data object.""" - return f"Data(text_key={self.text_key!r}, data={self.data!r}, default_value={self.default_value!r})" + """Return string representation of the JSON object.""" + return f"JSON(text_key={self.text_key!r}, data={self.data!r}, default_value={self.default_value!r})" def __hash__(self) -> int: - """Return hash of the Data object based on its string representation.""" + """Return hash of the JSON object based on its string representation.""" return hash(self.__repr__()) -def custom_serializer(obj): - if isinstance(obj, datetime): - utc_date = obj.replace(tzinfo=timezone.utc) - return utc_date.strftime("%Y-%m-%d %H:%M:%S %Z") - if isinstance(obj, Decimal): - return float(obj) - if isinstance(obj, UUID): - return str(obj) - if isinstance(obj, BaseModel): - return obj.model_dump() - if isinstance(obj, bytes): - return obj.decode("utf-8", errors="replace") - # Add more custom serialization rules as needed - msg = f"Type {type(obj)} not serializable" - raise TypeError(msg) - - -def serialize_data(data): - return json.dumps(data, indent=4, default=custom_serializer) +# Data class is maintained for backwards compatibility - it is now an alias to JSON +# All new code should use JSON instead of Data +Data = JSON diff --git a/src/lfx/src/lfx/schema/dataframe.py b/src/lfx/src/lfx/schema/dataframe.py index b5b3bad72111..46b2a1a9500e 100644 --- a/src/lfx/src/lfx/schema/dataframe.py +++ b/src/lfx/src/lfx/schema/dataframe.py @@ -1,3 +1,9 @@ +"""Table class for lfx package - pandas DataFrame subclass for Langflow data structures. + +This module provides the Table class (formerly DataFrame) as the base type for tabular data in Langflow. +DataFrame is maintained as an alias for backwards compatibility. +""" + from typing import TYPE_CHECKING, cast import pandas as pd @@ -10,15 +16,18 @@ from lfx.schema.message import Message -class DataFrame(pandas_DataFrame): - """A pandas DataFrame subclass specialized for handling collections of Data objects. +class Table(pandas_DataFrame): + """A pandas DataFrame subclass specialized for handling collections of JSON objects. + + This is the base type for Langflow tabular data structures, replacing the legacy DataFrame class. + DataFrame is maintained as an alias for backwards compatibility. This class extends pandas.DataFrame to provide seamless integration between - Langflow's Data objects and pandas' powerful data manipulation capabilities. + Langflow's JSON objects and pandas' powerful data manipulation capabilities. Args: data: Input data in various formats: - - List[Data]: List of Data objects + - List[Data]: List of Data/JSON objects - List[Dict]: List of dictionaries - Dict: Dictionary of arrays/lists - pandas.DataFrame: Existing DataFrame @@ -27,13 +36,13 @@ class DataFrame(pandas_DataFrame): Examples: >>> # From Data objects - >>> dataset = DataFrame([Data(data={"name": "John"}), Data(data={"name": "Jane"})]) + >>> dataset = Table([Data(data={"name": "John"}), Data(data={"name": "Jane"})]) >>> # From dictionaries - >>> dataset = DataFrame([{"name": "John"}, {"name": "Jane"}]) + >>> dataset = Table([{"name": "John"}, {"name": "Jane"}]) >>> # From dictionary of lists - >>> dataset = DataFrame({"name": ["John", "Jane"], "age": [30, 25]}) + >>> dataset = Table({"name": ["John", "Jane"], "age": [30, 25]}) """ def __init__( @@ -64,7 +73,7 @@ def __init__( self._update(data, **kwargs) def _update(self, data, **kwargs): - """Helper method to update DataFrame with new data.""" + """Helper method to update Table with new data.""" new_df = pd.DataFrame(data, **kwargs) self._update_inplace(new_df) @@ -76,7 +85,7 @@ def text_key(self) -> str: @text_key.setter def text_key(self, value: str) -> None: if value not in self.columns: - msg = f"Text key '{value}' not found in DataFrame columns" + msg = f"Text key '{value}' not found in Table columns" raise ValueError(msg) self._text_key = value @@ -89,37 +98,37 @@ def default_value(self, value: str) -> None: self._default_value = value def to_data_list(self) -> list[Data]: - """Converts the DataFrame back to a list of Data objects.""" + """Converts the Table back to a list of Data objects.""" list_of_dicts = self.to_dict(orient="records") # suggested change: [Data(**row) for row in list_of_dicts] return [Data(data=row) for row in list_of_dicts] - def add_row(self, data: dict | Data) -> "DataFrame": + def add_row(self, data: dict | Data) -> "Table": """Adds a single row to the dataset. Args: data: Either a Data object or a dictionary to add as a new row Returns: - DataFrame: A new DataFrame with the added row + Table: A new Table with the added row Example: - >>> dataset = DataFrame([{"name": "John"}]) + >>> dataset = Table([{"name": "John"}]) >>> dataset = dataset.add_row({"name": "Jane"}) """ if isinstance(data, Data): data = data.data new_df = self._constructor([data]) - return cast("DataFrame", pd.concat([self, new_df], ignore_index=True)) + return cast("Table", pd.concat([self, new_df], ignore_index=True)) - def add_rows(self, data: list[dict | Data]) -> "DataFrame": + def add_rows(self, data: list[dict | Data]) -> "Table": """Adds multiple rows to the dataset. Args: data: List of Data objects or dictionaries to add as new rows Returns: - DataFrame: A new DataFrame with the added rows + Table: A new Table with the added rows """ processed_data = [] for item in data: @@ -128,23 +137,23 @@ def add_rows(self, data: list[dict | Data]) -> "DataFrame": else: processed_data.append(item) new_df = self._constructor(processed_data) - return cast("DataFrame", pd.concat([self, new_df], ignore_index=True)) + return cast("Table", pd.concat([self, new_df], ignore_index=True)) @property def _constructor(self): def _c(*args, **kwargs): - return DataFrame(*args, **kwargs).__finalize__(self) + return Table(*args, **kwargs).__finalize__(self) return _c def __bool__(self): - """Truth value testing for the DataFrame. + """Truth value testing for the Table. - Returns True if the DataFrame has at least one row, False otherwise. + Returns True if the Table has at least one row, False otherwise. """ return not self.empty - __hash__ = None # DataFrames are mutable and shouldn't be hashable + __hash__ = None # Tables are mutable and shouldn't be hashable _CONTENT_COLUMNS = frozenset( { @@ -184,7 +193,7 @@ def smart_column_order(self) -> "DataFrame": return self[new_order] def to_lc_documents(self) -> list[Document]: - """Converts the DataFrame to a list of Documents. + """Converts the Table to a list of Documents. Returns: list[Document]: The converted list of Documents. @@ -201,31 +210,31 @@ def to_lc_documents(self) -> list[Document]: return documents def _docs_to_dataframe(self, docs): - """Converts a list of Documents to a DataFrame. + """Converts a list of Documents to a Table. Args: docs: List of Document objects Returns: - DataFrame: A new DataFrame with the converted Documents + Table: A new Table with the converted Documents """ - return DataFrame(docs) + return Table(docs) def __eq__(self, other): - """Override equality to handle comparison with empty DataFrames and non-DataFrame objects.""" + """Override equality to handle comparison with empty Tables and non-Table objects.""" if self.empty: return False if isinstance(other, list) and not other: # Empty list case return False - if not isinstance(other, DataFrame | pd.DataFrame): # Non-DataFrame case + if not isinstance(other, Table | pd.DataFrame): # Non-Table case return False return super().__eq__(other) def to_data(self) -> Data: - """Convert this DataFrame to a Data object. + """Convert this Table to a Data object. Returns: - Data: A Data object containing the DataFrame records under 'results' key. + Data: A Data object containing the Table records under 'results' key. """ dict_list = self.to_dict(orient="records") return Data(data={"results": dict_list}) @@ -233,7 +242,7 @@ def to_data(self) -> Data: def to_message(self) -> "Message": from lfx.schema.message import Message - # Process DataFrame similar to the _safe_convert method + # Process Table similar to the _safe_convert method # Remove empty rows processed_df = self.dropna(how="all") # Remove empty lines in each cell @@ -245,3 +254,8 @@ def to_message(self) -> "Message": processed_df = processed_df.map(lambda x: str(x).replace("\n", "
") if isinstance(x, str) else x) # Convert to markdown and wrap in a Message return Message(text=processed_df.to_markdown(index=False)) + + +# DataFrame class is maintained for backwards compatibility - it is now an alias to Table +# All new code should use Table instead of DataFrame +DataFrame = Table diff --git a/src/lfx/tests/unit/components/test_component_display_names.py b/src/lfx/tests/unit/components/test_component_display_names.py new file mode 100644 index 000000000000..9194e41fd2c1 --- /dev/null +++ b/src/lfx/tests/unit/components/test_component_display_names.py @@ -0,0 +1,230 @@ +"""Tests that ALL component outputs and inputs use the new display names. + +Validates the Data->JSON and DataFrame->Table rename migration is complete +across the entire component library. No component should still use the old +display names "Data", "DataFrame", or "Data or DataFrame". +""" + +from __future__ import annotations + +import importlib +import inspect +import os +from pathlib import Path + +import lfx.components + +# --- Named constants --- + +DEPRECATED_OUTPUT_DISPLAY_NAME_DATA = "Data" +DEPRECATED_OUTPUT_DISPLAY_NAME_DATAFRAME = "DataFrame" +DEPRECATED_DISPLAY_NAME_DATA_OR_DATAFRAME = "Data or DataFrame" + +EXPECTED_DATA_REPLACEMENT = "JSON" +EXPECTED_DATAFRAME_REPLACEMENT = "Table" + +MINIMUM_EXPECTED_COMPONENTS = 20 + + +def _discover_all_component_classes() -> list[tuple[str, type]]: + """Discover all component classes from the lfx.components package. + + Finds all .py files under lfx/components, imports them, and collects + all classes that inherit from Component. + + Returns: + List of (fully_qualified_name, class) tuples. + """ + from lfx.custom.custom_component.component import Component + + discovered: list[tuple[str, type]] = [] + components_pkg_path = Path(lfx.components.__path__[0]) + components_pkg_name = lfx.components.__name__ + + # Walk all .py files to discover component modules without relying + # on pkgutil.walk_packages, which may trigger __init__.py __getattr__ + # and fail on components with missing optional dependencies. + for root, _dirs, files in os.walk(components_pkg_path): + for filename in files: + if not filename.endswith(".py") or filename.startswith("_"): + continue + + filepath = Path(root) / filename + # Convert filesystem path to module path + relative = filepath.relative_to(components_pkg_path) + parts = list(relative.with_suffix("").parts) + module_name = f"{components_pkg_name}.{'.'.join(parts)}" + + try: + module = importlib.import_module(module_name) + except Exception: # noqa: S112 - intentionally skip modules with missing optional deps + continue + + for attr_name, attr_value in inspect.getmembers(module, inspect.isclass): + if ( + issubclass(attr_value, Component) + and attr_value is not Component + and attr_value.__module__ == module_name + ): + qualified_name = f"{module_name}.{attr_name}" + discovered.append((qualified_name, attr_value)) + + return discovered + + +def _get_output_display_names(component_class: type) -> list[tuple[str, str]]: + """Extract (output_name, display_name) pairs from a component class's outputs. + + Returns: + List of (output_name, display_name) tuples. + """ + outputs = getattr(component_class, "outputs", None) + if not outputs: + return [] + + results = [] + for output in outputs: + display_name = getattr(output, "display_name", None) + name = getattr(output, "name", "unknown") + if display_name is not None: + results.append((name, display_name)) + return results + + +def _get_input_display_names(component_class: type) -> list[tuple[str, str]]: + """Extract (input_name, display_name) pairs from a component class's inputs. + + Returns: + List of (input_name, display_name) tuples. + """ + inputs = getattr(component_class, "inputs", None) + if not inputs: + return [] + + results = [] + for inp in inputs: + display_name = getattr(inp, "display_name", None) + name = getattr(inp, "name", "unknown") + if display_name is not None: + results.append((name, display_name)) + return results + + +# Discover all components once at module level for reuse across tests +_ALL_COMPONENTS = _discover_all_component_classes() + + +class TestOutputDisplayNames: + """Validate that no component output uses deprecated display names.""" + + def test_should_not_have_data_as_output_display_name(self): + """No component output should have display_name='Data'. + + The correct display name after migration is 'JSON'. + """ + violations: list[str] = [] + + for qualified_name, component_class in _ALL_COMPONENTS: + for output_name, display_name in _get_output_display_names(component_class): + if display_name == DEPRECATED_OUTPUT_DISPLAY_NAME_DATA: + violations.append( + f"{qualified_name} output '{output_name}' has " + f"display_name='{DEPRECATED_OUTPUT_DISPLAY_NAME_DATA}' " + f"(should be '{EXPECTED_DATA_REPLACEMENT}')" + ) + + assert not violations, ( + f"Found {len(violations)} output(s) still using deprecated " + f"display_name='{DEPRECATED_OUTPUT_DISPLAY_NAME_DATA}':\n" + "\n".join(f" - {v}" for v in violations) + ) + + def test_should_not_have_dataframe_as_output_display_name(self): + """No component output should have display_name='DataFrame'. + + The correct display name after migration is 'Table'. + """ + violations: list[str] = [] + + for qualified_name, component_class in _ALL_COMPONENTS: + for output_name, display_name in _get_output_display_names(component_class): + if display_name == DEPRECATED_OUTPUT_DISPLAY_NAME_DATAFRAME: + violations.append( + f"{qualified_name} output '{output_name}' has " + f"display_name='{DEPRECATED_OUTPUT_DISPLAY_NAME_DATAFRAME}' " + f"(should be '{EXPECTED_DATAFRAME_REPLACEMENT}')" + ) + + assert not violations, ( + f"Found {len(violations)} output(s) still using deprecated " + f"display_name='{DEPRECATED_OUTPUT_DISPLAY_NAME_DATAFRAME}':\n" + "\n".join(f" - {v}" for v in violations) + ) + + def test_should_not_have_dataframe_as_input_display_name(self): + """No component input should have display_name='DataFrame'. + + The correct display name after migration is 'Table'. + """ + violations: list[str] = [] + + for qualified_name, component_class in _ALL_COMPONENTS: + for input_name, display_name in _get_input_display_names(component_class): + if display_name == DEPRECATED_OUTPUT_DISPLAY_NAME_DATAFRAME: + violations.append( + f"{qualified_name} input '{input_name}' has " + f"display_name='{DEPRECATED_OUTPUT_DISPLAY_NAME_DATAFRAME}' " + f"(should be '{EXPECTED_DATAFRAME_REPLACEMENT}')" + ) + + assert not violations, ( + f"Found {len(violations)} input(s) still using deprecated " + f"display_name='{DEPRECATED_OUTPUT_DISPLAY_NAME_DATAFRAME}':\n" + "\n".join(f" - {v}" for v in violations) + ) + + def test_should_not_have_data_or_dataframe_as_display_name(self): + """No component output or input should have display_name='Data or DataFrame'.""" + violations: list[str] = [] + + for qualified_name, component_class in _ALL_COMPONENTS: + for output_name, display_name in _get_output_display_names(component_class): + if display_name == DEPRECATED_DISPLAY_NAME_DATA_OR_DATAFRAME: + violations.append( + f"{qualified_name} output '{output_name}' has " + f"display_name='{DEPRECATED_DISPLAY_NAME_DATA_OR_DATAFRAME}'" + ) + + for input_name, display_name in _get_input_display_names(component_class): + if display_name == DEPRECATED_DISPLAY_NAME_DATA_OR_DATAFRAME: + violations.append( + f"{qualified_name} input '{input_name}' has " + f"display_name='{DEPRECATED_DISPLAY_NAME_DATA_OR_DATAFRAME}'" + ) + + assert not violations, ( + f"Found {len(violations)} field(s) still using deprecated " + f"display_name='{DEPRECATED_DISPLAY_NAME_DATA_OR_DATAFRAME}':\n" + "\n".join(f" - {v}" for v in violations) + ) + + +class TestComponentDiscoverySanity: + """Sanity checks to ensure the test infrastructure itself is working.""" + + def test_should_discover_a_minimum_number_of_components(self): + """Ensure we are actually scanning a meaningful number of components. + + If this fails, it means the discovery mechanism is broken and the + display name tests above are vacuously passing. + """ + assert len(_ALL_COMPONENTS) >= MINIMUM_EXPECTED_COMPONENTS, ( + f"Only discovered {len(_ALL_COMPONENTS)} components, expected at least " + f"{MINIMUM_EXPECTED_COMPONENTS}. Component discovery may be broken." + ) + + def test_should_find_components_with_outputs(self): + """At least some discovered components should have outputs defined.""" + components_with_outputs = [name for name, cls in _ALL_COMPONENTS if getattr(cls, "outputs", None)] + assert len(components_with_outputs) > 0, "No components with outputs found. Output inspection may be broken." + + def test_should_find_components_with_inputs(self): + """At least some discovered components should have inputs defined.""" + components_with_inputs = [name for name, cls in _ALL_COMPONENTS if getattr(cls, "inputs", None)] + assert len(components_with_inputs) > 0, "No components with inputs found. Input inspection may be broken." diff --git a/src/lfx/tests/unit/graph/edge/test_types_compatible.py b/src/lfx/tests/unit/graph/edge/test_types_compatible.py new file mode 100644 index 000000000000..8cef7c1c9a21 --- /dev/null +++ b/src/lfx/tests/unit/graph/edge/test_types_compatible.py @@ -0,0 +1,357 @@ +"""Tests for the types_compatible function and TYPE_MIGRATIONS constant. + +Validates that the Data->JSON and DataFrame->Table type rename migration +works correctly for edge compatibility checking. +""" + +from __future__ import annotations + +from lfx.graph.edge.base import TYPE_MIGRATIONS, types_compatible + +# --- Named constants to avoid magic values --- + +# Old type names (pre-migration) +OLD_DATA_TYPE = "Data" +OLD_DATAFRAME_TYPE = "DataFrame" + +# New type names (post-migration) +NEW_JSON_TYPE = "JSON" +NEW_TABLE_TYPE = "Table" + +# Types that are NOT part of any migration +MESSAGE_TYPE = "Message" +TEXT_TYPE = "Text" +CUSTOM_TYPE = "CustomType" +ANOTHER_CUSTOM_TYPE = "AnotherCustomType" + +# Invalid / adversarial type strings +LOWERCASE_DATA = "data" +LOWERCASE_JSON = "json" +PARTIAL_DATA = "Dat" +EMPTY_STRING_TYPE = "" + + +class TestTypeMigrationsConstant: + """Verify the TYPE_MIGRATIONS dict is correctly defined.""" + + def test_should_map_data_to_json(self): + assert TYPE_MIGRATIONS[OLD_DATA_TYPE] == NEW_JSON_TYPE + + def test_should_map_dataframe_to_table(self): + assert TYPE_MIGRATIONS[OLD_DATAFRAME_TYPE] == NEW_TABLE_TYPE + + def test_should_contain_exactly_two_entries(self): + assert len(TYPE_MIGRATIONS) == 2 + + +class TestTypesCompatibleSuccessCases: + """Tests for cases where types SHOULD be compatible.""" + + def test_should_be_compatible_when_old_output_matches_old_input(self): + # Arrange + output_types = [OLD_DATA_TYPE] + input_types = [OLD_DATA_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is True + + def test_should_be_compatible_when_new_output_matches_new_input(self): + # Arrange + output_types = [NEW_JSON_TYPE] + input_types = [NEW_JSON_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is True + + def test_should_be_compatible_when_old_output_matches_new_input(self): + # Arrange: Data output connecting to a JSON input + output_types = [OLD_DATA_TYPE] + input_types = [NEW_JSON_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is True + + def test_should_be_compatible_when_new_output_matches_old_input(self): + # Arrange: JSON output connecting to a Data input + output_types = [NEW_JSON_TYPE] + input_types = [OLD_DATA_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is True + + def test_should_be_compatible_when_dataframe_output_matches_table_input(self): + # Arrange + output_types = [OLD_DATAFRAME_TYPE] + input_types = [NEW_TABLE_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is True + + def test_should_be_compatible_when_table_output_matches_dataframe_input(self): + # Arrange + output_types = [NEW_TABLE_TYPE] + input_types = [OLD_DATAFRAME_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is True + + def test_should_be_compatible_when_non_migrated_types_match(self): + # Arrange: Message->Message, no migration involved + output_types = [MESSAGE_TYPE] + input_types = [MESSAGE_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is True + + def test_should_be_compatible_when_any_output_matches_any_input(self): + # Arrange: Multiple types in both lists, one pair matches + output_types = [TEXT_TYPE, OLD_DATA_TYPE] + input_types = [MESSAGE_TYPE, NEW_JSON_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert: Data->JSON migration makes this compatible + assert result is True + + +class TestTypesCompatibleNegativeCases: + """Tests for cases where types should NOT be compatible.""" + + def test_should_not_be_compatible_when_types_completely_different(self): + # Arrange: Data and Message are unrelated + output_types = [OLD_DATA_TYPE] + input_types = [MESSAGE_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_not_be_compatible_when_json_vs_table(self): + # Arrange: JSON and Table are different migration families + output_types = [NEW_JSON_TYPE] + input_types = [NEW_TABLE_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_not_be_compatible_when_data_vs_dataframe(self): + # Arrange: Data and DataFrame are from different migration families + output_types = [OLD_DATA_TYPE] + input_types = [OLD_DATAFRAME_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_not_be_compatible_when_data_vs_table(self): + # Arrange: Data (->JSON) should not match Table + output_types = [OLD_DATA_TYPE] + input_types = [NEW_TABLE_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_not_be_compatible_when_dataframe_vs_json(self): + # Arrange: DataFrame (->Table) should not match JSON + output_types = [OLD_DATAFRAME_TYPE] + input_types = [NEW_JSON_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + +class TestTypesCompatibleEdgeCases: + """Tests for boundary and edge conditions.""" + + def test_should_not_be_compatible_when_both_lists_empty(self): + # Arrange + output_types: list[str] = [] + input_types: list[str] = [] + + # Act + result = types_compatible(output_types, input_types) + + # Assert: No types to match means not compatible + assert result is False + + def test_should_not_be_compatible_when_output_types_empty(self): + # Arrange + output_types: list[str] = [] + input_types = [NEW_JSON_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_not_be_compatible_when_input_types_empty(self): + # Arrange + output_types = [OLD_DATA_TYPE] + input_types: list[str] = [] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_be_compatible_with_single_match_in_large_lists(self): + # Arrange: Many non-matching types but one match buried in the lists + output_types = [TEXT_TYPE, CUSTOM_TYPE, "TypeA", "TypeB", OLD_DATAFRAME_TYPE] + input_types = ["TypeC", "TypeD", "TypeE", MESSAGE_TYPE, NEW_TABLE_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert: DataFrame->Table migration makes this compatible + assert result is True + + def test_should_handle_unknown_types_without_migration(self): + # Arrange: Types not in TYPE_MIGRATIONS should still match directly + output_types = [CUSTOM_TYPE] + input_types = [CUSTOM_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is True + + def test_should_not_match_unknown_types_that_differ(self): + # Arrange: Two different unknown types + output_types = [CUSTOM_TYPE] + input_types = [ANOTHER_CUSTOM_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_not_crash_with_empty_string_types(self): + # Arrange: Empty strings as types + output_types = [EMPTY_STRING_TYPE] + input_types = [EMPTY_STRING_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert: Empty strings match each other (direct equality) + assert result is True + + def test_should_not_match_empty_string_vs_real_type(self): + # Arrange + output_types = [EMPTY_STRING_TYPE] + input_types = [OLD_DATA_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + +class TestTypesCompatibleAdversarialCases: + """Tests for adversarial / tricky inputs.""" + + def test_should_not_match_case_sensitive_types(self): + # Arrange: "data" (lowercase) should NOT match "Data" (capitalized) + output_types = [LOWERCASE_DATA] + input_types = [OLD_DATA_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert: Type matching is case-sensitive + assert result is False + + def test_should_not_match_lowercase_json_vs_uppercase(self): + # Arrange: "json" should NOT match "JSON" + output_types = [LOWERCASE_JSON] + input_types = [NEW_JSON_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_not_match_partial_type_names(self): + # Arrange: "Dat" should NOT match "Data" + output_types = [PARTIAL_DATA] + input_types = [OLD_DATA_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_be_compatible_when_mixed_old_and_new_in_same_list(self): + # Arrange: A list containing both old and new names + output_types = [OLD_DATA_TYPE, NEW_TABLE_TYPE] + input_types = [NEW_JSON_TYPE, OLD_DATAFRAME_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert: Data->JSON migration matches, and Table->DataFrame migration matches + assert result is True + + def test_should_not_match_reversed_migration_families(self): + # Arrange: JSON output should not match DataFrame input + output_types = [NEW_JSON_TYPE] + input_types = [OLD_DATAFRAME_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False + + def test_should_not_match_table_vs_data(self): + # Arrange: Table output should not match Data input + output_types = [NEW_TABLE_TYPE] + input_types = [OLD_DATA_TYPE] + + # Act + result = types_compatible(output_types, input_types) + + # Assert + assert result is False From 911bc9d70adcbba8fae24d966c1d514a4cb2fe32 Mon Sep 17 00:00:00 2001 From: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:15:28 -0400 Subject: [PATCH 02/29] fix: update openai api key prefix in fe (#12122) update openai api key prefix in fe --- .../modelProviderModal/components/ProviderConfigurationForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/modals/modelProviderModal/components/ProviderConfigurationForm.tsx b/src/frontend/src/modals/modelProviderModal/components/ProviderConfigurationForm.tsx index 5cf952294b2d..b80278e97e5b 100644 --- a/src/frontend/src/modals/modelProviderModal/components/ProviderConfigurationForm.tsx +++ b/src/frontend/src/modals/modelProviderModal/components/ProviderConfigurationForm.tsx @@ -13,7 +13,7 @@ const PROVIDER_KEY_PREVIEW: Record< string, { prefix: string; totalLength: number } > = { - OpenAI: { prefix: "sk-proj-", totalLength: 164 }, + OpenAI: { prefix: "sk-", totalLength: 164 }, Anthropic: { prefix: "sk-ant-", totalLength: 108 }, "Google Generative AI": { prefix: "AIza", totalLength: 39 }, "IBM watsonx": { prefix: "", totalLength: 44 }, From 9012a5476a72a7b5e9e203fd3dd95acb1dc88a5e Mon Sep 17 00:00:00 2001 From: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:40:10 -0400 Subject: [PATCH 03/29] feat: revise deployment schemas (#12150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * checkout schema revisions * fix: harden deployment schema validation and close coverage gaps - Add exactly-one validation to ConfigDeploymentBindingUpdate (both layers) using model_fields_set XOR for config_id vs raw_payload - Add raw_payload field to service-layer ConfigDeploymentBindingUpdate for symmetry with the snapshot passthrough pattern - Add deployment_type routing hint to execution methods (protocol, ABC, service) - Unify duplicate handling: silent dedup everywhere (dict.fromkeys) - Fix broken API tests after FlowVersionsAttach/Patch str→UUID migration - Restore deleted test coverage (blank ids, order-preserving dedup) and add new tests for raw_payload, mutual exclusion, snapshot_ids, and noop rejection - Add clarifying comments: overlap check ID domains, post-validation types, deployment_type scope in protocol docstring * tighten update payload validation and add provider_data * add validation for None fields and harden tests * use explicit boolean to unbind config --- .secrets.baseline | 6 +- .../langflow/api/v1/schemas/deployments.py | 86 +++++++--- .../unit/api/v1/test_deployment_schemas.py | 106 ++++++++++-- .../lfx/services/adapters/deployment/base.py | 9 ++ .../services/adapters/deployment/schema.py | 85 ++++++++-- .../services/adapters/deployment/service.py | 9 ++ src/lfx/src/lfx/services/interfaces.py | 12 ++ .../deployment/test_deployment_schema.py | 151 +++++++++++++++--- 8 files changed, 390 insertions(+), 74 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index d9c801cbbfb2..5aa28eab2900 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -2781,7 +2781,7 @@ "filename": "src/backend/tests/unit/api/v1/test_deployment_schemas.py", "hashed_secret": "99091d046a81493ef2545d8c3cd8e881e8702893", "is_verified": false, - "line_number": 45, + "line_number": 47, "is_secret": false }, { @@ -2789,7 +2789,7 @@ "filename": "src/backend/tests/unit/api/v1/test_deployment_schemas.py", "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_verified": false, - "line_number": 67, + "line_number": 69, "is_secret": false } ], @@ -7378,5 +7378,5 @@ } ] }, - "generated_at": "2026-03-10T15:42:06Z" + "generated_at": "2026-03-11T18:20:16Z" } diff --git a/src/backend/base/langflow/api/v1/schemas/deployments.py b/src/backend/base/langflow/api/v1/schemas/deployments.py index 68b4f6d8a59e..2f684d2b3283 100644 --- a/src/backend/base/langflow/api/v1/schemas/deployments.py +++ b/src/backend/base/langflow/api/v1/schemas/deployments.py @@ -63,23 +63,27 @@ def _validate_str_id_list(values: list[str], *, field_name: str) -> list[str]: - """Strip, reject empty values, reject empty lists, and reject duplicates in a list of string identifiers.""" + """Strip, reject empty/whitespace values, reject empty lists, and deduplicate preserving order.""" if not values: msg = f"{field_name} must not be empty." raise ValueError(msg) - cleaned: list[str] = [] - seen: set[str] = set() + stripped = [] for raw in values: value = raw.strip() if not value: msg = f"{field_name} must not contain empty values." raise ValueError(msg) - if value in seen: - msg = f"{field_name} must not contain duplicate values: '{value}'." - raise ValueError(msg) - seen.add(value) - cleaned.append(value) - return cleaned + stripped.append(value) + return list(dict.fromkeys(stripped)) + + +def _validate_uuid_list(values: list[UUID], *, field_name: str) -> list[UUID]: + """Deduplicate (preserving order) and reject empty lists.""" + deduped = list(dict.fromkeys(values)) + if not deduped: + msg = f"{field_name} must not be empty." + raise ValueError(msg) + return deduped def _normalize_str(value: str, *, field_name: str = "Field") -> str: @@ -297,18 +301,15 @@ class FlowVersionsAttach(BaseModel): model_config = {"extra": "forbid"} - # Typed as str (not UUID) because the service layer uses a flexible IdLike - # type (UUID | NormalizedId). The same str typing is used for the - # query-parameter variant in list_deployments for consistency. - ids: list[str] = Field( + ids: list[UUID] = Field( min_length=1, description="Langflow flow version ids to attach to the deployment.", ) @field_validator("ids") @classmethod - def validate_ids(cls, values: list[str]) -> list[str]: - return _validate_str_id_list(values, field_name="ids") + def validate_ids(cls, values: list[UUID]) -> list[UUID]: + return _validate_uuid_list(values, field_name="ids") class FlowVersionsPatch(BaseModel): @@ -316,21 +317,21 @@ class FlowVersionsPatch(BaseModel): model_config = {"extra": "forbid"} - add: list[str] | None = Field( + add: list[UUID] | None = Field( None, description="Langflow flow version ids to attach to the deployment. Omit to leave unchanged.", ) - remove: list[str] | None = Field( + remove: list[UUID] | None = Field( None, description="Langflow flow version ids to detach from the deployment. Omit to leave unchanged.", ) @field_validator("add", "remove") @classmethod - def validate_id_lists(cls, values: list[str] | None, info: ValidationInfo) -> list[str] | None: + def validate_id_lists(cls, values: list[UUID] | None, info: ValidationInfo) -> list[UUID] | None: if values is None: return None - return _validate_str_id_list(values, field_name=info.field_name) + return _validate_uuid_list(values, field_name=info.field_name) @model_validator(mode="after") def validate_operations(self): @@ -343,7 +344,7 @@ def validate_operations(self): overlap = set(add_values).intersection(remove_values) if overlap: - ids = ", ".join(sorted(overlap)) + ids = ", ".join(sorted(str(v) for v in overlap)) msg = f"Flow version ids cannot be present in both 'add' and 'remove': {ids}." raise ValueError(msg) return self @@ -403,15 +404,47 @@ def validate_exactly_one(self) -> DeploymentConfigCreate: class DeploymentConfigBindingUpdate(BaseModel): - """Config binding patch for an existing deployment.""" + """Config binding patch for an existing deployment. + + Exactly one of ``config_id``, ``raw_payload``, or ``unbind`` must be + provided: + + * ``config_id`` — bind an existing config by reference. + * ``raw_payload`` — create a new config and bind it. + * ``unbind = true`` — detach the current config. + """ model_config = {"extra": "forbid"} config_id: NonEmptyStr | None = Field( default=None, - description="Provider-owned config id to bind to the deployment. Use null to unbind.", + description="Provider-owned config id to bind to the deployment.", ) + raw_payload: _StrictDeploymentConfig | None = Field( + default=None, + description="Config payload to create and bind to the deployment.", + ) + + unbind: bool = Field( + default=False, + description="Set to true to detach the current config from the deployment.", + ) + + @model_validator(mode="after") + def validate_config_update(self) -> DeploymentConfigBindingUpdate: + provided = sum( + [ + self.config_id is not None, + self.raw_payload is not None, + self.unbind, + ] + ) + if provided != 1: + msg = "Exactly one of 'config_id', 'raw_payload', or 'unbind=true' must be provided." + raise ValueError(msg) + return self + # --------------------------------------------------------------------------- # Deployment create / update request schemas @@ -445,11 +478,18 @@ class DeploymentUpdateRequest(BaseModel): description="Flow version attach/detach operations.", ) config: DeploymentConfigBindingUpdate | None = Field(default=None, description="Deployment configuration update.") + provider_data: dict[str, Any] | None = Field( + default=None, + description="Provider-owned opaque update payload.", + ) @model_validator(mode="after") def ensure_any_field_provided(self) -> DeploymentUpdateRequest: if not self.model_fields_set: - msg = "At least one of 'spec', 'flow_version_ids', or 'config' must be provided." + msg = "At least one of 'spec', 'flow_version_ids', 'config', or 'provider_data' must be provided." + raise ValueError(msg) + if self.spec is None and self.flow_version_ids is None and self.config is None and self.provider_data is None: + msg = "At least one of 'spec', 'flow_version_ids', 'config', or 'provider_data' must be provided." raise ValueError(msg) return self diff --git a/src/backend/tests/unit/api/v1/test_deployment_schemas.py b/src/backend/tests/unit/api/v1/test_deployment_schemas.py index bf076fb93f94..029adc724249 100644 --- a/src/backend/tests/unit/api/v1/test_deployment_schemas.py +++ b/src/backend/tests/unit/api/v1/test_deployment_schemas.py @@ -7,9 +7,11 @@ import pytest from langflow.api.v1.schemas.deployments import ( + DeploymentConfigBindingUpdate, DeploymentProviderAccountCreateRequest, DeploymentProviderAccountGetResponse, DeploymentProviderAccountUpdateRequest, + DeploymentUpdateRequest, FlowVersionsAttach, FlowVersionsPatch, ) @@ -90,19 +92,101 @@ def test_rejects_whitespace_only(self): # --------------------------------------------------------------------------- -class TestIdListDuplicateRejection: - def test_flow_versions_attach_rejects_duplicates(self): - with pytest.raises(ValidationError, match="duplicate"): - FlowVersionsAttach(ids=["id1", "id1"]) +class TestUuidIdListDedup: + """UUID id lists silently deduplicate while preserving order.""" - def test_flow_versions_patch_rejects_duplicates_in_add(self): - with pytest.raises(ValidationError, match="duplicate"): - FlowVersionsPatch(add=["id1", "id1"]) + def test_flow_versions_attach_deduplicates(self): + u1, u2 = uuid4(), uuid4() + result = FlowVersionsAttach(ids=[u1, u2, u1]) + assert result.ids == [u1, u2] - def test_flow_versions_patch_rejects_duplicates_in_remove(self): - with pytest.raises(ValidationError, match="duplicate"): - FlowVersionsPatch(remove=["id1", "id1"]) + def test_flow_versions_patch_deduplicates_add(self): + u1, u2 = uuid4(), uuid4() + result = FlowVersionsPatch(add=[u1, u2, u1, u2]) + assert result.add == [u1, u2] + + def test_flow_versions_patch_deduplicates_remove(self): + u1, u2 = uuid4(), uuid4() + result = FlowVersionsPatch(remove=[u2, u1, u2, u1]) + assert result.remove == [u2, u1] def test_flow_versions_patch_rejects_overlap(self): + u1 = uuid4() with pytest.raises(ValidationError, match="both"): - FlowVersionsPatch(add=["id1"], remove=["id1"]) + FlowVersionsPatch(add=[u1], remove=[u1]) + + +# --------------------------------------------------------------------------- +# DeploymentConfigBindingUpdate validation +# --------------------------------------------------------------------------- + + +class TestDeploymentConfigBindingUpdate: + def test_accepts_config_id_only(self): + update = DeploymentConfigBindingUpdate(config_id="cfg_1") + assert update.config_id == "cfg_1" + assert update.raw_payload is None + assert update.unbind is False + + def test_accepts_raw_payload_only(self): + raw_payload = { + "name": "new cfg", + "description": "cfg desc", + "environment_variables": { + "OPENAI_API_KEY": {"value": "OPENAI_API_KEY", "source": "variable"}, + }, + "provider_config": {"region": "us-east-1", "flags": {"dry_run": True}}, + } + update = DeploymentConfigBindingUpdate(raw_payload=raw_payload) + assert update.raw_payload is not None + assert update.raw_payload.model_dump() == raw_payload + assert update.config_id is None + assert update.unbind is False + + def test_accepts_unbind(self): + update = DeploymentConfigBindingUpdate(unbind=True) + assert update.unbind is True + assert update.config_id is None + assert update.raw_payload is None + + def test_rejects_both_config_id_and_raw_payload(self): + with pytest.raises(ValidationError, match="Exactly one of"): + DeploymentConfigBindingUpdate(config_id="cfg_1", raw_payload={"name": "cfg"}) + + def test_rejects_config_id_with_unbind(self): + with pytest.raises(ValidationError, match="Exactly one of"): + DeploymentConfigBindingUpdate(config_id="cfg_1", unbind=True) + + def test_rejects_raw_payload_with_unbind(self): + with pytest.raises(ValidationError, match="Exactly one of"): + DeploymentConfigBindingUpdate(raw_payload={"name": "cfg"}, unbind=True) + + def test_rejects_all_three(self): + with pytest.raises(ValidationError, match="Exactly one of"): + DeploymentConfigBindingUpdate(config_id="cfg_1", raw_payload={"name": "cfg"}, unbind=True) + + def test_rejects_noop_empty_payload(self): + with pytest.raises(ValidationError, match="Exactly one of"): + DeploymentConfigBindingUpdate() + + def test_rejects_unbind_false_alone(self): + with pytest.raises(ValidationError, match="Exactly one of"): + DeploymentConfigBindingUpdate(unbind=False) + + def test_rejects_extra_fields(self): + with pytest.raises(ValidationError, match="Extra inputs"): + DeploymentConfigBindingUpdate(config_id="cfg_1", unknown_field="x") + + +class TestDeploymentUpdateRequest: + def test_accepts_provider_data_only(self): + payload = DeploymentUpdateRequest(provider_data={"mode": "dry_run"}) + assert payload.provider_data == {"mode": "dry_run"} + + def test_rejects_empty_payload(self): + with pytest.raises(ValidationError, match="At least one of"): + DeploymentUpdateRequest() + + def test_rejects_explicit_null_only_payload(self): + with pytest.raises(ValidationError, match="At least one of"): + DeploymentUpdateRequest(spec=None) diff --git a/src/lfx/src/lfx/services/adapters/deployment/base.py b/src/lfx/src/lfx/services/adapters/deployment/base.py index 2366758193f8..90042ea323a6 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/base.py +++ b/src/lfx/src/lfx/services/adapters/deployment/base.py @@ -20,6 +20,7 @@ DeploymentListResult, DeploymentListTypesResult, DeploymentStatusResult, + DeploymentType, DeploymentUpdate, DeploymentUpdateResult, ExecutionCreate, @@ -77,6 +78,7 @@ async def get( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentGetResult: """Return deployment metadata by provider ID.""" @@ -87,6 +89,7 @@ async def update( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, payload: DeploymentUpdate, db: AsyncSession, ) -> DeploymentUpdateResult: @@ -98,6 +101,7 @@ async def redeploy( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> RedeployResult: """Re-apply current deployment inputs without changing them.""" @@ -108,6 +112,7 @@ async def duplicate( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentDuplicateResult: """Create a new deployment using the same inputs as the source.""" @@ -118,6 +123,7 @@ async def delete( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentDeleteResult: """Delete the deployment from the provider.""" @@ -128,6 +134,7 @@ async def get_status( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentStatusResult: """Return provider-reported health/status for the deployment.""" @@ -137,6 +144,7 @@ async def create_execution( self, *, user_id: IdLike, + deployment_type: DeploymentType | None = None, payload: ExecutionCreate, db: AsyncSession, ) -> ExecutionCreateResult: @@ -148,6 +156,7 @@ async def get_execution( *, user_id: IdLike, execution_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> ExecutionStatusResult: """Get provider-agnostic deployment execution state/output.""" diff --git a/src/lfx/src/lfx/services/adapters/deployment/schema.py b/src/lfx/src/lfx/services/adapters/deployment/schema.py index dd90f2b53246..c283dc186eb8 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/schema.py +++ b/src/lfx/src/lfx/services/adapters/deployment/schema.py @@ -93,21 +93,30 @@ class SnapshotItems(BaseModel): class SnapshotDeploymentBindingUpdate(BaseModel): """Snapshot deployment binding patch payload. - Add or remove snapshot bindings for the deployment by reference ids. + Supports three operations: bind existing snapshots by ID, create new + snapshots from raw payloads, or unbind snapshots by ID. At least one + of the three fields must be provided. """ - add: list[IdLike] | None = Field( + add_ids: list[IdLike] | None = Field( None, - description="Snapshot reference ids to attach to the deployment. Omit to leave unchanged.", + description="Existing snapshot ids to attach to the deployment. Omit to leave unchanged.", ) - remove: list[IdLike] | None = Field( + add_raw_payloads: SnapshotList | None = Field( None, - description="Snapshot reference ids to detach from the deployment. Omit to leave unchanged.", + description="Raw snapshot payloads to create and attach to the deployment. Omit to leave unchanged.", + ) + remove_ids: list[IdLike] | None = Field( + None, + description="Snapshot ids to detach from the deployment. Omit to leave unchanged.", ) - @field_validator("add", "remove") + @field_validator("add_ids", "remove_ids") @classmethod def validate_id_lists(cls, v: list[IdLike] | None) -> list[str] | None: + # Post-validation: values are always normalized strings (UUIDs + # are stringified by _normalize_and_dedupe_id_list). The field + # annotation remains list[IdLike] so Pydantic accepts UUID input. if v is None: return None return _normalize_and_dedupe_id_list(v, field_name="snapshot_id") @@ -115,17 +124,21 @@ def validate_id_lists(cls, v: list[IdLike] | None) -> list[str] | None: @model_validator(mode="after") def validate_operations(self): """Ensure patch contains explicit and non-conflicting operations.""" - add_values = self.add or [] - remove_values = self.remove or [] + add_values = self.add_ids or [] + raw_values = self.add_raw_payloads or [] + remove_values = self.remove_ids or [] - if not add_values and not remove_values: - msg = "At least one of 'add' or 'remove' must be provided." + if not add_values and not raw_values and not remove_values: + msg = "At least one of 'add_ids', 'add_raw_payloads', or 'remove_ids' must be provided." raise ValueError(msg) + # Overlap check covers add_ids vs remove_ids only. + # add_raw_payloads carry flow-artifact IDs (Langflow domain), + # while add_ids/remove_ids carry snapshot IDs (provider domain). overlap = set(add_values).intersection(remove_values) if overlap: ids = ", ".join(sorted(overlap)) - msg = f"Snapshot ids cannot be present in both 'add' and 'remove': {ids}." + msg = f"Snapshot ids cannot be present in both 'add_ids' and 'remove_ids': {ids}." raise ValueError(msg) return self @@ -196,11 +209,27 @@ def validate_config_source(self) -> "ConfigItem": class ConfigDeploymentBindingUpdate(BaseModel): - """Config deployment binding patch payload.""" + """Config deployment binding patch payload. + + Exactly one of ``config_id``, ``raw_payload``, or ``unbind`` must be + provided: + + * ``config_id`` — bind an existing config by reference. + * ``raw_payload`` — create a new config and bind it. + * ``unbind = True`` — detach the current config. + """ config_id: IdLike | None = Field( None, - description="Config reference id to bind to the deployment. Use null to unbind.", + description="Config reference id to bind to the deployment.", + ) + raw_payload: DeploymentConfig | None = Field( + None, + description="Config payload to create and bind to the deployment.", + ) + unbind: bool = Field( + default=False, + description="Set to true to detach the current config from the deployment.", ) @field_validator("config_id") @@ -210,6 +239,20 @@ def validate_config_id(cls, v: IdLike | None) -> IdLike | None: return _normalize_and_validate_id(v, field_name="config_id") return v + @model_validator(mode="after") + def validate_config_source(self) -> "ConfigDeploymentBindingUpdate": + provided = sum( + [ + self.config_id is not None, + self.raw_payload is not None, + self.unbind, + ] + ) + if provided != 1: + msg = "Exactly one of 'config_id', 'raw_payload', or 'unbind=true' must be provided." + raise ValueError(msg) + return self + class ProviderDataModel(BaseModel): """Base model for provider metadata payloads.""" @@ -366,11 +409,18 @@ class DeploymentUpdate(BaseModel): spec: BaseDeploymentDataUpdate | None = Field(None, description="The metadata of the deployment") snapshot: SnapshotDeploymentBindingUpdate | None = Field(None, description="The snapshot of the deployment") config: ConfigDeploymentBindingUpdate | None = Field(None, description="The config of the deployment") + provider_data: ProviderPayload | None = Field( + None, + description="Provider-specific opaque payload for deployment update operations.", + ) @model_validator(mode="after") def validate_has_changes(self) -> "DeploymentUpdate": - if self.spec is None and self.snapshot is None and self.config is None: - msg = "At least one of 'spec', 'snapshot', or 'config' must be provided." + if not self.model_fields_set: + msg = "At least one of 'spec', 'snapshot', 'config', or 'provider_data' must be provided." + raise ValueError(msg) + if self.spec is None and self.snapshot is None and self.config is None and self.provider_data is None: + msg = "At least one of 'spec', 'snapshot', 'config', or 'provider_data' must be provided." raise ValueError(msg) return self @@ -378,6 +428,11 @@ def validate_has_changes(self) -> "DeploymentUpdate": class DeploymentUpdateResult(DeploymentOperationResult): """Model representing a result for a deployment update operation.""" + snapshot_ids: list[IdLike] = Field( + default_factory=list, + description="Snapshot ids produced or bound during the update.", + ) + class RedeployResult(DeploymentOperationResult): """Model representing a deployment redeployment operation result.""" diff --git a/src/lfx/src/lfx/services/adapters/deployment/service.py b/src/lfx/src/lfx/services/adapters/deployment/service.py index ca0bdb4a9bc3..b92915bb9c64 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/service.py +++ b/src/lfx/src/lfx/services/adapters/deployment/service.py @@ -22,6 +22,7 @@ DeploymentListResult, DeploymentListTypesResult, DeploymentStatusResult, + DeploymentType, DeploymentUpdate, DeploymentUpdateResult, ExecutionCreate, @@ -79,6 +80,7 @@ async def get( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentGetResult: """Return deployment metadata by provider ID.""" @@ -89,6 +91,7 @@ async def update( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, payload: DeploymentUpdate, db: AsyncSession, ) -> DeploymentUpdateResult: @@ -100,6 +103,7 @@ async def redeploy( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> RedeployResult: """Re-apply current deployment inputs without changing them.""" @@ -110,6 +114,7 @@ async def duplicate( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentDuplicateResult: """Create a new deployment using the same inputs as the source.""" @@ -120,6 +125,7 @@ async def delete( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentDeleteResult: """Delete the deployment from the provider.""" @@ -130,6 +136,7 @@ async def get_status( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentStatusResult: """Return provider-reported health/status for the deployment.""" @@ -139,6 +146,7 @@ async def create_execution( self, *, user_id: IdLike, + deployment_type: DeploymentType | None = None, payload: ExecutionCreate, db: AsyncSession, ) -> ExecutionCreateResult: @@ -150,6 +158,7 @@ async def get_execution( *, user_id: IdLike, execution_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> ExecutionStatusResult: """Get provider-agnostic deployment execution state/output.""" diff --git a/src/lfx/src/lfx/services/interfaces.py b/src/lfx/src/lfx/services/interfaces.py index e6269f23adf5..bb707c6d7fd8 100644 --- a/src/lfx/src/lfx/services/interfaces.py +++ b/src/lfx/src/lfx/services/interfaces.py @@ -21,6 +21,7 @@ DeploymentListResult, DeploymentListTypesResult, DeploymentStatusResult, + DeploymentType, DeploymentUpdate, DeploymentUpdateResult, ExecutionCreate, @@ -235,6 +236,9 @@ class DeploymentServiceProtocol(Protocol): Keep this protocol intentionally narrow (consumer-facing CRUD + status). Adapter-specific or advanced operations are defined on concrete deployment service classes. + + ``deployment_type`` is accepted as an optional routing hint by all + operations that act on a specific deployment (including executions). """ @abstractmethod @@ -275,6 +279,7 @@ async def get( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentGetResult: """Return deployment metadata by provider ID.""" @@ -286,6 +291,7 @@ async def update( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, payload: DeploymentUpdate, db: AsyncSession, ) -> DeploymentUpdateResult: @@ -298,6 +304,7 @@ async def redeploy( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> RedeployResult: """Re-apply current deployment inputs without changing them.""" @@ -309,6 +316,7 @@ async def duplicate( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentDuplicateResult: """Create a new deployment using the same inputs as the source.""" @@ -320,6 +328,7 @@ async def delete( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentDeleteResult: """Delete the deployment from the provider.""" @@ -331,6 +340,7 @@ async def get_status( *, user_id: IdLike, deployment_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> DeploymentStatusResult: """Return provider-reported health/status for the deployment.""" @@ -341,6 +351,7 @@ async def create_execution( self, *, user_id: IdLike, + deployment_type: DeploymentType | None = None, payload: ExecutionCreate, db: AsyncSession, ) -> ExecutionCreateResult: @@ -353,6 +364,7 @@ async def get_execution( *, user_id: IdLike, execution_id: IdLike, + deployment_type: DeploymentType | None = None, db: AsyncSession, ) -> ExecutionStatusResult: """Get provider-agnostic deployment execution state/output.""" diff --git a/src/lfx/tests/unit/services/deployment/test_deployment_schema.py b/src/lfx/tests/unit/services/deployment/test_deployment_schema.py index 69ff60eac6c4..106758b5eaf8 100644 --- a/src/lfx/tests/unit/services/deployment/test_deployment_schema.py +++ b/src/lfx/tests/unit/services/deployment/test_deployment_schema.py @@ -110,54 +110,94 @@ def test_deployment_list_params_rejects_blank_filter_ids() -> None: DeploymentListParams(deployment_ids=[" "]) -def test_snapshot_binding_update_accepts_idlike_and_dedupes() -> None: +def test_snapshot_binding_update_add_ids_dedupes() -> None: snapshot_uuid = uuid4() + payload = SnapshotDeploymentBindingUpdate( + add_ids=[snapshot_uuid, f" {snapshot_uuid} ", "snap_1", "snap_1"], + ) + assert payload.add_ids == [str(snapshot_uuid), "snap_1"] + +def test_snapshot_binding_update_remove_ids_dedupes() -> None: + snapshot_uuid = uuid4() payload = SnapshotDeploymentBindingUpdate( - add=[snapshot_uuid, f" {snapshot_uuid} ", "snap_1", "snap_1"], - remove=[" snap_2 ", "snap_2"], + remove_ids=[snapshot_uuid, f" {snapshot_uuid} ", "snap_2", "snap_2"], ) + assert payload.remove_ids == [str(snapshot_uuid), "snap_2"] + - assert payload.add == [str(snapshot_uuid), "snap_1"] - assert payload.remove == ["snap_2"] +def test_snapshot_binding_update_add_ids_only() -> None: + payload = SnapshotDeploymentBindingUpdate(add_ids=["snap_1"]) + assert payload.add_ids == ["snap_1"] + assert payload.add_raw_payloads is None + assert payload.remove_ids is None -def test_snapshot_binding_update_add_only() -> None: - payload = SnapshotDeploymentBindingUpdate(add=["snap_1"]) - assert payload.add == ["snap_1"] - assert payload.remove is None +def test_snapshot_binding_update_add_raw_payloads_only() -> None: + flow_id = uuid4() + payload = SnapshotDeploymentBindingUpdate( + add_raw_payloads=[ + { + "id": flow_id, + "name": "Flow", + "data": {"nodes": [], "edges": []}, + } + ] + ) + assert payload.add_raw_payloads is not None + assert len(payload.add_raw_payloads) == 1 + assert payload.add_ids is None + assert payload.remove_ids is None -def test_snapshot_binding_update_remove_only() -> None: - payload = SnapshotDeploymentBindingUpdate(remove=["snap_1"]) - assert payload.remove == ["snap_1"] - assert payload.add is None +def test_snapshot_binding_update_remove_ids_only() -> None: + payload = SnapshotDeploymentBindingUpdate(remove_ids=["snap_1"]) + assert payload.remove_ids == ["snap_1"] + assert payload.add_ids is None + assert payload.add_raw_payloads is None def test_snapshot_binding_update_rejects_overlap_after_normalization() -> None: snapshot_uuid = uuid4() - with pytest.raises(ValidationError, match="cannot be present in both 'add' and 'remove'"): + with pytest.raises(ValidationError, match="cannot be present in both"): SnapshotDeploymentBindingUpdate( - add=[snapshot_uuid, " snap_1 "], - remove=[str(snapshot_uuid), "snap_1"], + add_ids=[snapshot_uuid, " snap_1 "], + remove_ids=[str(snapshot_uuid), "snap_1"], ) def test_snapshot_binding_update_rejects_blank_ids() -> None: with pytest.raises(ValidationError): - SnapshotDeploymentBindingUpdate(add=[" "]) + SnapshotDeploymentBindingUpdate(add_ids=[" "]) def test_snapshot_binding_update_preserves_order_while_deduping() -> None: - payload = SnapshotDeploymentBindingUpdate(add=["b", "a", "b", "c", "a"]) - assert payload.add == ["b", "a", "c"] + payload = SnapshotDeploymentBindingUpdate(add_ids=["b", "a", "b", "c", "a"]) + assert payload.add_ids == ["b", "a", "c"] def test_snapshot_binding_update_rejects_noop_payload() -> None: - with pytest.raises(ValidationError, match="At least one of 'add' or 'remove'"): + with pytest.raises(ValidationError, match="At least one of"): SnapshotDeploymentBindingUpdate() +def test_snapshot_binding_update_add_ids_and_raw_payloads_together() -> None: + flow_id = uuid4() + payload = SnapshotDeploymentBindingUpdate( + add_ids=["existing_snap"], + add_raw_payloads=[ + { + "id": flow_id, + "name": "New Flow", + "data": {"nodes": [], "edges": []}, + } + ], + ) + assert payload.add_ids == ["existing_snap"] + assert payload.add_raw_payloads is not None + assert len(payload.add_raw_payloads) == 1 + + def test_config_item_reference_id_rejects_blank() -> None: with pytest.raises(ValidationError): ConfigItem(reference_id=" ") @@ -178,6 +218,50 @@ def test_config_deployment_binding_update_rejects_blank() -> None: ConfigDeploymentBindingUpdate(config_id=" ") +def test_config_deployment_binding_update_accepts_raw_payload() -> None: + update = ConfigDeploymentBindingUpdate(raw_payload={"name": "new cfg"}) + assert update.raw_payload is not None + assert update.config_id is None + assert update.unbind is False + + +def test_config_deployment_binding_update_accepts_unbind() -> None: + update = ConfigDeploymentBindingUpdate(unbind=True) + assert update.unbind is True + assert update.config_id is None + assert update.raw_payload is None + + +def test_config_deployment_binding_update_rejects_both_config_id_and_raw_payload() -> None: + with pytest.raises(ValidationError, match="Exactly one of"): + ConfigDeploymentBindingUpdate(config_id="cfg_1", raw_payload={"name": "cfg"}) + + +def test_config_deployment_binding_update_rejects_config_id_with_unbind() -> None: + with pytest.raises(ValidationError, match="Exactly one of"): + ConfigDeploymentBindingUpdate(config_id="cfg_1", unbind=True) + + +def test_config_deployment_binding_update_rejects_raw_payload_with_unbind() -> None: + with pytest.raises(ValidationError, match="Exactly one of"): + ConfigDeploymentBindingUpdate(raw_payload={"name": "cfg"}, unbind=True) + + +def test_config_deployment_binding_update_rejects_all_three() -> None: + with pytest.raises(ValidationError, match="Exactly one of"): + ConfigDeploymentBindingUpdate(config_id="cfg_1", raw_payload={"name": "cfg"}, unbind=True) + + +def test_config_deployment_binding_update_rejects_noop() -> None: + with pytest.raises(ValidationError, match="Exactly one of"): + ConfigDeploymentBindingUpdate() + + +def test_config_deployment_binding_update_rejects_unbind_false_alone() -> None: + with pytest.raises(ValidationError, match="Exactly one of"): + ConfigDeploymentBindingUpdate(unbind=False) + + def test_deployment_create_rejects_invalid_deployment_type() -> None: with pytest.raises(ValidationError, match="type"): DeploymentCreate(spec={"name": "my deployment", "type": "invalid-type"}) @@ -316,6 +400,16 @@ def test_execution_create_and_status_results_have_same_shape() -> None: assert create_result.model_dump() == status_result.model_dump() +def test_deployment_update_result_snapshot_ids_defaults_empty() -> None: + result = DeploymentUpdateResult(id="dep_1") + assert result.snapshot_ids == [] + + +def test_deployment_update_result_carries_snapshot_ids() -> None: + result = DeploymentUpdateResult(id="dep_1", snapshot_ids=["snap_1", "snap_2"]) + assert result.snapshot_ids == ["snap_1", "snap_2"] + + def test_operation_results_share_provider_result_contract() -> None: provider_result = {"accepted": True} @@ -334,10 +428,15 @@ def test_base_deployment_data_update_requires_at_least_one_field() -> None: def test_deployment_update_requires_at_least_one_section() -> None: - with pytest.raises(ValidationError, match="At least one of 'spec', 'snapshot', or 'config'"): + with pytest.raises(ValidationError, match="At least one of"): DeploymentUpdate() +def test_deployment_update_rejects_explicit_null_only_payload() -> None: + with pytest.raises(ValidationError, match="At least one of"): + DeploymentUpdate(spec=None) + + def test_deployment_update_accepts_spec_only() -> None: update = DeploymentUpdate(spec={"name": "new name"}) assert update.spec is not None @@ -353,12 +452,20 @@ def test_deployment_update_accepts_config_only() -> None: def test_deployment_update_accepts_snapshot_only() -> None: - update = DeploymentUpdate(snapshot={"add": ["snap_1"]}) + update = DeploymentUpdate(snapshot={"add_ids": ["snap_1"]}) assert update.snapshot is not None assert update.spec is None assert update.config is None +def test_deployment_update_accepts_provider_data_only() -> None: + update = DeploymentUpdate(provider_data={"mode": "dry_run"}) + assert update.provider_data == {"mode": "dry_run"} + assert update.spec is None + assert update.snapshot is None + assert update.config is None + + def test_env_var_config_accepts_raw_and_variable_sources() -> None: config = DeploymentConfig( name="cfg", From abf0fb9ab68ac17c937bb3ecb416eb5038780f69 Mon Sep 17 00:00:00 2001 From: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:00:05 -0400 Subject: [PATCH 04/29] feat(deployment): add list operations for configs and snapshots (#12162) * feat: add list ops for configs and snapshots, an api route for listing configs, and both api and deployment adapter schemas, and tests * add the new list ops to the test stub * add deployment id query parameter * make provider account id query param optional * update docstring * tighten deployment list params --- .secrets.baseline | 6 +- .../base/langflow/api/v1/deployments.py | 71 ++++--- .../langflow/api/v1/schemas/deployments.py | 17 ++ .../unit/api/v1/test_deployment_schemas.py | 56 ++++++ .../unit/api/v1/test_deployments_routes.py | 49 +++++ .../lfx/services/adapters/deployment/base.py | 24 +++ .../services/adapters/deployment/schema.py | 103 ++++++++-- .../services/adapters/deployment/service.py | 24 +++ src/lfx/src/lfx/services/interfaces.py | 26 +++ .../unit/services/adapter_test_helpers.py | 2 + .../deployment/test_deployment_exceptions.py | 2 + .../deployment/test_deployment_schema.py | 185 ++++++++++++++++++ 12 files changed, 518 insertions(+), 47 deletions(-) create mode 100644 src/backend/tests/unit/api/v1/test_deployments_routes.py diff --git a/.secrets.baseline b/.secrets.baseline index 5aa28eab2900..23f92df054b1 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -2781,7 +2781,7 @@ "filename": "src/backend/tests/unit/api/v1/test_deployment_schemas.py", "hashed_secret": "99091d046a81493ef2545d8c3cd8e881e8702893", "is_verified": false, - "line_number": 47, + "line_number": 49, "is_secret": false }, { @@ -2789,7 +2789,7 @@ "filename": "src/backend/tests/unit/api/v1/test_deployment_schemas.py", "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_verified": false, - "line_number": 69, + "line_number": 71, "is_secret": false } ], @@ -7378,5 +7378,5 @@ } ] }, - "generated_at": "2026-03-11T18:20:16Z" + "generated_at": "2026-03-12T02:41:29Z" } diff --git a/src/backend/base/langflow/api/v1/deployments.py b/src/backend/base/langflow/api/v1/deployments.py index c61c3cdd7098..c90c0b14af38 100644 --- a/src/backend/base/langflow/api/v1/deployments.py +++ b/src/backend/base/langflow/api/v1/deployments.py @@ -12,6 +12,7 @@ from langflow.api.utils import CurrentActiveUser, DbSession, DbSessionReadOnly from langflow.api.v1.schemas.deployments import ( + DeploymentConfigListResponse, DeploymentCreateRequest, DeploymentCreateResponse, DeploymentDuplicateResponse, @@ -47,6 +48,10 @@ UUID, Path(description="Langflow DB deployment UUID (`deployment.id`)."), ] +DeploymentIdQuery = Annotated[ + UUID, + Query(description="Langflow DB deployment UUID (`deployment.id`)."), +] # API provider-context contract matrix: @@ -145,16 +150,6 @@ async def create_deployment( raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") -@router.get("/types", response_model=DeploymentTypeListResponse) -async def list_deployment_types( - provider_id: DeploymentProviderAccountIdQuery, - session: DbSessionReadOnly, - current_user: CurrentActiveUser, -): - """List deployment types for the selected Langflow provider-account UUID.""" - raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") - - @router.get("", response_model=DeploymentListResponse) async def list_deployments( provider_id: DeploymentProviderAccountIdQuery, @@ -179,6 +174,16 @@ async def list_deployments( raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") +@router.get("/types", response_model=DeploymentTypeListResponse) +async def list_deployment_types( + provider_id: DeploymentProviderAccountIdQuery, + session: DbSessionReadOnly, + current_user: CurrentActiveUser, +): + """List deployment types for the selected Langflow provider-account UUID.""" + raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") + + # --------------------------------------------------------------------------- # Routes: Executions # --------------------------------------------------------------------------- @@ -206,7 +211,25 @@ async def get_deployment_execution( # --------------------------------------------------------------------------- -# Routes: Single deployment operations +# Routes: Configs +# --------------------------------------------------------------------------- + + +@router.get("/configs", response_model=DeploymentConfigListResponse) +async def list_deployment_configs( + session: DbSessionReadOnly, + current_user: CurrentActiveUser, + deployment_id: DeploymentIdQuery, # required today, not going to provide global listing for now + provider_id: DeploymentProviderAccountIdQuery | None = None, + page: Annotated[int, Query(ge=1)] = 1, + size: Annotated[int, Query(ge=1, le=50)] = 20, +): + """List deployment configs.""" + raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") + + +# --------------------------------------------------------------------------- +# Routes: Deployment details and actions # --------------------------------------------------------------------------- @@ -244,6 +267,19 @@ async def delete_deployment( raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") +@router.get( + "/{deployment_id}/status", + response_model=DeploymentStatusResponse, +) +async def get_deployment_status( + deployment_id: DeploymentIdPath, + session: DbSessionReadOnly, + current_user: CurrentActiveUser, +): + """Get deployment status.""" + raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") + + @router.post( "/{deployment_id}/redeploy", response_model=DeploymentRedeployResponse, @@ -269,16 +305,3 @@ async def duplicate_deployment( ): """Duplicate a deployment.""" raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") - - -@router.get( - "/{deployment_id}/status", - response_model=DeploymentStatusResponse, -) -async def get_deployment_status( - deployment_id: DeploymentIdPath, - session: DbSessionReadOnly, - current_user: CurrentActiveUser, -): - """Get deployment status.""" - raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented.") diff --git a/src/backend/base/langflow/api/v1/schemas/deployments.py b/src/backend/base/langflow/api/v1/schemas/deployments.py index 2f684d2b3283..1a8b40c54d3c 100644 --- a/src/backend/base/langflow/api/v1/schemas/deployments.py +++ b/src/backend/base/langflow/api/v1/schemas/deployments.py @@ -271,6 +271,23 @@ class DeploymentProviderAccountListResponse(_PaginatedResponse): providers: list[DeploymentProviderAccountGetResponse] +class DeploymentConfigListItem(BaseModel): + """Lean config representation used in list responses.""" + + id: str = Field(description="Provider-owned config identifier.") + name: str + created_at: datetime | None = None + updated_at: datetime | None = None + provider_data: dict[str, Any] | None = Field( + default=None, + description="Provider-owned opaque payload returned by the deployment provider.", + ) + + +class DeploymentConfigListResponse(_PaginatedResponse): + configs: list[DeploymentConfigListItem] + + class DeploymentCreateResponse(_DeploymentResponseBase): """API response for deployment creation.""" diff --git a/src/backend/tests/unit/api/v1/test_deployment_schemas.py b/src/backend/tests/unit/api/v1/test_deployment_schemas.py index 029adc724249..b77351677255 100644 --- a/src/backend/tests/unit/api/v1/test_deployment_schemas.py +++ b/src/backend/tests/unit/api/v1/test_deployment_schemas.py @@ -8,6 +8,8 @@ import pytest from langflow.api.v1.schemas.deployments import ( DeploymentConfigBindingUpdate, + DeploymentConfigListItem, + DeploymentConfigListResponse, DeploymentProviderAccountCreateRequest, DeploymentProviderAccountGetResponse, DeploymentProviderAccountUpdateRequest, @@ -190,3 +192,57 @@ def test_rejects_empty_payload(self): def test_rejects_explicit_null_only_payload(self): with pytest.raises(ValidationError, match="At least one of"): DeploymentUpdateRequest(spec=None) + + +# --------------------------------------------------------------------------- +# DeploymentConfigListItem / DeploymentConfigListResponse +# --------------------------------------------------------------------------- + + +class TestDeploymentConfigListItem: + def test_accepts_minimal_fields(self): + item = DeploymentConfigListItem(id="cfg_1", name="Config") + assert item.id == "cfg_1" + assert item.name == "Config" + assert item.created_at is None + assert item.updated_at is None + assert item.provider_data is None + + def test_does_not_have_description(self): + assert "description" not in DeploymentConfigListItem.model_fields + + def test_accepts_all_fields(self): + from datetime import datetime, timezone + + now = datetime.now(tz=timezone.utc) + item = DeploymentConfigListItem( + id="cfg_1", + name="Config", + created_at=now, + updated_at=now, + provider_data={"region": "us-east-1"}, + ) + assert item.created_at == now + assert item.provider_data == {"region": "us-east-1"} + + +class TestDeploymentConfigListResponse: + def test_wraps_items_with_pagination(self): + response = DeploymentConfigListResponse( + configs=[ + DeploymentConfigListItem(id="cfg_1", name="Config 1"), + DeploymentConfigListItem(id="cfg_2", name="Config 2"), + ], + page=1, + size=20, + total=2, + ) + assert len(response.configs) == 2 + assert response.page == 1 + assert response.total == 2 + + def test_pagination_defaults(self): + response = DeploymentConfigListResponse(configs=[]) + assert response.page == 1 + assert response.size == 20 + assert response.total == 0 diff --git a/src/backend/tests/unit/api/v1/test_deployments_routes.py b/src/backend/tests/unit/api/v1/test_deployments_routes.py new file mode 100644 index 000000000000..d4a1d870db27 --- /dev/null +++ b/src/backend/tests/unit/api/v1/test_deployments_routes.py @@ -0,0 +1,49 @@ +"""Route matching tests for deployment endpoints. + +These tests guard against route-order regressions where static routes could be +captured by the dynamic `/{deployment_id}` route. +""" + +from uuid import uuid4 + +import pytest +from fastapi import FastAPI +from fastapi.routing import APIRoute +from langflow.api.v1.deployments import router +from starlette.routing import Match + + +@pytest.fixture +def deployment_routes() -> list[APIRoute]: + app = FastAPI() + app.include_router(router) + return [route for route in app.router.routes if isinstance(route, APIRoute)] + + +def _resolve_endpoint_name(routes: list[APIRoute], *, path: str, method: str = "GET") -> str: + scope = {"type": "http", "path": path, "method": method} + for route in routes: + match, _ = route.matches(scope) + if match == Match.FULL: + return route.endpoint.__name__ + msg = f"No matching route for {method} {path}" + raise AssertionError(msg) + + +def test_configs_path_matches_configs_endpoint(deployment_routes: list[APIRoute]) -> None: + assert _resolve_endpoint_name(deployment_routes, path="/deployments/configs") == "list_deployment_configs" + + +def test_types_path_matches_types_endpoint(deployment_routes: list[APIRoute]) -> None: + assert _resolve_endpoint_name(deployment_routes, path="/deployments/types") == "list_deployment_types" + + +def test_deployment_status_path_matches_status_endpoint(deployment_routes: list[APIRoute]) -> None: + deployment_id = uuid4() + assert ( + _resolve_endpoint_name( + deployment_routes, + path=f"/deployments/{deployment_id}/status", + ) + == "get_deployment_status" + ) diff --git a/src/lfx/src/lfx/services/adapters/deployment/base.py b/src/lfx/src/lfx/services/adapters/deployment/base.py index 90042ea323a6..5b69419ccb64 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/base.py +++ b/src/lfx/src/lfx/services/adapters/deployment/base.py @@ -11,6 +11,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from lfx.services.adapters.deployment.schema import ( + ConfigListParams, + ConfigListResult, DeploymentCreate, DeploymentCreateResult, DeploymentDeleteResult, @@ -28,6 +30,8 @@ ExecutionStatusResult, IdLike, RedeployResult, + SnapshotListParams, + SnapshotListResult, ) from lfx.services.interfaces import DeploymentServiceProtocol @@ -161,6 +165,26 @@ async def get_execution( ) -> ExecutionStatusResult: """Get provider-agnostic deployment execution state/output.""" + @abstractmethod + async def list_configs( + self, + *, + user_id: IdLike, + params: ConfigListParams | None = None, + db: AsyncSession, + ) -> ConfigListResult: + """List configs visible to this adapter.""" + + @abstractmethod + async def list_snapshots( + self, + *, + user_id: IdLike, + params: SnapshotListParams | None = None, + db: AsyncSession, + ) -> SnapshotListResult: + """List snapshots visible to this adapter.""" + if TYPE_CHECKING: # Static assertion: keep ABC API in sync with the consumer protocol. diff --git a/src/lfx/src/lfx/services/adapters/deployment/schema.py b/src/lfx/src/lfx/services/adapters/deployment/schema.py index c283dc186eb8..e0bd38898109 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/schema.py +++ b/src/lfx/src/lfx/services/adapters/deployment/schema.py @@ -72,7 +72,6 @@ class SnapshotItem(BaseModel): id: IdLike = Field(description="The id of the snapshot item") name: str = Field(description="The name of the snapshot item") - description: str | None = Field(None, description="The description of the snapshot item") provider_data: dict | None = Field(None, description="The data of the snapshot item from the provider") @@ -254,6 +253,16 @@ def validate_config_source(self) -> "ConfigDeploymentBindingUpdate": return self +class ConfigListItem(BaseModel): + """Model representing a result for a config list item.""" + + id: IdLike = Field(description="The id of the config item") + name: str = Field(description="The name of the config item") + created_at: datetime.datetime | None = Field(None, description="The created timestamp of the config item") + updated_at: datetime.datetime | None = Field(None, description="The last updated timestamp of the config item") + provider_data: dict | None = Field(None, description="The data of the config item from the provider") + + class ProviderDataModel(BaseModel): """Base model for provider metadata payloads.""" @@ -330,28 +339,61 @@ class DeploymentListResult(ProviderResultModel): deployments: list[ItemResult] = Field(description="The list of deployments") -class DeploymentListParams(BaseModel): - """Query params for deployment list operations.""" +class ConfigListResult(ProviderResultModel): + """Model representing a result for a config list operation.""" + + configs: list[ConfigListItem] = Field(description="The list of configs") + + +class SnapshotListResult(ProviderResultModel): + """Model representing a result for a snapshot list operation.""" + + snapshots: list[SnapshotItem] = Field(description="The list of snapshots") + + +class _BaseListParams(BaseModel): + """Shared list-filter fields.""" provider_params: ProviderPayload | None = Field( None, - description="Provider-specific query params payload.", + description="Provider-specific query params to filter by.", ) - deployment_types: list[DeploymentType] | None = Field( + deployment_ids: list[IdLike] | None = Field( None, - description="Deployment types to include in the result set.", + description="Deployment ids to filter by.", ) - deployment_ids: list[IdLike] | None = Field( + + @staticmethod + def _normalize_filter_id_values(value: list[IdLike] | None, *, field_name: str) -> list[str] | None: + if value is None: + return None + normalized_ids = _normalize_and_validate_id_list( + [str(item) for item in value], + field_name=field_name, + ) + # Keep first occurrence order while removing duplicates. + return list(dict.fromkeys(normalized_ids)) + + @field_validator("deployment_ids") + @classmethod + def validate_filter_ids(cls, value: list[IdLike] | None, info) -> list[str] | None: + return cls._normalize_filter_id_values(value, field_name=info.field_name) + + +class DeploymentListParams(_BaseListParams): + """Query params for deployment list operations.""" + + deployment_types: list[DeploymentType] | None = Field( None, - description="Deployment ids to include in the result set.", + description="Deployment types to filter by.", ) snapshot_ids: list[IdLike] | None = Field( None, - description="Snapshot ids to include in the result set.", + description="Snapshot ids to filter by.", ) config_ids: list[IdLike] | None = Field( None, - description="Config ids to include in the result set.", + description="Config ids to filter by.", ) @field_validator("deployment_types") @@ -362,17 +404,38 @@ def validate_deployment_types(cls, value: list[DeploymentType] | None) -> list[D # Keep first occurrence order while removing duplicates. return list(dict.fromkeys(value)) - @field_validator("deployment_ids", "snapshot_ids", "config_ids") + @field_validator("snapshot_ids", "config_ids") @classmethod - def validate_filter_ids(cls, value: list[IdLike] | None, info) -> list[str] | None: - if value is None: - return None - normalized_ids = _normalize_and_validate_id_list( - [str(item) for item in value], - field_name=info.field_name, - ) - # Keep first occurrence order while removing duplicates. - return list(dict.fromkeys(normalized_ids)) + def validate_entity_filter_ids(cls, value: list[IdLike] | None, info) -> list[str] | None: + return cls._normalize_filter_id_values(value, field_name=info.field_name) + + +class ConfigListParams(_BaseListParams): + """Query params for config list operations.""" + + config_ids: list[IdLike] | None = Field( + None, + description="Config ids to filter by.", + ) + + @field_validator("config_ids") + @classmethod + def validate_config_ids(cls, value: list[IdLike] | None, info) -> list[str] | None: + return cls._normalize_filter_id_values(value, field_name=info.field_name) + + +class SnapshotListParams(_BaseListParams): + """Query params for snapshot list operations.""" + + snapshot_ids: list[IdLike] | None = Field( + None, + description="Snapshot ids to filter by.", + ) + + @field_validator("snapshot_ids") + @classmethod + def validate_snapshot_ids(cls, value: list[IdLike] | None, info) -> list[str] | None: + return cls._normalize_filter_id_values(value, field_name=info.field_name) class DeploymentCreate(BaseModel): diff --git a/src/lfx/src/lfx/services/adapters/deployment/service.py b/src/lfx/src/lfx/services/adapters/deployment/service.py index b92915bb9c64..7419fb48e356 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/service.py +++ b/src/lfx/src/lfx/services/adapters/deployment/service.py @@ -13,6 +13,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from lfx.services.adapters.deployment.schema import ( + ConfigListParams, + ConfigListResult, DeploymentCreate, DeploymentCreateResult, DeploymentDeleteResult, @@ -30,6 +32,8 @@ ExecutionStatusResult, IdLike, RedeployResult, + SnapshotListParams, + SnapshotListResult, ) @@ -164,5 +168,25 @@ async def get_execution( """Get provider-agnostic deployment execution state/output.""" raise DeploymentNotConfiguredError(method="get_execution") + async def list_configs( + self, + *, + user_id: IdLike, + params: ConfigListParams | None = None, + db: AsyncSession, + ) -> ConfigListResult: + """List configs visible to this adapter.""" + raise DeploymentNotConfiguredError(method="list_configs") + + async def list_snapshots( + self, + *, + user_id: IdLike, + params: SnapshotListParams | None = None, + db: AsyncSession, + ) -> SnapshotListResult: + """List snapshots visible to this adapter.""" + raise DeploymentNotConfiguredError(method="list_snapshots") + async def teardown(self) -> None: logger.debug("Deployment service teardown") diff --git a/src/lfx/src/lfx/services/interfaces.py b/src/lfx/src/lfx/services/interfaces.py index bb707c6d7fd8..cdc08420bc55 100644 --- a/src/lfx/src/lfx/services/interfaces.py +++ b/src/lfx/src/lfx/services/interfaces.py @@ -12,6 +12,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from lfx.services.adapters.deployment.schema import ( + ConfigListParams, + ConfigListResult, DeploymentCreate, DeploymentCreateResult, DeploymentDeleteResult, @@ -29,6 +31,8 @@ ExecutionStatusResult, IdLike, RedeployResult, + SnapshotListParams, + SnapshotListResult, ) from lfx.services.settings.base import Settings @@ -370,6 +374,28 @@ async def get_execution( """Get provider-agnostic deployment execution state/output.""" ... + @abstractmethod + async def list_configs( + self, + *, + user_id: IdLike, + params: ConfigListParams | None = None, + db: AsyncSession, + ) -> ConfigListResult: + """List configs visible to this adapter.""" + ... + + @abstractmethod + async def list_snapshots( + self, + *, + user_id: IdLike, + params: SnapshotListParams | None = None, + db: AsyncSession, + ) -> SnapshotListResult: + """List snapshots visible to this adapter.""" + ... + @abstractmethod async def teardown(self) -> None: """Teardown the deployment service.""" diff --git a/src/lfx/tests/unit/services/adapter_test_helpers.py b/src/lfx/tests/unit/services/adapter_test_helpers.py index ef84a868dd38..50922c556c5c 100644 --- a/src/lfx/tests/unit/services/adapter_test_helpers.py +++ b/src/lfx/tests/unit/services/adapter_test_helpers.py @@ -23,6 +23,8 @@ async def delete(self, **kw): ... async def get_status(self, **kw): ... async def create_execution(self, **kw): ... async def get_execution(self, **kw): ... + async def list_configs(self, **kw): ... + async def list_snapshots(self, **kw): ... async def teardown(self): ... diff --git a/src/lfx/tests/unit/services/deployment/test_deployment_exceptions.py b/src/lfx/tests/unit/services/deployment/test_deployment_exceptions.py index bcd7b3a8d332..3525c84a51bd 100644 --- a/src/lfx/tests/unit/services/deployment/test_deployment_exceptions.py +++ b/src/lfx/tests/unit/services/deployment/test_deployment_exceptions.py @@ -109,6 +109,8 @@ def test_deployment_service_is_base_deployment_service() -> None: ("create", {"user_id": "u1", "payload": None, "db": None}), ("list_types", {"user_id": "u1", "db": None}), ("list", {"user_id": "u1", "db": None}), + ("list_configs", {"user_id": "u1", "db": None}), + ("list_snapshots", {"user_id": "u1", "db": None}), ("get", {"user_id": "u1", "deployment_id": "d1", "db": None}), ("update", {"user_id": "u1", "deployment_id": "d1", "payload": None, "db": None}), ("redeploy", {"user_id": "u1", "deployment_id": "d1", "db": None}), diff --git a/src/lfx/tests/unit/services/deployment/test_deployment_schema.py b/src/lfx/tests/unit/services/deployment/test_deployment_schema.py index 106758b5eaf8..04fa86a63548 100644 --- a/src/lfx/tests/unit/services/deployment/test_deployment_schema.py +++ b/src/lfx/tests/unit/services/deployment/test_deployment_schema.py @@ -9,6 +9,9 @@ BaseFlowArtifact, ConfigDeploymentBindingUpdate, ConfigItem, + ConfigListItem, + ConfigListParams, + ConfigListResult, DeploymentConfig, DeploymentCreate, DeploymentCreateResult, @@ -24,7 +27,10 @@ ExecutionStatusResult, RedeployResult, SnapshotDeploymentBindingUpdate, + SnapshotItem, SnapshotItems, + SnapshotListParams, + SnapshotListResult, get_deployment_create_schema, get_str_id, get_uuid, @@ -526,3 +532,182 @@ def test_get_deployment_create_schema_is_cached() -> None: first = get_deployment_create_schema() second = get_deployment_create_schema() assert first is second + + +# --------------------------------------------------------------------------- +# SnapshotItem +# --------------------------------------------------------------------------- + + +def test_snapshot_item_accepts_minimal_fields() -> None: + item = SnapshotItem(id="snap_1", name="Snapshot") + assert item.id == "snap_1" + assert item.name == "Snapshot" + assert item.provider_data is None + + +def test_snapshot_item_accepts_uuid_id() -> None: + snap_uuid = uuid4() + item = SnapshotItem(id=snap_uuid, name="Snapshot") + assert item.id == snap_uuid + + +def test_snapshot_item_does_not_have_description() -> None: + assert "description" not in SnapshotItem.model_fields + + +# --------------------------------------------------------------------------- +# ConfigListItem +# --------------------------------------------------------------------------- + + +def test_config_list_item_accepts_minimal_fields() -> None: + item = ConfigListItem(id="cfg_1", name="Config") + assert item.id == "cfg_1" + assert item.name == "Config" + assert item.created_at is None + assert item.updated_at is None + assert item.provider_data is None + + +def test_config_list_item_accepts_uuid_id() -> None: + cfg_uuid = uuid4() + item = ConfigListItem(id=cfg_uuid, name="Config") + assert item.id == cfg_uuid + + +def test_config_list_item_does_not_have_description() -> None: + assert "description" not in ConfigListItem.model_fields + + +def test_config_list_item_accepts_all_fields() -> None: + import datetime + + now = datetime.datetime.now(tz=datetime.timezone.utc) + item = ConfigListItem( + id="cfg_1", + name="Config", + created_at=now, + updated_at=now, + provider_data={"region": "us-east-1"}, + ) + assert item.created_at == now + assert item.updated_at == now + assert item.provider_data == {"region": "us-east-1"} + + +# --------------------------------------------------------------------------- +# ConfigListResult / SnapshotListResult +# --------------------------------------------------------------------------- + + +def test_config_list_result_wraps_items() -> None: + result = ConfigListResult( + configs=[ + ConfigListItem(id="cfg_1", name="Config 1"), + ConfigListItem(id="cfg_2", name="Config 2"), + ], + ) + assert len(result.configs) == 2 + assert result.provider_result is None + + +def test_config_list_result_accepts_provider_result() -> None: + result = ConfigListResult( + configs=[], + provider_result={"total": 0}, + ) + assert result.provider_result == {"total": 0} + + +def test_snapshot_list_result_wraps_items() -> None: + result = SnapshotListResult( + snapshots=[ + SnapshotItem(id="snap_1", name="Snapshot 1"), + ], + ) + assert len(result.snapshots) == 1 + assert result.provider_result is None + + +def test_snapshot_list_result_accepts_provider_result() -> None: + result = SnapshotListResult( + snapshots=[], + provider_result={"total": 0}, + ) + assert result.provider_result == {"total": 0} + + +# --------------------------------------------------------------------------- +# ConfigListParams / SnapshotListParams / DeploymentListParams +# --------------------------------------------------------------------------- + + +def test_config_list_params_defaults_to_none() -> None: + params = ConfigListParams() + assert params.provider_params is None + assert params.deployment_ids is None + assert params.config_ids is None + + +def test_config_list_params_inherits_filter_validation() -> None: + cfg_uuid = uuid4() + params = ConfigListParams( + config_ids=[cfg_uuid, str(cfg_uuid)], + deployment_ids=[" dep-id ", "dep-id"], + ) + assert params.config_ids == [str(cfg_uuid)] + assert params.deployment_ids == ["dep-id"] + + +def test_config_list_params_rejects_blank_ids() -> None: + with pytest.raises(ValidationError): + ConfigListParams(config_ids=[" "]) + + +def test_snapshot_list_params_defaults_to_none() -> None: + params = SnapshotListParams() + assert params.provider_params is None + assert params.deployment_ids is None + assert params.snapshot_ids is None + + +def test_snapshot_list_params_inherits_filter_validation() -> None: + params = SnapshotListParams( + snapshot_ids=[" snap-id ", "snap-id"], + ) + assert params.snapshot_ids == ["snap-id"] + + +def test_snapshot_list_params_rejects_blank_ids() -> None: + with pytest.raises(ValidationError): + SnapshotListParams(snapshot_ids=[" "]) + + +def test_base_list_params_fields_shared_across_all_subclasses() -> None: + """All params classes share only provider/deployment-scoped base fields.""" + for params_cls in (DeploymentListParams, ConfigListParams, SnapshotListParams): + params = params_cls( + provider_params={"key": "value"}, + deployment_ids=["dep_1"], + ) + assert params.provider_params == {"key": "value"} + assert params.deployment_ids == ["dep_1"] + + +def test_entity_specific_fields_are_scoped_to_own_params_classes() -> None: + deployment_fields = set(DeploymentListParams.model_fields) + config_fields = set(ConfigListParams.model_fields) + snapshot_fields = set(SnapshotListParams.model_fields) + + assert "deployment_types" in deployment_fields + assert "snapshot_ids" in deployment_fields + assert "config_ids" in deployment_fields + + assert "config_ids" in config_fields + assert "snapshot_ids" not in config_fields + assert "deployment_types" not in config_fields + + assert "snapshot_ids" in snapshot_fields + assert "config_ids" not in snapshot_fields + assert "deployment_types" not in snapshot_fields From 329ae37230a3b5c0291a1f0ff5c40ef5a82f0876 Mon Sep 17 00:00:00 2001 From: Adam-Aghili <149833988+Adam-Aghili@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:27:01 -0400 Subject: [PATCH 05/29] chore: fix safe biome issues (#12169) * chore: fix safe biome issues ran `npx @biomejs/biome check --write` allowing biome to fix safe issues. I will likely open multiple smaller unsafe versions of this using `npx @biomejs/biome check --write --unsafe` when I have the time * chore: add noDuplicateAtImportRules add noDuplicateAtImportRules * chore: remove dup import in dialog.test.tsx remove dup import in dialog.test.tsx --- src/frontend/.biomeignore | 1 + src/frontend/biome.json | 3 +++ .../__tests__/primaryInputIdentification.test.ts | 2 +- .../components/RenderInputParameters/index.tsx | 10 +++++----- .../components/RenderInputParameters/utils.ts | 2 +- .../CanvasControlsDropdown.tsx | 4 ++-- .../hooks/useModelConnectionLogic.ts | 2 +- .../components/sortableListComponent/index.tsx | 2 +- .../reproduce_issue.test.tsx | 4 ++-- .../chat-input/components/text-area-wrapper.tsx | 2 +- .../src/components/ui/__tests__/dialog.test.tsx | 7 ++----- .../API/queries/flow-version/index.ts | 2 +- .../queries/models/use-update-enabled-models.ts | 2 +- .../API/queries/nodes/use-post-template-value.ts | 3 ++- .../src/customization/utils/custom-mcp-url.ts | 2 +- .../flows/__tests__/use-restore-version.test.ts | 2 +- .../src/hooks/flows/use-apply-flow-to-canvas.ts | 2 +- src/frontend/src/icons/eagerIconImports.ts | 2 +- .../exportModal/__tests__/ExportModal.spec.tsx | 3 +-- .../__tests__/EditableHeaderContent.test.tsx | 11 ++++++++--- .../__tests__/InspectionPanel.test.tsx | 2 +- .../__tests__/InspectionPanelEditField.test.tsx | 4 ++-- .../__tests__/InspectionPanelFields.test.tsx | 2 +- .../__tests__/InspectionPanelHeader.test.tsx | 2 +- .../components/EditableHeaderContent.tsx | 2 +- .../components/InspectionPanelEditField.tsx | 8 ++++---- .../components/InspectionPanelField.tsx | 16 ++++++++-------- .../components/InspectionPanelHeader.tsx | 4 ++-- .../components/InspectionPanelOutputs.tsx | 4 ++-- .../components/InspectionPanel/index.tsx | 6 +++--- .../FlowPage/components/PageComponent/index.tsx | 4 ++-- .../use-flow-version-sidebar.ts | 2 +- .../FlowVersionSidebarContent-store.spec.tsx | 2 +- .../helpers/__tests__/disable-item.test.ts | 2 +- .../__tests__/get-disabled-tooltip.test.ts | 2 +- .../nodeToolbarComponent/hooks/use-shortcuts.ts | 2 +- .../components/McpAutoInstallContent.tsx | 2 +- .../pages/homePage/components/McpServerTab.tsx | 3 +-- .../__tests__/McpAutoInstallContent.test.tsx | 2 +- .../src/utils/__tests__/buildUtils.test.ts | 6 +++--- .../Custom Component Generator.spec.ts | 2 +- .../integrations/Instagram Copywriter.spec.ts | 4 ++-- .../tests/core/integrations/Vector Store.spec.ts | 2 +- .../tests/core/integrations/decisionFlow.spec.ts | 4 ++-- .../general-bugs-invalid-json-upload.spec.ts | 2 +- .../session-deletion-data-leakage.spec.ts | 2 +- .../tests/extended/features/lock-flow.spec.ts | 2 +- .../regression/general-bugs-shard-3836.spec.ts | 2 +- src/frontend/tests/utils/initialGPTsetup.ts | 4 ++-- 49 files changed, 87 insertions(+), 82 deletions(-) diff --git a/src/frontend/.biomeignore b/src/frontend/.biomeignore index 525503b5b7ef..79808347b41a 100644 --- a/src/frontend/.biomeignore +++ b/src/frontend/.biomeignore @@ -6,6 +6,7 @@ dist/ node_modules/ # Test outputs +.nyc_output/ coverage/ test-results/ playwright-report/ diff --git a/src/frontend/biome.json b/src/frontend/biome.json index e7a05d5985b6..2fe767bdc41f 100644 --- a/src/frontend/biome.json +++ b/src/frontend/biome.json @@ -45,6 +45,8 @@ "noUnreachable": "error", "noUnreachableSuper": "error", "noUnsafeFinally": "error", + "noUnusedImports": "error", + "noUnusedLabels": "error", "noUnusedPrivateClassMembers": "error", "noUnusedVariables": "warn", @@ -71,6 +73,7 @@ "noDuplicateCase": "error", "noDuplicateClassMembers": "error", "noDuplicateElseIf": "error", + "noDuplicateAtImportRules": "error", "noDuplicateObjectKeys": "error", "noDuplicateParameters": "error", "noExplicitAny": "error", diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/__tests__/primaryInputIdentification.test.ts b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/__tests__/primaryInputIdentification.test.ts index de2a9ad1ffbc..b0329ac3956b 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/__tests__/primaryInputIdentification.test.ts +++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/__tests__/primaryInputIdentification.test.ts @@ -1,5 +1,5 @@ -import { scapedJSONStringfy } from "@/utils/reactflowUtils"; import type { Edge } from "@xyflow/react"; +import { scapedJSONStringfy } from "@/utils/reactflowUtils"; import { findPrimaryInput } from "../utils"; // Helper to create a mock edge for testing diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx index 243eae87e617..c3dcc5ea1217 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx @@ -1,16 +1,16 @@ import { useMemo } from "react"; import { getNodeInputColors } from "@/CustomNodes/helpers/get-node-input-colors"; import { getNodeInputColorsName } from "@/CustomNodes/helpers/get-node-input-colors-name"; -import { sortToolModeFields } from "@/CustomNodes/helpers/sort-tool-mode-field"; -import getFieldTitle from "@/CustomNodes/utils/get-field-title"; -import { scapedJSONStringfy } from "@/utils/reactflowUtils"; -import NodeInputField from "../NodeInputField"; -import { findPrimaryInput } from "./utils"; import { isCanvasVisible, isInternalField, } from "@/CustomNodes/helpers/parameter-filtering"; +import { sortToolModeFields } from "@/CustomNodes/helpers/sort-tool-mode-field"; +import getFieldTitle from "@/CustomNodes/utils/get-field-title"; import useFlowStore from "@/stores/flowStore"; +import { scapedJSONStringfy } from "@/utils/reactflowUtils"; +import NodeInputField from "../NodeInputField"; +import { findPrimaryInput } from "./utils"; const RenderInputParameters = ({ data, diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/utils.ts b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/utils.ts index 9141de7af4f5..3816af36bab8 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/utils.ts +++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/utils.ts @@ -1,6 +1,6 @@ +import type { Edge } from "@xyflow/react"; import { LANGFLOW_SUPPORTED_TYPES } from "@/constants/constants"; import { scapedJSONStringfy } from "@/utils/reactflowUtils"; -import type { Edge } from "@xyflow/react"; export type DisplayHandleTemplate = { type?: string; diff --git a/src/frontend/src/components/core/canvasControlsComponent/CanvasControlsDropdown.tsx b/src/frontend/src/components/core/canvasControlsComponent/CanvasControlsDropdown.tsx index 9f80214e6843..8c8da8930d3f 100644 --- a/src/frontend/src/components/core/canvasControlsComponent/CanvasControlsDropdown.tsx +++ b/src/frontend/src/components/core/canvasControlsComponent/CanvasControlsDropdown.tsx @@ -9,10 +9,10 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Separator } from "@/components/ui/separator"; -import DropdownControlButton from "./DropdownControlButton"; -import { formatZoomPercentage, reactFlowSelector } from "./utils/canvasUtils"; import useFlowStore from "@/stores/flowStore"; import { AllNodeType } from "@/types/flow"; +import DropdownControlButton from "./DropdownControlButton"; +import { formatZoomPercentage, reactFlowSelector } from "./utils/canvasUtils"; export const KEYBOARD_SHORTCUTS = { ZOOM_IN: { key: "+", code: "Equal" }, diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/modelInputComponent/hooks/useModelConnectionLogic.ts b/src/frontend/src/components/core/parameterRenderComponent/components/modelInputComponent/hooks/useModelConnectionLogic.ts index a059b39ae93f..a197d2f9ba4c 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/modelInputComponent/hooks/useModelConnectionLogic.ts +++ b/src/frontend/src/components/core/parameterRenderComponent/components/modelInputComponent/hooks/useModelConnectionLogic.ts @@ -1,9 +1,9 @@ +import { useCallback } from "react"; import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template"; import useFlowStore from "@/stores/flowStore"; import { useTypesStore } from "@/stores/typesStore"; import { scapedJSONStringfy } from "@/utils/reactflowUtils"; import { groupByFamily } from "@/utils/utils"; -import { useCallback } from "react"; interface UseModelConnectionLogicProps { nodeId: string; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx index 879bd237b138..5d2977bc0d0f 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx @@ -1,5 +1,5 @@ -import { memo, useCallback, useEffect, useMemo, useState } from "react"; import { isEqual } from "lodash"; +import { memo, useCallback, useEffect, useMemo, useState } from "react"; import { ReactSortable } from "react-sortablejs"; import ListSelectionComponent from "@/CustomNodes/GenericNode/components/ListSelectionComponent"; import ForwardedIconComponent from "@/components/common/genericIconComponent"; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/reproduce_issue.test.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/reproduce_issue.test.tsx index 9189abda2a55..7945b5ff2e68 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/reproduce_issue.test.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/reproduce_issue.test.tsx @@ -1,7 +1,7 @@ -import React from "react"; import { render, waitFor } from "@testing-library/react"; -import SortableListComponent from "./index"; +import React from "react"; import * as ReactSortableModule from "react-sortablejs"; +import SortableListComponent from "./index"; // Mock ListSelectionComponent to avoid its complexity jest.mock("@/CustomNodes/GenericNode/components/ListSelectionComponent", () => { diff --git a/src/frontend/src/components/core/playgroundComponent/chat-view/chat-input/components/text-area-wrapper.tsx b/src/frontend/src/components/core/playgroundComponent/chat-view/chat-input/components/text-area-wrapper.tsx index 581304922109..44717015ceff 100644 --- a/src/frontend/src/components/core/playgroundComponent/chat-view/chat-input/components/text-area-wrapper.tsx +++ b/src/frontend/src/components/core/playgroundComponent/chat-view/chat-input/components/text-area-wrapper.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef } from "react"; import { - CHAT_INPUT_MIN_HEIGHT, CHAT_INPUT_MAX_HEIGHT, + CHAT_INPUT_MIN_HEIGHT, } from "@/constants/constants"; import { useUtilityStore } from "@/stores/utilityStore"; import type { FilePreviewType } from "@/types/components"; diff --git a/src/frontend/src/components/ui/__tests__/dialog.test.tsx b/src/frontend/src/components/ui/__tests__/dialog.test.tsx index 133848fca3be..551d52d9c486 100644 --- a/src/frontend/src/components/ui/__tests__/dialog.test.tsx +++ b/src/frontend/src/components/ui/__tests__/dialog.test.tsx @@ -1,10 +1,11 @@ import { render, screen } from "@testing-library/react"; +import type { ReactElement } from "react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Dialog, DialogContent, - DialogTitle, DialogDescription, + DialogTitle, } from "../dialog"; // Mock genericIconComponent (already globally mocked, but be explicit) @@ -13,10 +14,6 @@ jest.mock("@/components/common/genericIconComponent", () => ({ default: () => null, })); -import type { ReactElement } from "react"; -import { render, screen } from "@testing-library/react"; -import { TooltipProvider } from "@/components/ui/tooltip"; - const renderWithProviders = (ui: ReactElement) => { return render({ui}); }; diff --git a/src/frontend/src/controllers/API/queries/flow-version/index.ts b/src/frontend/src/controllers/API/queries/flow-version/index.ts index b72cc51c0ce9..44993acb861c 100644 --- a/src/frontend/src/controllers/API/queries/flow-version/index.ts +++ b/src/frontend/src/controllers/API/queries/flow-version/index.ts @@ -1,4 +1,4 @@ export * from "./use-delete-version-entry"; -export * from "./use-get-flow-versions"; export * from "./use-get-flow-version-entry"; +export * from "./use-get-flow-versions"; export * from "./use-post-create-snapshot"; diff --git a/src/frontend/src/controllers/API/queries/models/use-update-enabled-models.ts b/src/frontend/src/controllers/API/queries/models/use-update-enabled-models.ts index 304cf6f55e5b..79c661a1e598 100644 --- a/src/frontend/src/controllers/API/queries/models/use-update-enabled-models.ts +++ b/src/frontend/src/controllers/API/queries/models/use-update-enabled-models.ts @@ -1,7 +1,7 @@ +import { UseMutationResult } from "@tanstack/react-query"; import { useMutationFunctionType } from "@/types/api"; import { api } from "../../api"; import { getURL } from "../../helpers/constants"; -import { UseMutationResult } from "@tanstack/react-query"; import { UseRequestProcessor } from "../../services/request-processor"; export interface ModelStatusUpdate { diff --git a/src/frontend/src/controllers/API/queries/nodes/use-post-template-value.ts b/src/frontend/src/controllers/API/queries/nodes/use-post-template-value.ts index 7801f8c78b56..c3457762a4ba 100644 --- a/src/frontend/src/controllers/API/queries/nodes/use-post-template-value.ts +++ b/src/frontend/src/controllers/API/queries/nodes/use-post-template-value.ts @@ -1,5 +1,6 @@ import type { UseMutationResult } from "@tanstack/react-query"; import useFlowStore from "@/stores/flowStore"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; import type { APIClassType, ResponseErrorDetailAPI, @@ -8,7 +9,7 @@ import type { import { api } from "../../api"; import { getURL } from "../../helpers/constants"; import { UseRequestProcessor } from "../../services/request-processor"; -import useFlowsManagerStore from "@/stores/flowsManagerStore"; + interface IPostTemplateValue { value: any; tool_mode?: boolean; diff --git a/src/frontend/src/customization/utils/custom-mcp-url.ts b/src/frontend/src/customization/utils/custom-mcp-url.ts index 5324337ba4e2..a0ede5f69a5c 100644 --- a/src/frontend/src/customization/utils/custom-mcp-url.ts +++ b/src/frontend/src/customization/utils/custom-mcp-url.ts @@ -1,5 +1,5 @@ -import type { MCPTransport } from "@/controllers/API/queries/mcp/use-patch-install-mcp"; import { api } from "@/controllers/API/api"; +import type { MCPTransport } from "@/controllers/API/queries/mcp/use-patch-install-mcp"; type ComposerConnectionOptions = { useComposer?: boolean; diff --git a/src/frontend/src/hooks/flows/__tests__/use-restore-version.test.ts b/src/frontend/src/hooks/flows/__tests__/use-restore-version.test.ts index a00dc9c7dbe1..9c59d10d80f2 100644 --- a/src/frontend/src/hooks/flows/__tests__/use-restore-version.test.ts +++ b/src/frontend/src/hooks/flows/__tests__/use-restore-version.test.ts @@ -1,4 +1,4 @@ -import { renderHook, act } from "@testing-library/react"; +import { act, renderHook } from "@testing-library/react"; // --------------------------------------------------------------------------- // Mocks — hoisted before imports diff --git a/src/frontend/src/hooks/flows/use-apply-flow-to-canvas.ts b/src/frontend/src/hooks/flows/use-apply-flow-to-canvas.ts index 2caa44172485..1c85b5af974c 100644 --- a/src/frontend/src/hooks/flows/use-apply-flow-to-canvas.ts +++ b/src/frontend/src/hooks/flows/use-apply-flow-to-canvas.ts @@ -1,8 +1,8 @@ import { cloneDeep } from "lodash"; import { useCallback } from "react"; import { useRefreshModelInputs } from "@/hooks/use-refresh-model-inputs"; -import useFlowsManagerStore from "@/stores/flowsManagerStore"; import useFlowStore from "@/stores/flowStore"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; import type { FlowType } from "@/types/flow"; import { processFlows } from "@/utils/reactflowUtils"; diff --git a/src/frontend/src/icons/eagerIconImports.ts b/src/frontend/src/icons/eagerIconImports.ts index be0935eea500..4329f4bc351d 100644 --- a/src/frontend/src/icons/eagerIconImports.ts +++ b/src/frontend/src/icons/eagerIconImports.ts @@ -115,9 +115,9 @@ import { WolframIcon } from "@/icons/Wolfram"; import { XAIIcon } from "@/icons/xAI"; import { YoutubeIcon as YouTubeIcon } from "@/icons/Youtube"; import { ZepMemoryIcon } from "@/icons/ZepMemory"; +import { AgenticsIcon } from "./Agentics"; import { JigsawStackIcon } from "./JigsawStack"; import { WindsurfIcon } from "./Windsurf"; -import { AgenticsIcon } from "./Agentics"; // Export the eagerly loaded icons map export const eagerIconsMapping = { diff --git a/src/frontend/src/modals/exportModal/__tests__/ExportModal.spec.tsx b/src/frontend/src/modals/exportModal/__tests__/ExportModal.spec.tsx index e097acc3d9f9..0586e23e7c8d 100644 --- a/src/frontend/src/modals/exportModal/__tests__/ExportModal.spec.tsx +++ b/src/frontend/src/modals/exportModal/__tests__/ExportModal.spec.tsx @@ -1,8 +1,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; - -import type { FlowType } from "@/types/flow"; import { TooltipProvider } from "@/components/ui/tooltip"; +import type { FlowType } from "@/types/flow"; const downloadFlowMock = jest.fn(); const removeApiKeysMock = jest.fn((flow: any) => flow); diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/EditableHeaderContent.test.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/EditableHeaderContent.test.tsx index 34b4037f0f8f..5c66a8d14352 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/EditableHeaderContent.test.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/EditableHeaderContent.test.tsx @@ -1,8 +1,13 @@ -import { render, screen, waitFor, fireEvent } from "@testing-library/react"; +import { + fireEvent, + render, + renderHook, + screen, + waitFor, +} from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { renderHook } from "@testing-library/react"; -import EditableHeaderContent from "../components/EditableHeaderContent"; import type { NodeDataType } from "@/types/flow"; +import EditableHeaderContent from "../components/EditableHeaderContent"; // Mock Markdown component jest.mock("react-markdown", () => { diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanel.test.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanel.test.tsx index d9971267f267..3e2dc291ffab 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanel.test.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanel.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import InspectionPanel from "../index"; import type { AllNodeType } from "@/types/flow"; +import InspectionPanel from "../index"; // Mock framer-motion jest.mock("framer-motion", () => ({ diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelEditField.test.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelEditField.test.tsx index f767fbabbd1b..c8288813d2cb 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelEditField.test.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelEditField.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import InspectionPanelEditField from "../components/InspectionPanelEditField"; -import type { NodeDataType } from "@/types/flow"; import { TooltipProvider } from "@/components/ui/tooltip"; +import type { NodeDataType } from "@/types/flow"; +import InspectionPanelEditField from "../components/InspectionPanelEditField"; // Mock IconComponent jest.mock("@/components/common/genericIconComponent", () => { diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelFields.test.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelFields.test.tsx index 31cf14e881f2..81c29a077294 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelFields.test.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelFields.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from "@testing-library/react"; -import InspectionPanelFields from "../components/InspectionPanelFields"; import type { NodeDataType } from "@/types/flow"; +import InspectionPanelFields from "../components/InspectionPanelFields"; // Mock getFieldTitle jest.mock("@/CustomNodes/utils/get-field-title", () => ({ diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelHeader.test.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelHeader.test.tsx index c0b597483eff..8055bc1b5eb1 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelHeader.test.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/__tests__/InspectionPanelHeader.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import InspectionPanelHeader from "../components/InspectionPanelHeader"; import type { NodeDataType } from "@/types/flow"; +import InspectionPanelHeader from "../components/InspectionPanelHeader"; // Mock EditableHeaderContent const mockHandleSave = jest.fn(); diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/EditableHeaderContent.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/EditableHeaderContent.tsx index ad129bc085de..e79809391f15 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/EditableHeaderContent.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/EditableHeaderContent.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useCallback, memo, useMemo } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import Markdown from "react-markdown"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelEditField.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelEditField.tsx index 8f72bbaad51d..dbcb57410d68 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelEditField.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelEditField.tsx @@ -1,11 +1,11 @@ import { useCallback } from "react"; -import IconComponent from "@/components/common/genericIconComponent"; import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value"; -import type { NodeDataType } from "@/types/flow"; -import { cn } from "@/utils/utils"; +import IconComponent from "@/components/common/genericIconComponent"; +import ShadTooltip from "@/components/common/shadTooltipComponent"; import useFlowStore from "@/stores/flowStore"; +import type { NodeDataType } from "@/types/flow"; import { scapeJSONParse } from "@/utils/reactflowUtils"; -import ShadTooltip from "@/components/common/shadTooltipComponent"; +import { cn } from "@/utils/utils"; interface InspectionPanelEditFieldProps { data: NodeDataType; diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelField.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelField.tsx index c04fe2cd3569..e9002daef463 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelField.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelField.tsx @@ -1,7 +1,15 @@ import { useCallback, useMemo } from "react"; +import NodeInputInfo from "@/CustomNodes/GenericNode/components/NodeInputInfo"; +import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value"; +import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class"; import { AssistantButton } from "@/components/common/assistant"; import IconComponent from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; +import { + DEFAULT_TOOLSET_PLACEHOLDER, + FLEX_VIEW_TYPES, + ICON_STROKE_WIDTH, +} from "@/constants/constants"; import { CustomParameterComponent, CustomParameterLabel, @@ -13,14 +21,6 @@ import useAuthStore from "@/stores/authStore"; import useFlowStore from "@/stores/flowStore"; import type { NodeInputFieldComponentType } from "@/types/components"; import { cn } from "@/utils/utils"; -import { - DEFAULT_TOOLSET_PLACEHOLDER, - FLEX_VIEW_TYPES, - ICON_STROKE_WIDTH, -} from "@/constants/constants"; -import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class"; -import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value"; -import NodeInputInfo from "@/CustomNodes/GenericNode/components/NodeInputInfo"; interface InspectionPanelFieldProps extends Omit< diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelHeader.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelHeader.tsx index 34710610f569..38f7178c125b 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelHeader.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelHeader.tsx @@ -1,10 +1,12 @@ import { useCallback, useMemo, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value"; import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class"; import ForwardedIconComponent from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { ICON_STROKE_WIDTH } from "@/constants/constants"; import { customOpenNewTab } from "@/customization/utils/custom-open-new-tab"; import CodeAreaModal from "@/modals/codeAreaModal"; import useAlertStore from "@/stores/alertStore"; @@ -13,8 +15,6 @@ import type { NodeDataType } from "@/types/flow"; import { cn } from "@/utils/utils"; import { ToolbarButton } from "../../nodeToolbarComponent/components/toolbar-button"; import EditableHeaderContent from "./EditableHeaderContent"; -import { ICON_STROKE_WIDTH } from "@/constants/constants"; -import { useHotkeys } from "react-hotkeys-hook"; interface InspectionPanelHeaderProps { data: NodeDataType; diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelOutputs.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelOutputs.tsx index 2934f2024247..55551c864e62 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelOutputs.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/components/InspectionPanelOutputs.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; -import type { NodeDataType } from "@/types/flow"; -import useFlowStore from "@/stores/flowStore"; import SwitchOutputView from "@/CustomNodes/GenericNode/components/outputModal/components/switchOutputView"; +import useFlowStore from "@/stores/flowStore"; +import type { NodeDataType } from "@/types/flow"; import { getGroupOutputNodeId } from "@/utils/reactflowUtils"; interface InspectionPanelOutputsProps { diff --git a/src/frontend/src/pages/FlowPage/components/InspectionPanel/index.tsx b/src/frontend/src/pages/FlowPage/components/InspectionPanel/index.tsx index e3b6a33f9689..92c21ef75495 100644 --- a/src/frontend/src/pages/FlowPage/components/InspectionPanel/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/InspectionPanel/index.tsx @@ -1,11 +1,11 @@ import { Panel } from "@xyflow/react"; -import { motion, AnimatePresence } from "framer-motion"; -import { memo, useState, useEffect } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { memo, useEffect, useState } from "react"; +import { Separator } from "@/components/ui/separator"; import type { AllNodeType } from "@/types/flow"; import { cn } from "@/utils/utils"; import InspectionPanelFields from "./components/InspectionPanelFields"; import InspectionPanelHeader from "./components/InspectionPanelHeader"; -import { Separator } from "@/components/ui/separator"; interface InspectionPanelProps { selectedNode: AllNodeType | null; diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 1f29b1277a70..06b1a9379d53 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -44,9 +44,9 @@ import ExportModal from "../../../../modals/exportModal"; import useAlertStore from "../../../../stores/alertStore"; import useFlowStore from "../../../../stores/flowStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; -import useVersionPreviewStore from "../../../../stores/versionPreviewStore"; import { useShortcutsStore } from "../../../../stores/shortcuts"; import { useTypesStore } from "../../../../stores/typesStore"; +import useVersionPreviewStore from "../../../../stores/versionPreviewStore"; import type { APIClassType } from "../../../../types/api"; import type { AllNodeType, @@ -67,8 +67,8 @@ import ConnectionLineComponent from "../ConnectionLineComponent"; import FlowBuildingComponent from "../flowBuildingComponent"; import SelectionMenu from "../SelectionMenuComponent"; import UpdateAllComponents from "../UpdateAllComponents"; -import VersionPreviewOverlay from "./components/VersionPreviewOverlay"; import HelperLines from "./components/helper-lines"; +import VersionPreviewOverlay from "./components/VersionPreviewOverlay"; import { getHelperLines, getSnapPosition, diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/FlowVersionSidebar/use-flow-version-sidebar.ts b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/FlowVersionSidebar/use-flow-version-sidebar.ts index 5f6fe3f0d6d4..25fe21386baf 100644 --- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/FlowVersionSidebar/use-flow-version-sidebar.ts +++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/FlowVersionSidebar/use-flow-version-sidebar.ts @@ -11,8 +11,8 @@ import { api } from "@/controllers/API/api"; import { getURL } from "@/controllers/API/helpers/constants"; import { useDeleteVersionEntry, - useGetFlowVersions, useGetFlowVersionEntry, + useGetFlowVersions, } from "@/controllers/API/queries/flow-version"; import useAlertStore from "@/stores/alertStore"; import useFlowStore from "@/stores/flowStore"; diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/FlowVersionSidebarContent-store.spec.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/FlowVersionSidebarContent-store.spec.tsx index e2b5a39ec7c1..b8bb89090cd1 100644 --- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/FlowVersionSidebarContent-store.spec.tsx +++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/FlowVersionSidebarContent-store.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, act } from "@testing-library/react"; +import { act, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; // --------------------------------------------------------------------------- diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/__tests__/disable-item.test.ts b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/__tests__/disable-item.test.ts index 13e7e623be9a..d18baca09633 100644 --- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/__tests__/disable-item.test.ts +++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/__tests__/disable-item.test.ts @@ -1,5 +1,5 @@ -import { disableItem } from "../disable-item"; import type { UniqueInputsComponents } from "../../types"; +import { disableItem } from "../disable-item"; describe("disableItem", () => { describe("ChatInput component", () => { diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/__tests__/get-disabled-tooltip.test.ts b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/__tests__/get-disabled-tooltip.test.ts index 660e0ec24b1f..3fc10c75ab14 100644 --- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/__tests__/get-disabled-tooltip.test.ts +++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/__tests__/get-disabled-tooltip.test.ts @@ -1,5 +1,5 @@ -import { getDisabledTooltip } from "../get-disabled-tooltip"; import type { UniqueInputsComponents } from "../../types"; +import { getDisabledTooltip } from "../get-disabled-tooltip"; describe("getDisabledTooltip", () => { describe("ChatInput component", () => { diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/hooks/use-shortcuts.ts b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/hooks/use-shortcuts.ts index b1e045118ef3..6839b65bafb6 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/hooks/use-shortcuts.ts +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/hooks/use-shortcuts.ts @@ -1,7 +1,7 @@ import { useHotkeys } from "react-hotkeys-hook"; +import useFlowStore from "@/stores/flowStore"; import { useShortcutsStore } from "@/stores/shortcuts"; import isWrappedWithClass from "../../PageComponent/utils/is-wrapped-with-class"; -import useFlowStore from "@/stores/flowStore"; export default function useShortcuts({ showOverrideModal, diff --git a/src/frontend/src/pages/MainPage/pages/homePage/components/McpAutoInstallContent.tsx b/src/frontend/src/pages/MainPage/pages/homePage/components/McpAutoInstallContent.tsx index 161838f323a3..52f26cd0aaa2 100644 --- a/src/frontend/src/pages/MainPage/pages/homePage/components/McpAutoInstallContent.tsx +++ b/src/frontend/src/pages/MainPage/pages/homePage/components/McpAutoInstallContent.tsx @@ -1,9 +1,9 @@ import { ForwardedIconComponent } from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; import { Button } from "@/components/ui/button"; +import type { MCPTransport } from "@/controllers/API/queries/mcp/use-patch-install-mcp"; import { toSpaceCase } from "@/utils/stringManipulation"; import { cn } from "@/utils/utils"; -import type { MCPTransport } from "@/controllers/API/queries/mcp/use-patch-install-mcp"; import { autoInstallers } from "../utils/mcpServerUtils"; interface McpAutoInstallContentProps { diff --git a/src/frontend/src/pages/MainPage/pages/homePage/components/McpServerTab.tsx b/src/frontend/src/pages/MainPage/pages/homePage/components/McpServerTab.tsx index b169e69e0a98..28967152df1a 100644 --- a/src/frontend/src/pages/MainPage/pages/homePage/components/McpServerTab.tsx +++ b/src/frontend/src/pages/MainPage/pages/homePage/components/McpServerTab.tsx @@ -3,12 +3,11 @@ import { useParams } from "react-router-dom"; import { ForwardedIconComponent } from "@/components/common/genericIconComponent"; import { Button } from "@/components/ui/button"; +import type { MCPTransport } from "@/controllers/API/queries/mcp/use-patch-install-mcp"; import { ENABLE_MCP_COMPOSER } from "@/customization/feature-flags"; import { useCustomIsLocalConnection } from "@/customization/hooks/use-custom-is-local-connection"; import useTheme from "@/customization/hooks/use-custom-theme"; import AuthModal from "@/modals/authModal"; - -import type { MCPTransport } from "@/controllers/API/queries/mcp/use-patch-install-mcp"; import { useFolderStore } from "@/stores/foldersStore"; import { cn, getOS } from "@/utils/utils"; import { useMcpServer } from "../hooks/useMcpServer"; diff --git a/src/frontend/src/pages/MainPage/pages/homePage/components/__tests__/McpAutoInstallContent.test.tsx b/src/frontend/src/pages/MainPage/pages/homePage/components/__tests__/McpAutoInstallContent.test.tsx index 860c05486d4d..ffbf3073a4f4 100644 --- a/src/frontend/src/pages/MainPage/pages/homePage/components/__tests__/McpAutoInstallContent.test.tsx +++ b/src/frontend/src/pages/MainPage/pages/homePage/components/__tests__/McpAutoInstallContent.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import { McpAutoInstallContent } from "../McpAutoInstallContent"; import { autoInstallers } from "../../utils/mcpServerUtils"; +import { McpAutoInstallContent } from "../McpAutoInstallContent"; jest.mock("@/components/common/genericIconComponent", () => ({ ForwardedIconComponent: ({ name }: { name: string }) => {name}, diff --git a/src/frontend/src/utils/__tests__/buildUtils.test.ts b/src/frontend/src/utils/__tests__/buildUtils.test.ts index 4d9a80d07845..dd2fd0e3017b 100644 --- a/src/frontend/src/utils/__tests__/buildUtils.test.ts +++ b/src/frontend/src/utils/__tests__/buildUtils.test.ts @@ -102,10 +102,10 @@ jest.mock("@/controllers/API", () => ({ })); import { - processEndVertexEvent, - processBatchedEvents, - BATCHABLE_EVENTS, BATCH_YIELD_MS, + BATCHABLE_EVENTS, + processBatchedEvents, + processEndVertexEvent, } from "../buildUtils"; // --- Helpers --- diff --git a/src/frontend/tests/core/integrations/Custom Component Generator.spec.ts b/src/frontend/tests/core/integrations/Custom Component Generator.spec.ts index 34624bc1d468..aed2d1c19408 100644 --- a/src/frontend/tests/core/integrations/Custom Component Generator.spec.ts +++ b/src/frontend/tests/core/integrations/Custom Component Generator.spec.ts @@ -3,8 +3,8 @@ import path from "path"; import { expect, test } from "../../fixtures"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; import { getAllResponseMessage } from "../../utils/get-all-response-message"; -import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes"; import { selectAnthropicModel } from "../../utils/select-anthropic-model"; +import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes"; withEventDeliveryModes( "Custom Component Generator", diff --git a/src/frontend/tests/core/integrations/Instagram Copywriter.spec.ts b/src/frontend/tests/core/integrations/Instagram Copywriter.spec.ts index e6ec46b8a3f6..6595dae9db48 100644 --- a/src/frontend/tests/core/integrations/Instagram Copywriter.spec.ts +++ b/src/frontend/tests/core/integrations/Instagram Copywriter.spec.ts @@ -1,12 +1,12 @@ import * as dotenv from "dotenv"; import path from "path"; import { expect, test } from "../../fixtures"; +import { adjustScreenView } from "../../utils/adjust-screen-view"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; import { getAllResponseMessage } from "../../utils/get-all-response-message"; import { initialGPTsetup } from "../../utils/initialGPTsetup"; -import { waitForOpenModalWithChatInput } from "../../utils/wait-for-open-modal"; import { unselectNodes } from "../../utils/unselect-nodes"; -import { adjustScreenView } from "../../utils/adjust-screen-view"; +import { waitForOpenModalWithChatInput } from "../../utils/wait-for-open-modal"; test( "Instagram Copywriter", diff --git a/src/frontend/tests/core/integrations/Vector Store.spec.ts b/src/frontend/tests/core/integrations/Vector Store.spec.ts index 834114874fe0..c8e023949a46 100644 --- a/src/frontend/tests/core/integrations/Vector Store.spec.ts +++ b/src/frontend/tests/core/integrations/Vector Store.spec.ts @@ -3,8 +3,8 @@ import { expect, test } from "../../fixtures"; import { adjustScreenView } from "../../utils/adjust-screen-view"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; import { initialGPTsetup } from "../../utils/initialGPTsetup"; -import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes"; import { disableInspectPanel } from "../../utils/open-advanced-options"; +import { withEventDeliveryModes } from "../../utils/withEventDeliveryModes"; // Add this line to declare Node.js global variables declare const process: any; diff --git a/src/frontend/tests/core/integrations/decisionFlow.spec.ts b/src/frontend/tests/core/integrations/decisionFlow.spec.ts index 4edd685c465a..6908f9a251d5 100644 --- a/src/frontend/tests/core/integrations/decisionFlow.spec.ts +++ b/src/frontend/tests/core/integrations/decisionFlow.spec.ts @@ -4,12 +4,12 @@ import { test } from "../../fixtures"; import { addLegacyComponents } from "../../utils/add-legacy-components"; import { adjustScreenView } from "../../utils/adjust-screen-view"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; -import { zoomOut } from "../../utils/zoom-out"; -import { selectGptModel } from "../../utils/select-gpt-model"; import { closeAdvancedOptions, openAdvancedOptions, } from "../../utils/open-advanced-options"; +import { selectGptModel } from "../../utils/select-gpt-model"; +import { zoomOut } from "../../utils/zoom-out"; test( "should create a flow with decision", diff --git a/src/frontend/tests/core/regression/general-bugs-invalid-json-upload.spec.ts b/src/frontend/tests/core/regression/general-bugs-invalid-json-upload.spec.ts index 384e30867791..e9095568e3e4 100644 --- a/src/frontend/tests/core/regression/general-bugs-invalid-json-upload.spec.ts +++ b/src/frontend/tests/core/regression/general-bugs-invalid-json-upload.spec.ts @@ -1,6 +1,6 @@ +import type { Page } from "@playwright/test"; import { expect, test } from "../../fixtures"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; -import type { Page } from "@playwright/test"; test.describe("Invalid JSON Upload Error Handling", () => { // Helper function to verify error appears diff --git a/src/frontend/tests/core/regression/session-deletion-data-leakage.spec.ts b/src/frontend/tests/core/regression/session-deletion-data-leakage.spec.ts index 3887fb939447..b84b500a6f70 100644 --- a/src/frontend/tests/core/regression/session-deletion-data-leakage.spec.ts +++ b/src/frontend/tests/core/regression/session-deletion-data-leakage.spec.ts @@ -1,7 +1,7 @@ +import type { Page } from "@playwright/test"; import { expect, test } from "../../fixtures"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; import { initialGPTsetup } from "../../utils/initialGPTsetup"; -import type { Page } from "@playwright/test"; test.describe("Session Deletion Data Leakage Fix", () => { // Helper to send a message in the playground diff --git a/src/frontend/tests/extended/features/lock-flow.spec.ts b/src/frontend/tests/extended/features/lock-flow.spec.ts index 28637a5792b2..0850104885b7 100644 --- a/src/frontend/tests/extended/features/lock-flow.spec.ts +++ b/src/frontend/tests/extended/features/lock-flow.spec.ts @@ -2,10 +2,10 @@ import type { Page } from "@playwright/test"; import * as dotenv from "dotenv"; import path from "path"; import { expect, test } from "../../fixtures"; +import { adjustScreenView } from "../../utils/adjust-screen-view"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; import { lockFlow, unlockFlow } from "../../utils/lock-flow"; import { unselectNodes } from "../../utils/unselect-nodes"; -import { adjustScreenView } from "../../utils/adjust-screen-view"; test( "user must be able to lock a flow and it must be saved", diff --git a/src/frontend/tests/extended/regression/general-bugs-shard-3836.spec.ts b/src/frontend/tests/extended/regression/general-bugs-shard-3836.spec.ts index ae22b1c37af6..3bbb93f27956 100644 --- a/src/frontend/tests/extended/regression/general-bugs-shard-3836.spec.ts +++ b/src/frontend/tests/extended/regression/general-bugs-shard-3836.spec.ts @@ -3,11 +3,11 @@ import path from "path"; import { expect, test } from "../../fixtures"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; import { initialGPTsetup } from "../../utils/initialGPTsetup"; -import { uploadFile } from "../../utils/upload-file"; import { closeAdvancedOptions, openAdvancedOptions, } from "../../utils/open-advanced-options"; +import { uploadFile } from "../../utils/upload-file"; test( "user must be able to send an image on chat using advanced tool on ChatInputComponent", diff --git a/src/frontend/tests/utils/initialGPTsetup.ts b/src/frontend/tests/utils/initialGPTsetup.ts index 650b2037351e..a421d52eb6da 100644 --- a/src/frontend/tests/utils/initialGPTsetup.ts +++ b/src/frontend/tests/utils/initialGPTsetup.ts @@ -1,9 +1,9 @@ import type { Page } from "@playwright/test"; +import { addOpenAiInputKey } from "./add-open-ai-input-key"; import { adjustScreenView } from "./adjust-screen-view"; import { selectGptModel } from "./select-gpt-model"; -import { updateOldComponents } from "./update-old-components"; -import { addOpenAiInputKey } from "./add-open-ai-input-key"; import { unselectNodes } from "./unselect-nodes"; +import { updateOldComponents } from "./update-old-components"; export async function initialGPTsetup( page: Page, From 170c98671407bcb318ffb9bf502773b953485a89 Mon Sep 17 00:00:00 2001 From: Mendon Kissling <59585235+mendonk@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:33:01 -0400 Subject: [PATCH 06/29] docs: square up inline icons CSS (#12159) * square-up-inline-icons * use-default-alignment * make-mcp-icon-use-currentColor * dont-rule-images * rules-for-svg --- docs/css/custom.css | 9 +++++++++ docs/static/logos/mcp-icon.svg | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/css/custom.css b/docs/css/custom.css index 9d2cb295f93d..999833c7e25b 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -481,3 +481,12 @@ body { color: var(--ifm-color-emphasis-500) !important; size: 16px !important; } + +/* Inline Icons */ +.markdown svg, +.markdown .icon { + width: 16px !important; + height: 16px !important; + stroke-width: 2.5 !important; + display: inline-block; +} \ No newline at end of file diff --git a/docs/static/logos/mcp-icon.svg b/docs/static/logos/mcp-icon.svg index ae83a5f2cfc2..807024ee6a36 100644 --- a/docs/static/logos/mcp-icon.svg +++ b/docs/static/logos/mcp-icon.svg @@ -1,7 +1,7 @@ - - + + From bc2da110e5c193da376398b531f4a0e06ad58275 Mon Sep 17 00:00:00 2001 From: vjgit96 Date: Thu, 12 Mar 2026 14:36:51 -0400 Subject: [PATCH 07/29] fix: gate PyPI publish jobs on CI completion in release workflow (#12168) fix: gate PyPI publish jobs on CI completion The publish-base, publish-main, and publish-lfx jobs were not waiting for the CI workflow to complete before publishing to PyPI. This could result in broken packages being published if CI fails. Docker builds already correctly depend on CI. Add 'ci' to the needs array of all three publish jobs so packages are only published after the full test suite passes. --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5a6e5d18d4c3..cd2dc5e2970c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -599,7 +599,7 @@ jobs: publish-base: name: Publish Langflow Base to PyPI if: ${{ inputs.release_package_base }} - needs: [build-base, test-cross-platform] + needs: [build-base, test-cross-platform, ci] runs-on: ubuntu-latest steps: - name: Download base artifact @@ -622,7 +622,7 @@ jobs: publish-main: name: Publish Langflow Main to PyPI if: ${{ inputs.release_package_main }} - needs: [build-main, test-cross-platform, publish-base] + needs: [build-main, test-cross-platform, publish-base, ci] runs-on: ubuntu-latest steps: - name: Download main artifact @@ -645,7 +645,7 @@ jobs: publish-lfx: name: Publish LFX to PyPI if: ${{ inputs.release_lfx }} - needs: [build-lfx, test-cross-platform] + needs: [build-lfx, test-cross-platform, ci] runs-on: ubuntu-latest steps: - name: Download LFX artifact From c4adc7d6d3504f90f46f27ca3bdf5ab25782e96e Mon Sep 17 00:00:00 2001 From: vjgit96 Date: Thu, 12 Mar 2026 15:53:57 -0400 Subject: [PATCH 08/29] fix: filter Docker Hub tag queries to prevent rc0 overwrite (LE-515) (#12173) --- .github/workflows/docker-build-v2.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build-v2.yml b/.github/workflows/docker-build-v2.yml index ab7344399660..f55a64dc5dee 100644 --- a/.github/workflows/docker-build-v2.yml +++ b/.github/workflows/docker-build-v2.yml @@ -82,12 +82,12 @@ jobs: echo "Base version from pyproject.toml: $version" if [ ${{inputs.pre_release}} == "true" ]; then - last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -E '\.rc[0-9]+' | sort -V | tail -n 1) + last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -E '^base-.*\.rc[0-9]+' | grep -vE '\-(amd64|arm64)$' | sed 's/^base-//' | sort -V | tail -n 1) version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")" echo "Latest base pre-release version: $last_released_version" echo "Base pre-release version to be released: $version" else - last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -v 'latest' | sort -V | tail -n 1) + last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -E '^base-' | grep -vE '\-(amd64|arm64)$' | grep -v 'latest' | sed 's/^base-//' | sort -V | tail -n 1) echo "Latest base release version: $last_released_version" fi @@ -128,12 +128,12 @@ jobs: echo "Main version from pyproject.toml: $version" if [ ${{inputs.pre_release}} == "true" ]; then - last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -E '\.rc[0-9]+' | sort -V | tail -n 1) + last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -E '\.rc[0-9]+' | grep -v '^base-' | grep -vE '\-(amd64|arm64)$' | sort -V | tail -n 1) version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")" echo "Latest main pre-release version: $last_released_version" echo "Main pre-release version to be released: $version" else - last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -v 'latest' | sort -V | tail -n 1) + last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -v '^base-' | grep -vE '\-(amd64|arm64)$' | grep -v 'latest' | sort -V | tail -n 1) echo "Latest main release version: $last_released_version" fi From 4334e0bd32795b5504dbcef9e3fac7a8984d9a6b Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Thu, 12 Mar 2026 13:21:04 -0700 Subject: [PATCH 09/29] fix: Cascading deletes to files when deleting users (#12155) * fix: Cascading delets to files when deleting users * Add a test for delete cascades * Ensure in-db cascade * [autofix.ci] apply automated fixes * Update 0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py * Update 0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py * Update src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test_user.py * Update src/backend/tests/unit/test_user.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test_user.py * Update src/backend/tests/unit/test_user.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...add_ondelete_cascade_to_file_user_id_fk.py | 68 ++++++++++++++++ .../services/database/models/file/model.py | 11 ++- .../services/database/models/user/model.py | 5 ++ src/backend/tests/unit/test_user.py | 77 +++++++++++++++++++ 4 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py diff --git a/src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py b/src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py new file mode 100644 index 000000000000..a9f99d5d83f3 --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py @@ -0,0 +1,68 @@ +"""Add ondelete CASCADE to file.user_id FK + +Revision ID: 0e6138e7a0c2 +Revises: fc7f696a57bf +Create Date: 2026-03-11 13:28:25.239444 + +Phase: EXPAND +""" +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op +from langflow.utils import migration + + +# revision identifiers, used by Alembic. +revision: str = "0e6138e7a0c2" # pragma: allowlist secret +down_revision: str | None = "fc7f696a57bf" # pragma: allowlist secret +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def _get_fk_constraint_name(conn, table_name: str, column_name: str) -> str | None: + """Find the foreign key constraint name for a given column.""" + inspector = sa.inspect(conn) + for fk in inspector.get_foreign_keys(table_name): + if column_name in fk["constrained_columns"]: + return fk["name"] + return None + + +def upgrade() -> None: + conn = op.get_bind() + + if not migration.table_exists("file", conn): + return + + fk_name = _get_fk_constraint_name(conn, "file", "user_id") + + if fk_name is None: + with op.batch_alter_table('file', schema=None) as batch_op: + batch_op.create_foreign_key( + "fk_file_user_id_user", 'user', ['user_id'], ['id'], ondelete='CASCADE' + ) + else: + with op.batch_alter_table('file', schema=None) as batch_op: + batch_op.drop_constraint(fk_name, type_='foreignkey') + batch_op.create_foreign_key( + "fk_file_user_id_user", 'user', ['user_id'], ['id'], ondelete='CASCADE' + ) + + +def downgrade() -> None: + conn = op.get_bind() + + if not migration.table_exists("file", conn): + return + + fk_name = _get_fk_constraint_name(conn, "file", "user_id") + + if fk_name is None: + return + + with op.batch_alter_table('file', schema=None) as batch_op: + batch_op.drop_constraint(fk_name, type_='foreignkey') + batch_op.create_foreign_key( + "fk_file_user_id_user", 'user', ['user_id'], ['id'] + ) diff --git a/src/backend/base/langflow/services/database/models/file/model.py b/src/backend/base/langflow/services/database/models/file/model.py index be76ae3963bb..d9a989dcb5eb 100644 --- a/src/backend/base/langflow/services/database/models/file/model.py +++ b/src/backend/base/langflow/services/database/models/file/model.py @@ -1,14 +1,21 @@ from datetime import datetime, timezone +from typing import TYPE_CHECKING from uuid import UUID, uuid4 -from sqlmodel import Field, SQLModel, UniqueConstraint +import sqlalchemy as sa +from sqlalchemy import ForeignKey +from sqlmodel import Column, Field, Relationship, SQLModel, UniqueConstraint from langflow.schema.serialize import UUIDstr +if TYPE_CHECKING: + from langflow.services.database.models.user.model import User + class File(SQLModel, table=True): # type: ignore[call-arg] id: UUIDstr = Field(default_factory=uuid4, primary_key=True) - user_id: UUID = Field(foreign_key="user.id") + user_id: UUID = Field(sa_column=Column(sa.Uuid(), ForeignKey("user.id", ondelete="CASCADE"), nullable=False)) + user: "User" = Relationship(back_populates="files") name: str = Field(nullable=False) path: str = Field(nullable=False) size: int = Field(nullable=False) diff --git a/src/backend/base/langflow/services/database/models/user/model.py b/src/backend/base/langflow/services/database/models/user/model.py index d578e1412625..d82543b3c665 100644 --- a/src/backend/base/langflow/services/database/models/user/model.py +++ b/src/backend/base/langflow/services/database/models/user/model.py @@ -12,6 +12,7 @@ from langflow.services.database.models.api_key.model import ApiKey from langflow.services.database.models.deployment.model import Deployment from langflow.services.database.models.deployment_provider_account.model import DeploymentProviderAccount + from langflow.services.database.models.file.model import File from langflow.services.database.models.flow.model import Flow from langflow.services.database.models.folder.model import Folder from langflow.services.database.models.variable.model import Variable @@ -55,6 +56,10 @@ class User(SQLModel, table=True): # type: ignore[call-arg] back_populates="user", sa_relationship_kwargs={"cascade": "delete"}, ) + files: list["File"] = Relationship( + back_populates="user", + sa_relationship_kwargs={"cascade": "delete"}, + ) folders: list["Folder"] = Relationship( back_populates="user", sa_relationship_kwargs={"cascade": "delete"}, diff --git a/src/backend/tests/unit/test_user.py b/src/backend/tests/unit/test_user.py index fb08daf53322..6e02585e976c 100644 --- a/src/backend/tests/unit/test_user.py +++ b/src/backend/tests/unit/test_user.py @@ -3,11 +3,13 @@ import pytest from httpx import AsyncClient from langflow.services.auth.utils import create_super_user, get_password_hash +from langflow.services.database.models.file.model import File from langflow.services.database.models.user import UserUpdate from langflow.services.database.models.user.model import User from langflow.services.database.utils import session_getter from langflow.services.deps import get_db_service, get_settings_service from lfx.services.settings.constants import DEFAULT_SUPERUSER +from sqlalchemy import text from sqlmodel import select @@ -259,6 +261,81 @@ async def test_delete_user_wrong_id(client: AsyncClient, super_user_headers): assert error["type"] == "uuid_parsing" +@pytest.mark.api_key_required +async def test_delete_user_cascades_to_files(client: AsyncClient, test_user, super_user_headers): # noqa: ARG001 + """Deleting a user should cascade-delete associated file records (e.g. _mcp_servers).""" + user_id = test_user["id"] + + # Create a file record owned by the user + import tempfile + + async with session_getter(get_db_service()) as session: + with tempfile.TemporaryDirectory() as tmpdirname: + file_path = f"{tmpdirname}/{user_id}" + file = File(user_id=user_id, name=f"_mcp_servers_{user_id}.json", path=file_path, size=42) + session.add(file) + await session.commit() + file_id = file.id + + # Verify the file exists + async with session_getter(get_db_service()) as session: + assert await session.get(File, file_id) is not None + + # Delete the user using a Core-level bulk DELETE to bypass ORM relationship cascades + async with session_getter(get_db_service()) as session: + from sqlalchemy import delete + + await session.exec(delete(User).where(User.id == user_id)) + await session.commit() + + # Verify the file was cascade-deleted by the database FK + async with session_getter(get_db_service()) as session: + assert await session.get(File, file_id) is None + + +@pytest.mark.api_key_required +async def test_delete_user_db_level_cascade(client): # noqa: ARG001 + """Raw SQL DELETE on users should cascade-delete File rows via DB-level ON DELETE CASCADE.""" + import tempfile + + # Create a user and an associated file + async with session_getter(get_db_service()) as session: + user = User( + username="cascade_test_user", + password=get_password_hash("testpassword"), + is_active=True, + last_login_at=datetime.now(tz=timezone.utc), + ) + session.add(user) + await session.commit() + await session.refresh(user) + user_id = user.id + + with tempfile.TemporaryDirectory() as tmpdirname: + file_path = f"{tmpdirname}/{user_id}" + file = File(user_id=user_id, name=f"_mcp_servers_{user_id}.json", path=file_path, size=42) + session.add(file) + await session.commit() + await session.refresh(file) + file_id = file.id + + # Verify the file exists before deletion + async with session_getter(get_db_service()) as session: + assert await session.get(File, file_id) is not None + + # Delete the user via raw SQL (bypasses ORM cascade, tests DB-level ON DELETE CASCADE) + db_service = get_db_service() + async with db_service.engine.connect() as conn: + await conn.execute(text("PRAGMA foreign_keys = ON")) + await conn.execute(text('DELETE FROM "user" WHERE id = :id'), {"id": str(user_id)}) + await conn.commit() + + # Verify the file was cascade-deleted at the DB level (use a fresh session to avoid cache) + async with db_service.engine.connect() as conn: + result = await conn.execute(text("SELECT id FROM file WHERE id = :id"), {"id": str(file_id)}) + assert result.first() is None + + @pytest.mark.api_key_required async def test_normal_user_cant_delete_user(client: AsyncClient, test_user, logged_in_headers): user_id = test_user["id"] From a2e166048e4f39b5932f638440f8d82f1e40bd4e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:59:48 +0000 Subject: [PATCH 10/29] docs: OpenAPI spec content updated without version change (#12113) * docs: OpenAPI spec content updated without version change * remove-duplicate-mcp-entries --------- Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com> Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com> --- docs/openapi/openapi.json | 3503 +++++++++++++++++++++---------------- 1 file changed, 2037 insertions(+), 1466 deletions(-) diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index 75957225a1c5..524cb40f7c2c 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -4,6 +4,98 @@ "title": "Langflow", "version": "1.8.0" }, + "$defs": { + "ComponentOutput": { + "description": "Component output schema.", + "properties": { + "type": { + "description": "Type of the component output (e.g., 'message', 'data', 'tool', 'text')", + "title": "Type", + "type": "string" + }, + "status": { + "$ref": "#/$defs/JobStatus" + }, + "content": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Content" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + } + }, + "required": [ + "type", + "status" + ], + "title": "ComponentOutput", + "type": "object" + }, + "ErrorDetail": { + "description": "Error detail schema.", + "properties": { + "error": { + "title": "Error", + "type": "string" + }, + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code" + }, + "details": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Details" + } + }, + "required": [ + "error" + ], + "title": "ErrorDetail", + "type": "object" + }, + "JobStatus": { + "description": "Job execution status.", + "enum": [ + "queued", + "in_progress", + "completed", + "failed", + "cancelled", + "timed_out" + ], + "title": "JobStatus", + "type": "string" + } + }, "paths": { "/api/v1/build/{flow_id}/flow": { "post": { @@ -663,7 +755,7 @@ "Base" ], "summary": "Get Config", - "description": "Retrieve the current application configuration settings.

Requires authentication to prevent exposure of sensitive configuration details.

Returns:
ConfigResponse: The configuration settings of the application.

Raises:
HTTPException: If an error occurs while retrieving the configuration.", + "description": "Retrieve application configuration settings.

Returns different configuration based on authentication status:
- Authenticated users: Full ConfigResponse with all settings
- Unauthenticated users: PublicConfigResponse with limited, safe-to-expose settings

Args:
user: The authenticated user, or None if unauthenticated.

Returns:
ConfigResponse | PublicConfigResponse: Configuration settings appropriate for the user's auth status.

Raises:
HTTPException: If an error occurs while retrieving the configuration.", "operationId": "get_config_api_v1_config_get", "responses": { "200": { @@ -671,7 +763,15 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ConfigResponse" + "anyOf": [ + { + "$ref": "#/components/schemas/ConfigResponse" + }, + { + "$ref": "#/components/schemas/PublicConfigResponse" + } + ], + "title": "Response Get Config Api V1 Config Get" } } } @@ -2647,29 +2747,14 @@ } } }, - "/api/v1/projects/": { + "/api/v1/monitor/traces": { "get": { "tags": [ - "Projects" + "Traces" ], - "summary": "Read Projects", - "operationId": "read_projects_api_v1_projects__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/FolderRead" - }, - "type": "array", - "title": "Response Read Projects Api V1 Projects Get" - } - } - } - } - }, + "summary": "Get Traces", + "description": "Get list of traces for a flow.

Args:
current_user: Authenticated user (required for authorization)
flow_id: Filter by flow ID
session_id: Filter by session ID
status: Filter by trace status
query: Search query for trace name/id/session id
start_time: Filter traces starting on/after this time (ISO)
end_time: Filter traces starting on/before this time (ISO)
page: Page number (1-based)
size: Page size

Returns:
List of traces", + "operationId": "get_traces_api_v1_monitor_traces_get", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -2680,148 +2765,128 @@ { "API key header": [] } - ] - }, - "post": { - "tags": [ - "Projects" ], - "summary": "Create Project", - "operationId": "create_project_api_v1_projects__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FolderCreate" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FolderRead" + "parameters": [ + { + "name": "flow_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" } - } + ], + "title": "Flow Id" } }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + { + "name": "session_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" } - } + ], + "title": "Session Id" } - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, - { - "API key header": [] - } - ] - } - }, - "/api/v1/projects/{project_id}": { - "get": { - "tags": [ - "Projects" - ], - "summary": "Read Project", - "operationId": "read_project_api_v1_projects__project_id__get", - "security": [ - { - "OAuth2PasswordBearerCookie": [] }, { - "API key query": [] - }, - { - "API key header": [] - } - ], - "parameters": [ - { - "name": "project_id", - "in": "path", - "required": true, + "name": "status", + "in": "query", + "required": false, "schema": { - "type": "string", - "format": "uuid", - "title": "Project Id" + "anyOf": [ + { + "$ref": "#/components/schemas/SpanStatus" + }, + { + "type": "null" + } + ], + "title": "Status" } }, { - "name": "page", + "name": "query", "in": "query", "required": false, "schema": { "anyOf": [ { - "type": "integer" + "type": "string" }, { "type": "null" } ], - "title": "Page" + "title": "Query" } }, { - "name": "size", + "name": "start_time", "in": "query", "required": false, "schema": { "anyOf": [ { - "type": "integer" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Size" + "title": "Start Time" } }, { - "name": "is_component", + "name": "end_time", "in": "query", "required": false, "schema": { - "type": "boolean", - "default": false, - "title": "Is Component" + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Time" } }, { - "name": "is_flow", + "name": "page", "in": "query", "required": false, "schema": { - "type": "boolean", - "default": false, - "title": "Is Flow" + "type": "integer", + "minimum": 0, + "default": 1, + "title": "Page" } }, { - "name": "search", + "name": "size", "in": "query", "required": false, "schema": { - "type": "string", - "default": "", - "title": "Search" + "type": "integer", + "maximum": 200, + "minimum": 1, + "default": 50, + "title": "Size" } } ], @@ -2831,15 +2896,7 @@ "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/FolderWithPaginatedFlows" - }, - { - "$ref": "#/components/schemas/FolderReadWithFlows" - } - ], - "title": "Response Read Project Api V1 Projects Project Id Get" + "$ref": "#/components/schemas/TraceListResponse" } } } @@ -2856,12 +2913,13 @@ } } }, - "patch": { + "delete": { "tags": [ - "Projects" + "Traces" ], - "summary": "Update Project", - "operationId": "update_project_api_v1_projects__project_id__patch", + "summary": "Delete Traces By Flow", + "description": "Delete all traces for a flow.

Args:
flow_id: The ID of the flow whose traces should be deleted.
current_user: The authenticated user (required for authorization).", + "operationId": "delete_traces_by_flow_api_v1_monitor_traces_delete", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -2875,36 +2933,19 @@ ], "parameters": [ { - "name": "project_id", - "in": "path", + "name": "flow_id", + "in": "query", "required": true, "schema": { "type": "string", "format": "uuid", - "title": "Project Id" + "title": "Flow Id" } } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FolderUpdate" - } - } - } - }, "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FolderRead" - } - } - } + "204": { + "description": "Successful Response" }, "422": { "description": "Validation Error", @@ -2917,13 +2958,16 @@ } } } - }, - "delete": { + } + }, + "/api/v1/monitor/traces/{trace_id}": { + "get": { "tags": [ - "Projects" + "Traces" ], - "summary": "Delete Project", - "operationId": "delete_project_api_v1_projects__project_id__delete", + "summary": "Get Trace", + "description": "Get a single trace with its hierarchical span tree.

Args:
trace_id: The ID of the trace to retrieve.
current_user: The authenticated user (required for authorization).

Returns:
TraceRead containing the trace and its hierarchical span tree.", + "operationId": "get_trace_api_v1_monitor_traces__trace_id__get", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -2937,19 +2981,26 @@ ], "parameters": [ { - "name": "project_id", + "name": "trace_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid", - "title": "Project Id" + "title": "Trace Id" } } ], "responses": { - "204": { - "description": "Successful Response" + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TraceRead" + } + } + } }, "422": { "description": "Validation Error", @@ -2962,16 +3013,14 @@ } } } - } - }, - "/api/v1/projects/download/{project_id}": { - "get": { + }, + "delete": { "tags": [ - "Projects" + "Traces" ], - "summary": "Download File", - "description": "Download all flows from project as a zip file.", - "operationId": "download_file_api_v1_projects_download__project_id__get", + "summary": "Delete Trace", + "description": "Delete a trace and all its spans.

Args:
trace_id: The ID of the trace to delete.
current_user: The authenticated user (required for authorization).", + "operationId": "delete_trace_api_v1_monitor_traces__trace_id__delete", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -2985,24 +3034,19 @@ ], "parameters": [ { - "name": "project_id", + "name": "trace_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid", - "title": "Project Id" + "title": "Trace Id" } } ], "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } + "204": { + "description": "Successful Response" }, "422": { "description": "Validation Error", @@ -3017,19 +3061,52 @@ } } }, - "/api/v1/projects/upload/": { + "/api/v1/projects/": { + "get": { + "tags": [ + "Projects" + ], + "summary": "Read Projects", + "operationId": "read_projects_api_v1_projects__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/FolderRead" + }, + "type": "array", + "title": "Response Read Projects Api V1 Projects Get" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearerCookie": [] + }, + { + "API key query": [] + }, + { + "API key header": [] + } + ] + }, "post": { "tags": [ "Projects" ], - "summary": "Upload File", - "description": "Upload flows from a file.", - "operationId": "upload_file_api_v1_projects_upload__post", + "summary": "Create Project", + "operationId": "create_project_api_v1_projects__post", "requestBody": { "content": { - "multipart/form-data": { + "application/json": { "schema": { - "$ref": "#/components/schemas/Body_upload_file_api_v1_projects_upload__post" + "$ref": "#/components/schemas/FolderCreate" } } }, @@ -3041,11 +3118,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/FlowRead" - }, - "type": "array", - "title": "Response Upload File Api V1 Projects Upload Post" + "$ref": "#/components/schemas/FolderRead" } } } @@ -3074,30 +3147,13 @@ ] } }, - "/api/v1/starter-projects/": { + "/api/v1/projects/{project_id}": { "get": { "tags": [ - "Flows" + "Projects" ], - "summary": "Get Starter Projects", - "description": "Get a list of starter projects.", - "operationId": "get_starter_projects_api_v1_starter_projects__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/GraphDumpResponse" - }, - "type": "array", - "title": "Response Get Starter Projects Api V1 Starter Projects Get" - } - } - } - } - }, + "summary": "Read Project", + "operationId": "read_project_api_v1_projects__project_id__get", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -3108,85 +3164,118 @@ { "API key header": [] } - ] - } - }, - "/api/v1/mcp/sse": { - "get": { - "tags": [ - "mcp" ], - "summary": "Handle Sse", - "operationId": "handle_sse_api_v1_mcp_sse_get", - "responses": { - "200": { - "description": "Successful Response" - } - }, - "security": [ + "parameters": [ { - "OAuth2PasswordBearerCookie": [] + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Project Id" + } }, { - "API key query": [] + "name": "page", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Page" + } }, { - "API key header": [] + "name": "size", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Size" + } + }, + { + "name": "is_component", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Is Component" + } + }, + { + "name": "is_flow", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false, + "title": "Is Flow" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "", + "title": "Search" + } } - ] - }, - "head": { - "tags": [ - "mcp" ], - "summary": "Im Alive", - "operationId": "im_alive_api_v1_mcp_sse_head", "responses": { "200": { "description": "Successful Response", "content": { - "text/html": { + "application/json": { "schema": { - "type": "string" + "anyOf": [ + { + "$ref": "#/components/schemas/FolderWithPaginatedFlows" + }, + { + "$ref": "#/components/schemas/FolderReadWithFlows" + } + ], + "title": "Response Read Project Api V1 Projects Project Id Get" } } } - } - } - } - }, - "/api/v1/mcp/": { - "post": { - "tags": [ - "mcp" - ], - "summary": "Handle Messages", - "operationId": "handle_messages_api_v1_mcp__post", - "responses": { - "200": { - "description": "Successful Response", + }, + "422": { + "description": "Validation Error", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } } } } - } - }, - "/api/v1/mcp/streamable": { - "get": { + }, + "patch": { "tags": [ - "mcp" + "Projects" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_v1_mcp_streamable_post", - "responses": { - "200": { - "description": "Successful Response" - } - }, + "summary": "Update Project", + "operationId": "update_project_api_v1_projects__project_id__patch", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -3197,87 +3286,58 @@ { "API key header": [] } - ] - }, - "post": { - "tags": [ - "mcp" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_v1_mcp_streamable_post", - "responses": { - "200": { - "description": "Successful Response" - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, + "parameters": [ { - "API key header": [] + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Project Id" + } } - ] - }, - "delete": { - "tags": [ - "mcp" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_v1_mcp_streamable_post", - "responses": { - "200": { - "description": "Successful Response" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FolderUpdate" + } + } } }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, - { - "API key header": [] - } - ] - }, - "head": { - "tags": [ - "mcp" - ], - "summary": "Streamable Health", - "operationId": "streamable_health_api_v1_mcp_streamable_head", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/FolderRead" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } } } } } - } - }, - "/api/v1/mcp/streamable/": { - "get": { + }, + "delete": { "tags": [ - "mcp" + "Projects" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_v1_mcp_streamable__post", - "responses": { - "200": { - "description": "Successful Response" - } - }, + "summary": "Delete Project", + "operationId": "delete_project_api_v1_projects__project_id__delete", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -3288,65 +3348,44 @@ { "API key header": [] } - ] - }, - "post": { - "tags": [ - "mcp" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_v1_mcp_streamable__post", - "responses": { - "200": { - "description": "Successful Response" - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, + "parameters": [ { - "API key header": [] + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Project Id" + } } - ] - }, - "delete": { - "tags": [ - "mcp" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_v1_mcp_streamable__post", "responses": { - "200": { + "204": { "description": "Successful Response" - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] }, - { - "API key header": [] + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } } - ] + } } }, - "/api/v1/mcp/project/{project_id}": { + "/api/v1/projects/download/{project_id}": { "get": { "tags": [ - "mcp_projects" + "Projects" ], - "summary": "List Project Tools", - "description": "List project MCP tools.", - "operationId": "list_project_tools_api_v1_mcp_project__project_id__get", + "summary": "Download File", + "description": "Download all flows from project as a zip file.", + "operationId": "download_file_api_v1_projects_download__project_id__get", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -3368,16 +3407,6 @@ "format": "uuid", "title": "Project Id" } - }, - { - "name": "mcp_enabled", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "default": true, - "title": "Mcp Enabled" - } } ], "responses": { @@ -3400,32 +3429,38 @@ } } } - }, + } + }, + "/api/v1/projects/upload/": { "post": { "tags": [ - "mcp_projects" + "Projects" ], - "summary": "Handle Project Messages", - "description": "Handle POST messages for a project-specific MCP server.", - "operationId": "handle_project_messages_api_v1_mcp_project__project_id__post", - "parameters": [ - { - "name": "project_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "title": "Project Id" + "summary": "Upload File", + "description": "Upload flows from a file.", + "operationId": "upload_file_api_v1_projects_upload__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_file_api_v1_projects_upload__post" + } } - } - ], + }, + "required": true + }, "responses": { - "200": { + "201": { "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "items": { + "$ref": "#/components/schemas/FlowRead" + }, + "type": "array", + "title": "Response Upload File Api V1 Projects Upload Post" + } } } }, @@ -3439,15 +3474,7 @@ } } } - } - }, - "patch": { - "tags": [ - "mcp_projects" - ], - "summary": "Update Project Mcp Settings", - "description": "Update the MCP settings of all flows in a project and project-level auth settings.

On MCP Composer failure, this endpoint should return with a 200 status code and an error message in
the body of the response to display to the user.", - "operationId": "update_project_mcp_settings_api_v1_mcp_project__project_id__patch", + }, "security": [ { "OAuth2PasswordBearerCookie": [] @@ -3458,69 +3485,76 @@ { "API key header": [] } + ] + } + }, + "/api/v1/starter-projects/": { + "get": { + "tags": [ + "Flows" ], - "parameters": [ - { - "name": "project_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "title": "Project Id" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MCPProjectUpdateRequest" - } - } - } - }, + "summary": "Get Starter Projects", + "description": "Get a list of starter projects.", + "operationId": "get_starter_projects_api_v1_starter_projects__get", "responses": { "200": { "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "items": { + "$ref": "#/components/schemas/GraphDumpResponse" + }, + "type": "array", + "title": "Response Get Starter Projects Api V1 Starter Projects Get" } } } } - } + }, + "security": [ + { + "OAuth2PasswordBearerCookie": [] + }, + { + "API key query": [] + }, + { + "API key header": [] + } + ] } }, - "/api/v1/mcp/project/{project_id}/sse": { - "head": { + "/api/v1/mcp/sse": { + "get": { "tags": [ - "mcp_projects" + "mcp" ], - "summary": "Im Alive", - "operationId": "im_alive_api_v1_mcp_project__project_id__sse_head", - "parameters": [ + "summary": "Handle Sse", + "operationId": "handle_sse_api_v1_mcp_sse_get", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "security": [ { - "name": "project_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "title": "Project Id" - } + "OAuth2PasswordBearerCookie": [] + }, + { + "API key query": [] + }, + { + "API key header": [] } + ] + }, + "head": { + "tags": [ + "mcp" ], + "summary": "Im Alive", + "operationId": "im_alive_api_v1_mcp_sse_head", "responses": { "200": { "description": "Successful Response", @@ -3531,26 +3565,139 @@ } } } - }, - "422": { - "description": "Validation Error", + } + } + } + }, + "/api/v1/mcp/": { + "post": { + "tags": [ + "mcp" + ], + "summary": "Handle Messages", + "operationId": "handle_messages_api_v1_mcp__post", + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } + "schema": {} } } } } + } + }, + "/api/v1/mcp/streamable": { + "get": { + "tags": [ + "mcp" + ], + "summary": "Handle Streamable Http", + "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", + "operationId": "handle_streamable_http_api_v1_mcp_streamable_delete", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "security": [ + { + "OAuth2PasswordBearerCookie": [] + }, + { + "API key query": [] + }, + { + "API key header": [] + } + ] + }, + "post": { + "tags": [ + "mcp" + ], + "summary": "Handle Streamable Http", + "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", + "operationId": "handle_streamable_http_api_v1_mcp_streamable_delete", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "security": [ + { + "OAuth2PasswordBearerCookie": [] + }, + { + "API key query": [] + }, + { + "API key header": [] + } + ] }, + "delete": { + "tags": [ + "mcp" + ], + "summary": "Handle Streamable Http", + "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", + "operationId": "handle_streamable_http_api_v1_mcp_streamable_delete", + "responses": { + "200": { + "description": "Successful Response" + } + }, + "security": [ + { + "OAuth2PasswordBearerCookie": [] + }, + { + "API key query": [] + }, + { + "API key header": [] + } + ] + }, + "head": { + "tags": [ + "mcp" + ], + "summary": "Streamable Health", + "operationId": "streamable_health_api_v1_mcp_streamable_head", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + } + } + }, + "/api/v1/mcp/project/{project_id}": { "get": { "tags": [ "mcp_projects" ], - "summary": "Handle Project Sse", - "description": "Handle SSE connections for a specific project.", - "operationId": "handle_project_sse_api_v1_mcp_project__project_id__sse_get", + "summary": "List Project Tools", + "description": "List project MCP tools.", + "operationId": "list_project_tools_api_v1_mcp_project__project_id__get", + "security": [ + { + "OAuth2PasswordBearerCookie": [] + }, + { + "API key query": [] + }, + { + "API key header": [] + } + ], "parameters": [ { "name": "project_id", @@ -3561,16 +3708,24 @@ "format": "uuid", "title": "Project Id" } + }, + { + "name": "mcp_enabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": true, + "title": "Mcp Enabled" + } } ], "responses": { "200": { "description": "Successful Response", "content": { - "text/html": { - "schema": { - "type": "string" - } + "application/json": { + "schema": {} } } }, @@ -3585,16 +3740,14 @@ } } } - } - }, - "/api/v1/mcp/project/{project_id}/": { + }, "post": { "tags": [ "mcp_projects" ], "summary": "Handle Project Messages", "description": "Handle POST messages for a project-specific MCP server.", - "operationId": "handle_project_messages_api_v1_mcp_project__project_id___post", + "operationId": "handle_project_messages_api_v1_mcp_project__project_id__post", "parameters": [ { "name": "project_id", @@ -3627,15 +3780,25 @@ } } } - } - }, - "/api/v1/mcp/project/{project_id}/streamable": { - "head": { + }, + "patch": { "tags": [ "mcp_projects" ], - "summary": "Streamable Health", - "operationId": "streamable_health_api_v1_mcp_project__project_id__streamable_head", + "summary": "Update Project Mcp Settings", + "description": "Update the MCP settings of all flows in a project and project-level auth settings.

On MCP Composer failure, this endpoint should return with a 200 status code and an error message in
the body of the response to display to the user.", + "operationId": "update_project_mcp_settings_api_v1_mcp_project__project_id__patch", + "security": [ + { + "OAuth2PasswordBearerCookie": [] + }, + { + "API key query": [] + }, + { + "API key header": [] + } + ], "parameters": [ { "name": "project_id", @@ -3648,6 +3811,16 @@ } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MCPProjectUpdateRequest" + } + } + } + }, "responses": { "200": { "description": "Successful Response", @@ -3668,14 +3841,15 @@ } } } - }, - "post": { + } + }, + "/api/v1/mcp/project/{project_id}/sse": { + "head": { "tags": [ "mcp_projects" ], - "summary": "Handle Project Streamable Http", - "description": "Handle Streamable HTTP connections for a specific project.", - "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable_post", + "summary": "Im Alive", + "operationId": "im_alive_api_v1_mcp_project__project_id__sse_head", "parameters": [ { "name": "project_id", @@ -3683,17 +3857,23 @@ "required": true, "schema": { "type": "string", - "format": "uuid", "title": "Project Id" } } ], "responses": { "200": { - "description": "Successful Response" - }, - "422": { - "description": "Validation Error", + "description": "Successful Response", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + }, + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { @@ -3708,9 +3888,9 @@ "tags": [ "mcp_projects" ], - "summary": "Handle Project Streamable Http", - "description": "Handle Streamable HTTP connections for a specific project.", - "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable_post", + "summary": "Handle Project Sse", + "description": "Handle SSE connections for a specific project.", + "operationId": "handle_project_sse_api_v1_mcp_project__project_id__sse_get", "parameters": [ { "name": "project_id", @@ -3725,7 +3905,14 @@ ], "responses": { "200": { - "description": "Successful Response" + "description": "Successful Response", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } }, "422": { "description": "Validation Error", @@ -3738,14 +3925,16 @@ } } } - }, - "delete": { + } + }, + "/api/v1/mcp/project/{project_id}/": { + "post": { "tags": [ "mcp_projects" ], - "summary": "Handle Project Streamable Http", - "description": "Handle Streamable HTTP connections for a specific project.", - "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable_post", + "summary": "Handle Project Messages", + "description": "Handle POST messages for a project-specific MCP server.", + "operationId": "handle_project_messages_api_v1_mcp_project__project_id___post", "parameters": [ { "name": "project_id", @@ -3760,7 +3949,12 @@ ], "responses": { "200": { - "description": "Successful Response" + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } }, "422": { "description": "Validation Error", @@ -3775,14 +3969,53 @@ } } }, - "/api/v1/mcp/project/{project_id}/streamable/": { - "post": { + "/api/v1/mcp/project/{project_id}/streamable": { + "head": { + "tags": [ + "mcp_projects" + ], + "summary": "Streamable Health", + "operationId": "streamable_health_api_v1_mcp_project__project_id__streamable_head", + "parameters": [ + { + "name": "project_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Project Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { "tags": [ "mcp_projects" ], "summary": "Handle Project Streamable Http", "description": "Handle Streamable HTTP connections for a specific project.", - "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable__post", + "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable_delete", "parameters": [ { "name": "project_id", @@ -3817,7 +4050,7 @@ ], "summary": "Handle Project Streamable Http", "description": "Handle Streamable HTTP connections for a specific project.", - "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable__post", + "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable_delete", "parameters": [ { "name": "project_id", @@ -3846,13 +4079,13 @@ } } }, - "delete": { + "post": { "tags": [ "mcp_projects" ], "summary": "Handle Project Streamable Http", "description": "Handle Streamable HTTP connections for a specific project.", - "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable__post", + "operationId": "handle_project_streamable_http_api_v1_mcp_project__project_id__streamable_delete", "parameters": [ { "name": "project_id", @@ -4355,7 +4588,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UploadFileResponse" + "$ref": "#/components/schemas/langflow__api__schemas__UploadFileResponse" } } } @@ -4398,7 +4631,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/File" + "$ref": "#/components/schemas/langflow__services__database__models__file__model__File" }, "title": "Response List Files Api V2 Files Get" } @@ -4484,7 +4717,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UploadFileResponse" + "$ref": "#/components/schemas/langflow__api__schemas__UploadFileResponse" } } } @@ -4527,7 +4760,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/File" + "$ref": "#/components/schemas/langflow__services__database__models__file__model__File" }, "title": "Response List Files Api V2 Files Get" } @@ -4783,7 +5016,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UploadFileResponse" + "$ref": "#/components/schemas/langflow__api__schemas__UploadFileResponse" } } } @@ -4995,9 +5228,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true, - "title": "Server Config" + "$ref": "#/components/schemas/MCPServerConfig" } } } @@ -5056,9 +5287,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true, - "title": "Server Config" + "$ref": "#/components/schemas/MCPServerConfig" } } } @@ -5168,10 +5397,269 @@ "schema": { "oneOf": [ { - "$ref": "#/components/schemas/WorkflowExecutionResponse" + "$defs": { + "ComponentOutput": { + "description": "Component output schema.", + "properties": { + "type": { + "description": "Type of the component output (e.g., 'message', 'data', 'tool', 'text')", + "title": "Type", + "type": "string" + }, + "status": { + "$ref": "#/$defs/JobStatus" + }, + "content": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Content" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + } + }, + "required": [ + "type", + "status" + ], + "title": "ComponentOutput", + "type": "object" + }, + "ErrorDetail": { + "description": "Error detail schema.", + "properties": { + "error": { + "title": "Error", + "type": "string" + }, + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code" + }, + "details": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Details" + } + }, + "required": [ + "error" + ], + "title": "ErrorDetail", + "type": "object" + }, + "JobStatus": { + "description": "Job execution status.", + "enum": [ + "queued", + "in_progress", + "completed", + "failed", + "cancelled", + "timed_out" + ], + "title": "JobStatus", + "type": "string" + } + }, + "description": "Synchronous workflow execution response.", + "properties": { + "flow_id": { + "title": "Flow Id", + "type": "string" + }, + "job_id": { + "anyOf": [ + { + "type": "string" + }, + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Job Id" + }, + "object": { + "const": "response", + "default": "response", + "title": "Object", + "type": "string" + }, + "created_timestamp": { + "title": "Created Timestamp", + "type": "string" + }, + "status": { + "$ref": "#/$defs/JobStatus" + }, + "errors": { + "default": [], + "items": { + "$ref": "#/$defs/ErrorDetail" + }, + "title": "Errors", + "type": "array" + }, + "inputs": { + "additionalProperties": true, + "default": {}, + "title": "Inputs", + "type": "object" + }, + "outputs": { + "additionalProperties": { + "$ref": "#/$defs/ComponentOutput" + }, + "default": {}, + "title": "Outputs", + "type": "object" + } + }, + "required": [ + "flow_id", + "status" + ], + "title": "WorkflowExecutionResponse", + "type": "object" }, { - "$ref": "#/components/schemas/WorkflowJobResponse" + "$defs": { + "ErrorDetail": { + "description": "Error detail schema.", + "properties": { + "error": { + "title": "Error", + "type": "string" + }, + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code" + }, + "details": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Details" + } + }, + "required": [ + "error" + ], + "title": "ErrorDetail", + "type": "object" + }, + "JobStatus": { + "description": "Job execution status.", + "enum": [ + "queued", + "in_progress", + "completed", + "failed", + "cancelled", + "timed_out" + ], + "title": "JobStatus", + "type": "string" + } + }, + "description": "Background job response.", + "properties": { + "job_id": { + "anyOf": [ + { + "type": "string" + }, + { + "format": "uuid", + "type": "string" + } + ], + "title": "Job Id" + }, + "flow_id": { + "title": "Flow Id", + "type": "string" + }, + "object": { + "const": "job", + "default": "job", + "title": "Object", + "type": "string" + }, + "created_timestamp": { + "title": "Created Timestamp", + "type": "string" + }, + "status": { + "$ref": "#/$defs/JobStatus" + }, + "links": { + "additionalProperties": { + "type": "string" + }, + "title": "Links", + "type": "object" + }, + "errors": { + "default": [], + "items": { + "$ref": "#/$defs/ErrorDetail" + }, + "title": "Errors", + "type": "array" + } + }, + "required": [ + "job_id", + "flow_id", + "status" + ], + "title": "WorkflowJobResponse", + "type": "object" } ], "discriminator": { @@ -5275,6 +5763,98 @@ "content": { "application/json": { "schema": { + "$defs": { + "ComponentOutput": { + "description": "Component output schema.", + "properties": { + "type": { + "description": "Type of the component output (e.g., 'message', 'data', 'tool', 'text')", + "title": "Type", + "type": "string" + }, + "status": { + "$ref": "#/$defs/JobStatus" + }, + "content": { + "anyOf": [ + {}, + { + "type": "null" + } + ], + "title": "Content" + }, + "metadata": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" + } + }, + "required": [ + "type", + "status" + ], + "title": "ComponentOutput", + "type": "object" + }, + "ErrorDetail": { + "description": "Error detail schema.", + "properties": { + "error": { + "title": "Error", + "type": "string" + }, + "code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Code" + }, + "details": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Details" + } + }, + "required": [ + "error" + ], + "title": "ErrorDetail", + "type": "object" + }, + "JobStatus": { + "description": "Job execution status.", + "enum": [ + "queued", + "in_progress", + "completed", + "failed", + "cancelled", + "timed_out" + ], + "title": "JobStatus", + "type": "string" + } + }, "description": "Synchronous workflow execution response.", "properties": { "flow_id": { @@ -5307,12 +5887,12 @@ "type": "string" }, "status": { - "$ref": "#/components/schemas/JobStatus_2" + "$ref": "#/$defs/JobStatus" }, "errors": { "default": [], "items": { - "$ref": "#/components/schemas/ErrorDetail_2" + "$ref": "#/$defs/ErrorDetail" }, "title": "Errors", "type": "array" @@ -5325,7 +5905,7 @@ }, "outputs": { "additionalProperties": { - "$ref": "#/components/schemas/ComponentOutput_1" + "$ref": "#/$defs/ComponentOutput" }, "default": {}, "title": "Outputs", @@ -5438,80 +6018,62 @@ ] } }, - "/api/mcp/sse": { + "/health": { "get": { "tags": [ - "mcp" - ], - "summary": "Handle Sse", - "operationId": "handle_sse_api_mcp_sse_get", - "responses": { - "200": { - "description": "Successful Response" - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, - { - "API key header": [] - } - ] - }, - "head": { - "tags": [ - "mcp" + "Health Check" ], - "summary": "Im Alive", - "operationId": "im_alive_api_mcp_sse_head", + "summary": "Health", + "operationId": "health_health_get", "responses": { "200": { "description": "Successful Response", "content": { - "text/html": { - "schema": { - "type": "string" - } + "application/json": { + "schema": {} } } } } } }, - "/api/mcp/": { - "post": { + "/health_check": { + "get": { "tags": [ - "mcp" + "Health Check" ], - "summary": "Handle Messages", - "operationId": "handle_messages_api_mcp__post", + "summary": "Health Check", + "operationId": "health_check_health_check_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } } } } } } }, - "/api/mcp/streamable": { + "/logs-stream": { "get": { "tags": [ - "mcp" + "Log" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_mcp_streamable_post", + "summary": "Stream Logs", + "description": "HTTP/2 Server-Sent-Event (SSE) endpoint for streaming logs.

Requires authentication to prevent exposure of sensitive log data.
It establishes a long-lived connection to the server and receives log messages in real-time.
The client should use the header \"Accept: text/event-stream\".", + "operationId": "stream_logs_logs_stream_get", "responses": { "200": { - "description": "Successful Response" + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } } }, "security": [ @@ -5525,19 +6087,16 @@ "API key header": [] } ] - }, - "post": { + } + }, + "/logs": { + "get": { "tags": [ - "mcp" + "Log" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_mcp_streamable_post", - "responses": { - "200": { - "description": "Successful Response" - } - }, + "summary": "Logs", + "description": "Retrieve application logs with authentication required.

SECURITY: Logs may contain sensitive information and require authentication.", + "operationId": "logs_logs_get", "security": [ { "OAuth2PasswordBearerCookie": [] @@ -5548,207 +6107,19 @@ { "API key header": [] } - ] - }, - "delete": { - "tags": [ - "mcp" ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_mcp_streamable_post", - "responses": { - "200": { - "description": "Successful Response" - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, + "parameters": [ { - "API key query": [] - }, - { - "API key header": [] - } - ] - }, - "head": { - "tags": [ - "mcp" - ], - "summary": "Streamable Health", - "operationId": "streamable_health_api_mcp_streamable_head", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - } - } - } - }, - "/api/mcp/streamable/": { - "get": { - "tags": [ - "mcp" - ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_mcp_streamable__post", - "responses": { - "200": { - "description": "Successful Response" - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, - { - "API key header": [] - } - ] - }, - "post": { - "tags": [ - "mcp" - ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_mcp_streamable__post", - "responses": { - "200": { - "description": "Successful Response" - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, - { - "API key header": [] - } - ] - }, - "delete": { - "tags": [ - "mcp" - ], - "summary": "Handle Streamable Http", - "description": "Streamable HTTP endpoint for MCP clients that support the new transport.", - "operationId": "handle_streamable_http_api_mcp_streamable__post", - "responses": { - "200": { - "description": "Successful Response" - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, - { - "API key header": [] - } - ] - } - }, - "/health_check": { - "get": { - "tags": [ - "Health Check" - ], - "summary": "Health Check", - "operationId": "health_check_health_check_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HealthResponse" - } - } - } - } - } - } - }, - "/logs-stream": { - "get": { - "tags": [ - "Log" - ], - "summary": "Stream Logs", - "description": "HTTP/2 Server-Sent-Event (SSE) endpoint for streaming logs.

Requires authentication to prevent exposure of sensitive log data.
It establishes a long-lived connection to the server and receives log messages in real-time.
The client should use the header \"Accept: text/event-stream\".", - "operationId": "stream_logs_logs_stream_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - } - }, - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, - { - "API key header": [] - } - ] - } - }, - "/logs": { - "get": { - "tags": [ - "Log" - ], - "summary": "Logs", - "description": "Retrieve application logs with authentication required.

SECURITY: Logs may contain sensitive information and require authentication.", - "operationId": "logs_logs_get", - "security": [ - { - "OAuth2PasswordBearerCookie": [] - }, - { - "API key query": [] - }, - { - "API key header": [] - } - ], - "parameters": [ - { - "name": "lines_before", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "description": "The number of logs before the timestamp or the last log", - "default": 0, - "title": "Lines Before" - }, - "description": "The number of logs before the timestamp or the last log" + "name": "lines_before", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "description": "The number of logs before the timestamp or the last log", + "default": 0, + "title": "Lines Before" + }, + "description": "The number of logs before the timestamp or the last log" }, { "name": "lines_after", @@ -6211,7 +6582,7 @@ "properties": { "file": { "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", "title": "File" } }, @@ -6225,7 +6596,7 @@ "properties": { "file": { "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", "title": "File" } }, @@ -6239,7 +6610,7 @@ "properties": { "file": { "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", "title": "File" } }, @@ -6253,7 +6624,7 @@ "properties": { "file": { "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", "title": "File" } }, @@ -6267,7 +6638,7 @@ "properties": { "file": { "type": "string", - "format": "binary", + "contentMediaType": "application/octet-stream", "title": "File" } }, @@ -6456,6 +6827,33 @@ }, "ConfigResponse": { "properties": { + "max_file_size_upload": { + "type": "integer", + "title": "Max File Size Upload" + }, + "event_delivery": { + "type": "string", + "enum": [ + "polling", + "streaming", + "direct" + ], + "title": "Event Delivery" + }, + "voice_mode_available": { + "type": "boolean", + "title": "Voice Mode Available" + }, + "frontend_timeout": { + "type": "integer", + "title": "Frontend Timeout" + }, + "type": { + "type": "string", + "const": "full", + "title": "Type", + "default": "full" + }, "feature_flags": { "$ref": "#/components/schemas/FeatureFlags" }, @@ -6467,10 +6865,6 @@ "type": "integer", "title": "Serialization Max Text Length" }, - "frontend_timeout": { - "type": "integer", - "title": "Frontend Timeout" - }, "auto_saving": { "type": "boolean", "title": "Auto Saving" @@ -6483,10 +6877,6 @@ "type": "integer", "title": "Health Check Max Retries" }, - "max_file_size_upload": { - "type": "integer", - "title": "Max File Size Upload" - }, "webhook_polling_interval": { "type": "integer", "title": "Webhook Polling Interval" @@ -6499,23 +6889,10 @@ "type": "integer", "title": "Public Flow Expiration" }, - "event_delivery": { - "type": "string", - "enum": [ - "polling", - "streaming", - "direct" - ], - "title": "Event Delivery" - }, "webhook_auth_enable": { "type": "boolean", "title": "Webhook Auth Enable" }, - "voice_mode_available": { - "type": "boolean", - "title": "Voice Mode Available" - }, "default_folder_name": { "type": "string", "title": "Default Folder Name" @@ -6527,24 +6904,25 @@ }, "type": "object", "required": [ + "max_file_size_upload", + "event_delivery", + "voice_mode_available", + "frontend_timeout", "feature_flags", "serialization_max_items_length", "serialization_max_text_length", - "frontend_timeout", "auto_saving", "auto_saving_interval", "health_check_max_retries", - "max_file_size_upload", "webhook_polling_interval", "public_flow_cleanup_interval", "public_flow_expiration", - "event_delivery", "webhook_auth_enable", - "voice_mode_available", "default_folder_name", "hide_getting_started_progress" ], - "title": "ConfigResponse" + "title": "ConfigResponse", + "description": "Full configuration response for authenticated users.\n\nThe 'type' field is a discriminator to distinguish from PublicConfigResponse." }, "ContentBlock": { "properties": { @@ -6613,31 +6991,24 @@ "type": "object", "title": "FeatureFlags" }, - "File": { + "Flow": { "properties": { - "id": { - "type": "string", - "format": "uuid", - "title": "Id" - }, - "user_id": { - "type": "string", - "format": "uuid", - "title": "User Id" - }, "name": { "type": "string", "title": "Name" }, - "path": { - "type": "string", - "title": "Path" - }, - "size": { - "type": "integer", - "title": "Size" + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Description" }, - "provider": { + "icon": { "anyOf": [ { "type": "string" @@ -6646,55 +7017,7 @@ "type": "null" } ], - "title": "Provider" - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "title": "Updated At" - } - }, - "type": "object", - "required": [ - "user_id", - "name", - "path", - "size" - ], - "title": "File" - }, - "Flow": { - "properties": { - "name": { - "type": "string", - "title": "Name" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "icon": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Icon" + "title": "Icon" }, "icon_bg_color": { "anyOf": [ @@ -8226,6 +8549,78 @@ "title": "MCPProjectUpdateRequest", "description": "Request model for updating MCP project settings including auth." }, + "MCPServerConfig": { + "properties": { + "command": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Command" + }, + "args": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Args" + }, + "env": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Env" + }, + "headers": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Headers" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Url" + } + }, + "additionalProperties": true, + "type": "object", + "title": "MCPServerConfig", + "description": "Pydantic model for MCP server configuration." + }, "MCPSettings": { "properties": { "id": { @@ -8898,11 +9293,72 @@ "type": "array", "title": "Targets", "default": [] + }, + "usage": { + "anyOf": [ + { + "$ref": "#/components/schemas/Usage" + }, + { + "type": "null" + } + ] + }, + "build_duration": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Build Duration" } }, "type": "object", "title": "Properties" }, + "PublicConfigResponse": { + "properties": { + "max_file_size_upload": { + "type": "integer", + "title": "Max File Size Upload" + }, + "event_delivery": { + "type": "string", + "enum": [ + "polling", + "streaming", + "direct" + ], + "title": "Event Delivery" + }, + "voice_mode_available": { + "type": "boolean", + "title": "Voice Mode Available" + }, + "frontend_timeout": { + "type": "integer", + "title": "Frontend Timeout" + }, + "type": { + "type": "string", + "const": "public", + "title": "Type", + "default": "public" + } + }, + "type": "object", + "required": [ + "max_file_size_upload", + "event_delivery", + "voice_mode_available", + "frontend_timeout" + ], + "title": "PublicConfigResponse", + "description": "Configuration response for public/unauthenticated endpoints like the public playground.\n\nContains only the configuration values needed for public features, without sensitive data.\nThe 'type' field is a discriminator to distinguish from full ConfigResponse." + }, "ResultData": { "properties": { "results": { @@ -9212,93 +9668,195 @@ "type": "object", "title": "Source" }, - "TextContent": { - "additionalProperties": true, - "type": "object" - }, - "ToolContent": { - "additionalProperties": true, - "type": "object" - }, - "TransactionLogsResponse": { + "SpanReadResponse": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, - "timestamp": { + "name": { "type": "string", - "format": "date-time", - "title": "Timestamp" + "title": "Name" }, - "vertex_id": { - "type": "string", - "title": "Vertex Id" + "type": { + "$ref": "#/components/schemas/SpanType" }, - "target_id": { + "status": { + "$ref": "#/components/schemas/SpanStatus" + }, + "startTime": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Target Id" + "title": "Starttime" + }, + "endTime": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Endtime" + }, + "latencyMs": { + "type": "integer", + "title": "Latencyms" }, "inputs": { - "additionalProperties": true, - "type": "object", + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], "title": "Inputs" }, "outputs": { - "additionalProperties": true, - "type": "object", + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], "title": "Outputs" }, - "status": { - "type": "string", - "title": "Status" + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Error" + }, + "modelName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Modelname" + }, + "tokenUsage": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Tokenusage" + }, + "children": { + "items": { + "$ref": "#/components/schemas/SpanReadResponse" + }, + "type": "array", + "title": "Children" } }, "type": "object", "required": [ "id", - "vertex_id", - "status" - ], - "title": "TransactionLogsResponse", - "description": "Transaction response model for logs view - excludes error and flow_id fields." + "name", + "type", + "status", + "startTime", + "endTime", + "latencyMs", + "inputs", + "outputs", + "error", + "modelName", + "tokenUsage" + ], + "title": "SpanReadResponse", + "description": "Response model for a single span, with nested children.\n\nSerializes to camelCase JSON to match the frontend API contract." + }, + "SpanStatus": { + "type": "string", + "enum": [ + "unset", + "ok", + "error" + ], + "title": "SpanStatus", + "description": "OpenTelemetry status codes.\n\n- UNSET: Default status, span has not ended yet\n- OK: Span completed successfully\n- ERROR: Span completed with an error" }, - "Tweaks": { - "additionalProperties": { - "anyOf": [ - { - "type": "string" + "SpanType": { + "type": "string", + "enum": [ + "chain", + "llm", + "tool", + "retriever", + "embedding", + "parser", + "agent" + ], + "title": "SpanType", + "description": "Types of spans that can be recorded." + }, + "TextContent": { + "additionalProperties": true, + "type": "object" + }, + "ToolContent": { + "additionalProperties": true, + "type": "object" + }, + "TraceListResponse": { + "properties": { + "traces": { + "items": { + "$ref": "#/components/schemas/TraceSummaryRead" }, - { - "additionalProperties": true, - "type": "object" - } - ] + "type": "array", + "title": "Traces" + }, + "total": { + "type": "integer", + "title": "Total" + }, + "pages": { + "type": "integer", + "title": "Pages" + } }, "type": "object", - "title": "Tweaks", - "description": "A dictionary of tweaks to adjust the flow's execution. Allows customizing flow behavior dynamically. All tweaks are overridden by the input values.", - "examples": [ - { - "Component Name": { - "parameter_name": "value" - }, - "component_id": { - "parameter_name": "value" - }, - "parameter_name": "value" - } - ] + "required": [ + "traces", + "total", + "pages" + ], + "title": "TraceListResponse", + "description": "Paginated list response for traces." }, - "UploadFileResponse": { + "TraceRead": { "properties": { "id": { "type": "string", @@ -9309,48 +9867,51 @@ "type": "string", "title": "Name" }, - "path": { - "type": "string", - "format": "path", - "title": "Path" + "status": { + "$ref": "#/components/schemas/SpanStatus" }, - "size": { - "type": "integer", - "title": "Size" + "startTime": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Starttime" }, - "provider": { + "endTime": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Provider" - } - }, - "type": "object", - "required": [ - "id", - "name", - "path", - "size" - ], - "title": "UploadFileResponse", - "description": "File upload response schema." - }, - "UserCreate": { - "properties": { - "username": { + "title": "Endtime" + }, + "totalLatencyMs": { + "type": "integer", + "title": "Totallatencyms" + }, + "totalTokens": { + "type": "integer", + "title": "Totaltokens" + }, + "flowId": { "type": "string", - "title": "Username" + "format": "uuid", + "title": "Flowid" }, - "password": { + "sessionId": { "type": "string", - "title": "Password" + "title": "Sessionid" }, - "optins": { + "input": { "anyOf": [ { "additionalProperties": true, @@ -9360,85 +9921,99 @@ "type": "null" } ], - "title": "Optins", - "default": { - "github_starred": false, - "dialog_dismissed": false, - "discord_clicked": false - } + "title": "Input" + }, + "output": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Output" + }, + "spans": { + "items": { + "$ref": "#/components/schemas/SpanReadResponse" + }, + "type": "array", + "title": "Spans" } }, "type": "object", "required": [ - "username", - "password" + "id", + "name", + "status", + "startTime", + "endTime", + "totalLatencyMs", + "totalTokens", + "flowId", + "sessionId" ], - "title": "UserCreate" + "title": "TraceRead", + "description": "Response model for a single trace with its hierarchical span tree.\n\nSerializes to camelCase JSON to match the frontend API contract." }, - "UserRead": { + "TraceSummaryRead": { "properties": { "id": { "type": "string", "format": "uuid", "title": "Id" }, - "username": { + "name": { "type": "string", - "title": "Username" + "title": "Name" }, - "profile_image": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Profile Image" + "status": { + "$ref": "#/components/schemas/SpanStatus" }, - "store_api_key": { + "startTime": { "anyOf": [ { - "type": "string" + "type": "string", + "format": "date-time" }, { "type": "null" } ], - "title": "Store Api Key" + "title": "Starttime" }, - "is_active": { - "type": "boolean", - "title": "Is Active" + "totalLatencyMs": { + "type": "integer", + "title": "Totallatencyms" }, - "is_superuser": { - "type": "boolean", - "title": "Is Superuser" + "totalTokens": { + "type": "integer", + "title": "Totaltokens" }, - "create_at": { + "flowId": { "type": "string", - "format": "date-time", - "title": "Create At" + "format": "uuid", + "title": "Flowid" }, - "updated_at": { + "sessionId": { "type": "string", - "format": "date-time", - "title": "Updated At" + "title": "Sessionid" }, - "last_login_at": { + "input": { "anyOf": [ { - "type": "string", - "format": "date-time" + "additionalProperties": true, + "type": "object" }, { "type": "null" } ], - "title": "Last Login At" + "title": "Input" }, - "optins": { + "output": { "anyOf": [ { "additionalProperties": true, @@ -9448,80 +10023,323 @@ "type": "null" } ], - "title": "Optins" + "title": "Output" } }, "type": "object", "required": [ - "username", - "profile_image", - "store_api_key", - "is_active", - "is_superuser", - "create_at", - "updated_at", - "last_login_at" + "id", + "name", + "status", + "startTime", + "totalLatencyMs", + "totalTokens", + "flowId", + "sessionId" ], - "title": "UserRead" + "title": "TraceSummaryRead", + "description": "Lightweight trace model for list endpoint.\n\nSerializes to camelCase JSON to match the frontend API contract." }, - "UserUpdate": { + "TransactionLogsResponse": { "properties": { - "username": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Username" + "id": { + "type": "string", + "format": "uuid", + "title": "Id" }, - "profile_image": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Profile Image" + "timestamp": { + "type": "string", + "format": "date-time", + "title": "Timestamp" }, - "password": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Password" + "vertex_id": { + "type": "string", + "title": "Vertex Id" }, - "is_active": { + "target_id": { "anyOf": [ { - "type": "boolean" + "type": "string" }, { "type": "null" } ], - "title": "Is Active" + "title": "Target Id" }, - "is_superuser": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "Is Superuser" + "inputs": { + "additionalProperties": true, + "type": "object", + "title": "Inputs" }, - "last_login_at": { + "outputs": { + "additionalProperties": true, + "type": "object", + "title": "Outputs" + }, + "status": { + "type": "string", + "title": "Status" + } + }, + "type": "object", + "required": [ + "id", + "vertex_id", + "status" + ], + "title": "TransactionLogsResponse", + "description": "Transaction response model for logs view - excludes error and flow_id fields." + }, + "Tweaks": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": true, + "type": "object" + } + ] + }, + "type": "object", + "title": "Tweaks", + "description": "A dictionary of tweaks to adjust the flow's execution. Allows customizing flow behavior dynamically. All tweaks are overridden by the input values.", + "examples": [ + { + "Component Name": { + "parameter_name": "value" + }, + "component_id": { + "parameter_name": "value" + }, + "parameter_name": "value" + } + ] + }, + "Usage": { + "properties": { + "input_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Input Tokens" + }, + "output_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Output Tokens" + }, + "total_tokens": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Total Tokens" + } + }, + "type": "object", + "title": "Usage", + "description": "Token usage information from LLM responses." + }, + "UserCreate": { + "properties": { + "username": { + "type": "string", + "title": "Username" + }, + "password": { + "type": "string", + "title": "Password" + }, + "optins": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Optins", + "default": { + "github_starred": false, + "dialog_dismissed": false, + "discord_clicked": false + } + } + }, + "type": "object", + "required": [ + "username", + "password" + ], + "title": "UserCreate" + }, + "UserRead": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "username": { + "type": "string", + "title": "Username" + }, + "profile_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Profile Image" + }, + "store_api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Store Api Key" + }, + "is_active": { + "type": "boolean", + "title": "Is Active" + }, + "is_superuser": { + "type": "boolean", + "title": "Is Superuser" + }, + "create_at": { + "type": "string", + "format": "date-time", + "title": "Create At" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" + }, + "last_login_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Last Login At" + }, + "optins": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Optins" + } + }, + "type": "object", + "required": [ + "username", + "profile_image", + "store_api_key", + "is_active", + "is_superuser", + "create_at", + "updated_at", + "last_login_at" + ], + "title": "UserRead" + }, + "UserUpdate": { + "properties": { + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Username" + }, + "profile_image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Profile Image" + }, + "password": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Password" + }, + "is_active": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Active" + }, + "is_superuser": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Is Superuser" + }, + "last_login_at": { "anyOf": [ { "type": "string", @@ -9593,6 +10411,13 @@ "type": { "type": "string", "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" } }, "type": "object", @@ -9726,218 +10551,9 @@ }, "flow_id": { "type": "string", - "title": "Flow Id" - }, - "inputs": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Inputs", - "description": "Component-specific inputs in flat format: 'component_id.param_name': value" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "flow_id" - ], - "title": "WorkflowExecutionRequest", - "description": "Request schema for workflow execution.", - "examples": [ - { - "background": false, - "flow_id": "flow_67ccd2be17f0819081ff3bb2cf6508e60bb6a6b452d3795b", - "inputs": { - "ChatInput-abc.input_value": "Hello, how can you help me today?", - "ChatInput-abc.session_id": "session-123", - "LLM-xyz.max_tokens": 100, - "LLM-xyz.temperature": 0.7, - "OpenSearch-def.opensearch_url": "https://opensearch:9200" - }, - "stream": false - }, - { - "background": true, - "flow_id": "flow_67ccd2be17f0819081ff3bb2cf6508e60bb6a6b452d3795b", - "inputs": { - "ChatInput-abc.input_value": "Process this in the background" - }, - "stream": false - }, - { - "background": false, - "flow_id": "flow_67ccd2be17f0819081ff3bb2cf6508e60bb6a6b452d3795b", - "inputs": { - "ChatInput-abc.input_value": "Stream this conversation" - }, - "stream": true - } - ] - }, - "WorkflowStopRequest": { - "properties": { - "job_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "uuid" - } - ], - "title": "Job Id" - } - }, - "type": "object", - "required": [ - "job_id" - ], - "title": "WorkflowStopRequest", - "description": "Request schema for stopping workflow." - }, - "WorkflowStopResponse": { - "properties": { - "job_id": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string", - "format": "uuid" - } - ], - "title": "Job Id" - }, - "message": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Message" - } - }, - "type": "object", - "required": [ - "job_id" - ], - "title": "WorkflowStopResponse", - "description": "Response schema for stopping workflow." - }, - "langflow__api__v1__schemas__UploadFileResponse": { - "properties": { - "flowId": { - "type": "string", - "title": "Flowid" - }, - "file_path": { - "type": "string", - "format": "path", - "title": "File Path" - } - }, - "type": "object", - "required": [ - "flowId", - "file_path" - ], - "title": "UploadFileResponse", - "description": "Upload file response schema." - }, - "lfx__utils__schemas__File": { - "properties": { - "path": { - "type": "string", - "title": "Path" - }, - "name": { - "type": "string", - "title": "Name" - }, - "type": { - "type": "string", - "title": "Type" - } - }, - "type": "object", - "required": [ - "path", - "name", - "type" - ], - "title": "File", - "description": "File schema." - }, - "ComponentOutput": { - "description": "Component output schema.", - "properties": { - "type": { - "description": "Type of the component output (e.g., 'message', 'data', 'tool', 'text')", - "title": "Type", - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/JobStatus" - }, - "content": { - "anyOf": [ - {}, - { - "type": "null" - } - ], - "title": "Content" - }, - "metadata": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Metadata" - } - }, - "required": [ - "type", - "status" - ], - "title": "ComponentOutput", - "type": "object" - }, - "ErrorDetail": { - "description": "Error detail schema.", - "properties": { - "error": { - "title": "Error", - "type": "string" - }, - "code": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Code" + "title": "Flow Id" }, - "details": { + "inputs": { "anyOf": [ { "additionalProperties": true, @@ -9947,288 +10563,243 @@ "type": "null" } ], - "title": "Details" + "title": "Inputs", + "description": "Component-specific inputs in flat format: 'component_id.param_name': value" } }, + "additionalProperties": false, + "type": "object", "required": [ - "error" + "flow_id" ], - "title": "ErrorDetail", - "type": "object" - }, - "JobStatus": { - "description": "Job execution status.", - "enum": [ - "queued", - "in_progress", - "completed", - "failed", - "cancelled", - "timed_out" - ], - "title": "JobStatus", - "type": "string" + "title": "WorkflowExecutionRequest", + "description": "Request schema for workflow execution.", + "examples": [ + { + "background": false, + "flow_id": "flow_67ccd2be17f0819081ff3bb2cf6508e60bb6a6b452d3795b", + "inputs": { + "ChatInput-abc.input_value": "Hello, how can you help me today?", + "ChatInput-abc.session_id": "session-123", + "LLM-xyz.max_tokens": 100, + "LLM-xyz.temperature": 0.7, + "OpenSearch-def.opensearch_url": "https://opensearch:9200" + }, + "stream": false + }, + { + "background": true, + "flow_id": "flow_67ccd2be17f0819081ff3bb2cf6508e60bb6a6b452d3795b", + "inputs": { + "ChatInput-abc.input_value": "Process this in the background" + }, + "stream": false + }, + { + "background": false, + "flow_id": "flow_67ccd2be17f0819081ff3bb2cf6508e60bb6a6b452d3795b", + "inputs": { + "ChatInput-abc.input_value": "Stream this conversation" + }, + "stream": true + } + ] }, - "ErrorDetail_1": { - "description": "Error detail schema.", + "WorkflowStopRequest": { "properties": { - "error": { - "title": "Error", - "type": "string" - }, - "code": { + "job_id": { "anyOf": [ { "type": "string" }, { - "type": "null" - } - ], - "title": "Code" - }, - "details": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" + "type": "string", + "format": "uuid" } ], - "title": "Details" + "title": "Job Id" } }, + "type": "object", "required": [ - "error" + "job_id" ], - "title": "ErrorDetail", - "type": "object" - }, - "JobStatus_1": { - "description": "Job execution status.", - "enum": [ - "queued", - "in_progress", - "completed", - "failed", - "cancelled", - "timed_out" - ], - "title": "JobStatus", - "type": "string" + "title": "WorkflowStopRequest", + "description": "Request schema for stopping workflow." }, - "ComponentOutput_1": { - "description": "Component output schema.", + "WorkflowStopResponse": { "properties": { - "type": { - "description": "Type of the component output (e.g., 'message', 'data', 'tool', 'text')", - "title": "Type", - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/JobStatus" - }, - "content": { + "job_id": { "anyOf": [ - {}, { - "type": "null" + "type": "string" + }, + { + "type": "string", + "format": "uuid" } ], - "title": "Content" + "title": "Job Id" }, - "metadata": { + "message": { "anyOf": [ { - "additionalProperties": true, - "type": "object" + "type": "string" }, { "type": "null" } ], - "title": "Metadata" + "title": "Message" } }, + "type": "object", "required": [ - "type", - "status" + "job_id" ], - "title": "ComponentOutput", - "type": "object" + "title": "WorkflowStopResponse", + "description": "Response schema for stopping workflow." }, - "ErrorDetail_2": { - "description": "Error detail schema.", + "langflow__api__schemas__UploadFileResponse": { "properties": { - "error": { - "title": "Error", - "type": "string" + "id": { + "type": "string", + "format": "uuid", + "title": "Id" }, - "code": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Code" + "name": { + "type": "string", + "title": "Name" + }, + "path": { + "type": "string", + "format": "path", + "title": "Path" }, - "details": { + "size": { + "type": "integer", + "title": "Size" + }, + "provider": { "anyOf": [ { - "additionalProperties": true, - "type": "object" + "type": "string" }, { "type": "null" } ], - "title": "Details" + "title": "Provider" } }, + "type": "object", "required": [ - "error" + "id", + "name", + "path", + "size" ], - "title": "ErrorDetail", - "type": "object" + "title": "UploadFileResponse", + "description": "File upload response schema." }, - "JobStatus_2": { - "description": "Job execution status.", - "enum": [ - "queued", - "in_progress", - "completed", - "failed", - "cancelled", - "timed_out" - ], - "title": "JobStatus", - "type": "string" + "langflow__api__v1__schemas__UploadFileResponse": { + "properties": { + "flowId": { + "type": "string", + "title": "Flowid" + }, + "file_path": { + "type": "string", + "format": "path", + "title": "File Path" + } + }, + "type": "object", + "required": [ + "flowId", + "file_path" + ], + "title": "UploadFileResponse", + "description": "Upload file response schema." }, - "WorkflowExecutionResponse": { - "description": "Synchronous workflow execution response.", + "langflow__services__database__models__file__model__File": { "properties": { - "flow_id": { - "title": "Flow Id", - "type": "string" + "id": { + "type": "string", + "format": "uuid", + "title": "Id" }, - "job_id": { + "user_id": { + "type": "string", + "format": "uuid", + "title": "User Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "path": { + "type": "string", + "title": "Path" + }, + "size": { + "type": "integer", + "title": "Size" + }, + "provider": { "anyOf": [ { "type": "string" }, - { - "format": "uuid", - "type": "string" - }, { "type": "null" } ], - "title": "Job Id" - }, - "object": { - "const": "response", - "default": "response", - "title": "Object", - "type": "string" - }, - "created_timestamp": { - "title": "Created Timestamp", - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/JobStatus" - }, - "errors": { - "default": [], - "items": { - "$ref": "#/components/schemas/ErrorDetail" - }, - "title": "Errors", - "type": "array" + "title": "Provider" }, - "inputs": { - "additionalProperties": true, - "default": {}, - "title": "Inputs", - "type": "object" + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" }, - "outputs": { - "additionalProperties": { - "$ref": "#/components/schemas/ComponentOutput" - }, - "default": {}, - "title": "Outputs", - "type": "object" + "updated_at": { + "type": "string", + "format": "date-time", + "title": "Updated At" } }, + "type": "object", "required": [ - "flow_id", - "status" + "user_id", + "name", + "path", + "size" ], - "title": "WorkflowExecutionResponse", - "type": "object" + "title": "File" }, - "WorkflowJobResponse": { - "description": "Background job response.", + "lfx__utils__schemas__File": { "properties": { - "job_id": { - "anyOf": [ - { - "type": "string" - }, - { - "format": "uuid", - "type": "string" - } - ], - "title": "Job Id" - }, - "flow_id": { - "title": "Flow Id", - "type": "string" - }, - "object": { - "const": "job", - "default": "job", - "title": "Object", - "type": "string" - }, - "created_timestamp": { - "title": "Created Timestamp", - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/JobStatus_1" + "path": { + "type": "string", + "title": "Path" }, - "links": { - "additionalProperties": { - "type": "string" - }, - "title": "Links", - "type": "object" + "name": { + "type": "string", + "title": "Name" }, - "errors": { - "default": [], - "items": { - "$ref": "#/components/schemas/ErrorDetail_1" - }, - "title": "Errors", - "type": "array" + "type": { + "type": "string", + "title": "Type" } }, + "type": "object", "required": [ - "job_id", - "flow_id", - "status" + "path", + "name", + "type" ], - "title": "WorkflowJobResponse", - "type": "object" + "title": "File", + "description": "File schema." } }, "securitySchemes": { @@ -10253,4 +10824,4 @@ } } } -} \ No newline at end of file +} From 1fdbbfaab16f0df07a70b49563805a69210c5059 Mon Sep 17 00:00:00 2001 From: Mendon Kissling <59585235+mendonk@users.noreply.github.com> Date: Fri, 13 Mar 2026 09:40:50 -0400 Subject: [PATCH 11/29] docs: uv included in 1.8.1 images (#12135) * docs-uvx-included-in-181 * position * version-in-docker-docs * clarify-with-includes --- docs/docs/Deployment/deployment-docker.mdx | 4 ++-- docs/docs/Support/release-notes.mdx | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/docs/Deployment/deployment-docker.mdx b/docs/docs/Deployment/deployment-docker.mdx index 1217fc1f4c50..6a58e5ea488e 100644 --- a/docs/docs/Deployment/deployment-docker.mdx +++ b/docs/docs/Deployment/deployment-docker.mdx @@ -272,7 +272,7 @@ For example, this Docker Compose file uses a bind mount for Langflow data (`./la This approach keeps the persistent volumes separate from the Langflow container, so you can upgrade the Langflow application without losing data. -If you need to upgrade to a custom image based on a Langflow release, such as to add `uv` in `1.8.x`, first build a derived image from the official image, and then follow the same steps above. +If you need to upgrade to a custom image based on a Langflow release, such as to add `uv` in `1.8.0`, first build a derived image from the official image, and then follow the same steps above. Set the custom image in your compose file or `docker run`, and then pull and restart. -For a minimal Dockerfile that adds `uv` to the 1.8.x image, see the [release notes](/release-notes) (“Docker image no longer includes uv or uvx”). \ No newline at end of file +For a minimal Dockerfile that adds `uv` to the 1.8.0 image, see the [release notes](/release-notes) (“Docker image no longer includes uv or uvx”). \ No newline at end of file diff --git a/docs/docs/Support/release-notes.mdx b/docs/docs/Support/release-notes.mdx index b9127ed9800a..77c22270dc81 100644 --- a/docs/docs/Support/release-notes.mdx +++ b/docs/docs/Support/release-notes.mdx @@ -80,12 +80,17 @@ For all changes, see the [Changelog](https://github.com/langflow-ai/langflow/rel For more information about available optional dependency groups, see [Install optional dependency groups for `langflow-base`](/install-custom-dependencies#install-optional-dependency-groups-for-langflow-base). -- Docker image no longer includes `uv` or `uvx` +- Docker image does not include `uv` or `uvx` - In Langflow 1.8.x, the Langflow Docker image no longer includes `uv` or `uvx` in the runtime image. + :::tip + Starting with Langflow 1.8.1, the official Docker images include `uv` and `uvx` again. + If you're using Langflow 1.8.0, follow the steps in this release note to add `uv` and `uvx` in a derived image. + ::: + + In Langflow 1.8.0, the Langflow Docker image does not include `uv` or `uvx` in the runtime image. This means that MCP server configurations, including the default Langflow MCP server, that rely on commands like `uvx mcp-proxy` will fail inside the container with a `command not found` error. - If you use MCP from within a Langflow 1.8.x Docker image, you must install `uv` in an image derived from the official `langflowai/langflow` image. + If you use MCP from within a Langflow 1.8.0 Docker image, you must install `uv` in an image derived from the official `langflowai/langflow` image. To install `uv` and `uvx` in a derived image based on the official `langflowai/langflow` image, do the following: From 73b6612e3ef25fdae0a752d75b0fabd47328d4f0 Mon Sep 17 00:00:00 2001 From: Janardan Singh Kavia Date: Fri, 13 Mar 2026 19:14:58 +0530 Subject: [PATCH 12/29] fix: prevent RCE via data parameter in build_public_tmp endpoint (#12160) * fix: prevent RCE via data parameter in build_public_tmp endpoint * [autofix.ci] apply automated fixes --------- Co-authored-by: Janardan S Kavia Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/backend/base/langflow/api/v1/chat.py | 11 ++- src/backend/tests/unit/test_chat_endpoint.py | 76 ++++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/backend/base/langflow/api/v1/chat.py b/src/backend/base/langflow/api/v1/chat.py index 427577d7432b..3a6ff1bd70d9 100644 --- a/src/backend/base/langflow/api/v1/chat.py +++ b/src/backend/base/langflow/api/v1/chat.py @@ -583,7 +583,6 @@ async def build_public_tmp( background_tasks: LimitVertexBuildBackgroundTasks, flow_id: uuid.UUID, inputs: Annotated[InputValueRequest | None, Body(embed=True)] = None, - data: Annotated[FlowDataRequest | None, Body(embed=True)] = None, files: list[str] | None = None, stop_component_id: str | None = None, start_component_id: str | None = None, @@ -598,10 +597,16 @@ async def build_public_tmp( This endpoint is specifically for public flows that don't require authentication. It uses a client_id cookie to create a deterministic flow ID for tracking purposes. + Security Note: + - The 'data' parameter is NOT accepted to prevent flow definition tampering + - Public flows must execute the stored flow definition only + - The flow definition is always loaded from the database + The endpoint: 1. Verifies the requested flow is marked as public in the database 2. Creates a deterministic UUID based on client_id and flow_id 3. Uses the flow owner's permissions to build the flow + 4. Always loads the flow definition from the database Requirements: - The flow must be marked as PUBLIC in the database @@ -611,7 +616,6 @@ async def build_public_tmp( flow_id: UUID of the public flow to build background_tasks: Background tasks manager inputs: Optional input values for the flow - data: Optional flow data files: Optional files to include stop_component_id: Optional ID of component to stop at start_component_id: Optional ID of component to start from @@ -630,11 +634,12 @@ async def build_public_tmp( owner_user, new_flow_id = await verify_public_flow_and_get_user(flow_id=flow_id, client_id=client_id) # Start the flow build using the new flow ID + # data is always None for public flows - they load from database only job_id = await start_flow_build( flow_id=new_flow_id, background_tasks=background_tasks, inputs=inputs, - data=data, + data=None, # Always None - public flows load from database only files=files, stop_component_id=stop_component_id, start_component_id=start_component_id, diff --git a/src/backend/tests/unit/test_chat_endpoint.py b/src/backend/tests/unit/test_chat_endpoint.py index 0021e06eac45..c0c09ae378ac 100644 --- a/src/backend/tests/unit/test_chat_endpoint.py +++ b/src/backend/tests/unit/test_chat_endpoint.py @@ -432,3 +432,79 @@ async def mock_cancel_flow_build_with_cancelled_error(*_args, **_kwargs): finally: # Restore the original function to avoid affecting other tests monkeypatch.setattr(langflow.api.v1.chat, "cancel_flow_build", original_cancel_flow_build) + + +@pytest.mark.benchmark +async def test_build_public_tmp_ignores_data_parameter(client, json_memory_chatbot_no_llm, logged_in_headers): + """Test that build_public_tmp endpoint silently ignores data parameter for security. + + Security Test: Verifies that when a user attempts to provide custom flow data + to the public flow endpoint, FastAPI silently ignores the extra parameter and + the endpoint functions normally using the stored flow data from the database. + """ + # Create a flow + flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers) + + # Make the flow public + response = await client.patch( + f"api/v1/flows/{flow_id}", + json={"access_type": "PUBLIC"}, + headers=logged_in_headers, + ) + assert response.status_code == codes.OK + + # Create malicious flow data with different structure + malicious_data = {"nodes": [{"id": "malicious", "data": {"type": "CustomComponent"}}], "edges": []} + + # Set a client_id cookie + client.cookies.set("client_id", "test-security-client-123") + + # Attempt to build with malicious data - FastAPI will silently ignore it + response = await client.post( + f"api/v1/build_public_tmp/{flow_id}/flow", + json={ + "inputs": {"session": "test_session"}, + "data": malicious_data, # This will be silently ignored by FastAPI + }, + headers={"Content-Type": "application/json"}, + ) + + # Verify the request succeeded - the data parameter is simply ignored + assert response.status_code == codes.OK + response_data = response.json() + assert "job_id" in response_data + + +@pytest.mark.benchmark +async def test_build_public_tmp_without_data_parameter(client, json_memory_chatbot_no_llm, logged_in_headers): + """Test that build_public_tmp endpoint works without data parameter. + + Security Test: Verifies that when no data parameter is provided, the endpoint + works normally and returns a job_id. This proves the data parameter is optional + and the stored flow definition is always used. + """ + # Create a flow + flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers) + + # Make the flow public + response = await client.patch( + f"api/v1/flows/{flow_id}", + json={"access_type": "PUBLIC"}, + headers=logged_in_headers, + ) + assert response.status_code == codes.OK + + # Set a client_id cookie + client.cookies.set("client_id", "test-no-data-client") + + # Build without providing data parameter + response = await client.post( + f"api/v1/build_public_tmp/{flow_id}/flow", + json={"inputs": {"session": "test_session"}}, + headers={"Content-Type": "application/json"}, + ) + + # Verify the request succeeded + assert response.status_code == codes.OK + response_data = response.json() + assert "job_id" in response_data From 7838d0d282cd50065c02579777ef23328aa15086 Mon Sep 17 00:00:00 2001 From: Aryaman Sinha <106659803+AryamanSi17@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:29:17 +0530 Subject: [PATCH 13/29] fix: Postgres JSON column fails with "Token NaN is invalid" when Agent stores message (psycopg InvalidTextRepresentation) (#11977) * Refactor message model and validation methods * [autofix.ci] apply automated fixes * fix: address reviewer feedback on NaN sanitization PR - Remove broken else branch in from_message that called file.path on str (fixes AttributeError when message.files contains plain strings) - Restore # type: ignore comments for SQLModel/mypy compatibility - Move import math to module level instead of inside method - Add TestSanitizeJson with 13 tests covering nan/inf/nested/Decimal cases * test: actually inject float('inf') in content_blocks test TextContent.duration is int | None so Pydantic rejects float('inf') at construction. Use a raw dict to bypass validation and genuinely exercise the _sanitize_json path, then assert duration is None after sanitization and the full JSON round-trip produces no NaN/Inf values. * fix: resolve all Ruff CI lint errors model.py: - EM101/TRY003: assign exception messages to variables before raising test_messages.py: - D205: add blank line between docstring summary and description - FBT003: avoid boolean positional arg by assigning to variable first - PLW0177: replace != float('nan') with math.isnan() * fix: D415 docstring period and I001 import sort * style: fix comment spacing (2 spaces before inline comment) * refactor: address maintainer feedback on NaN sanitization - Use math.isfinite() for cleaner NaN/Inf checks - Add docstring to _sanitize_json explaining PostgreSQL jsonb constraints - Document redundant sanitization in serializer as a defensive measure - Simplify NaN assertion in properties test for better clarity * fix: remove unused math import in test * style: match CI formatting after import removal * fix: preserve string file paths in mixed lists Corrected regression where string paths were lost if a message contained both string paths and Image objects. Updated the corresponding backend test to assert preservation of mixed lists. * fix: relocate tests to unit and fix NaN properties validation test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida --- .../services/database/models/message/model.py | 87 ++++--- src/backend/tests/{ => unit}/test_messages.py | 226 +++++++++++++++++- 2 files changed, 279 insertions(+), 34 deletions(-) rename src/backend/tests/{ => unit}/test_messages.py (81%) diff --git a/src/backend/base/langflow/services/database/models/message/model.py b/src/backend/base/langflow/services/database/models/message/model.py index 6c1c3269b137..864b39c3f22f 100644 --- a/src/backend/base/langflow/services/database/models/message/model.py +++ b/src/backend/base/langflow/services/database/models/message/model.py @@ -1,4 +1,5 @@ import json +import math from datetime import datetime, timezone from typing import TYPE_CHECKING, Annotated from uuid import UUID, uuid4 @@ -38,32 +39,31 @@ def serialize_timestamp(self, value): if value.tzinfo is None: value = value.replace(tzinfo=timezone.utc) return value.strftime("%Y-%m-%d %H:%M:%S %Z") + if isinstance(value, str): - # Make sure the timestamp is in UTC value = datetime.fromisoformat(value).replace(tzinfo=timezone.utc) return value.strftime("%Y-%m-%d %H:%M:%S %Z") + return value @field_validator("files", mode="before") @classmethod def validate_files(cls, value): - if not value: - value = [] - return value + return value or [] @field_validator("session_id", mode="before") @classmethod def validate_session_id(cls, value): if isinstance(value, UUID): - value = str(value) + return str(value) return value @classmethod def from_message(cls, message: "Message", flow_id: str | UUID | None = None): - # first check if the record has all the required fields if message.text is None or not message.sender or not message.sender_name: msg = "The message does not have the required fields (text, sender, sender_name)." raise ValueError(msg) + if message.files: image_paths = [] for file in message.files: @@ -77,22 +77,23 @@ def from_message(cls, message: "Message", flow_id: str | UUID | None = None): image_paths.append(file.path) else: image_paths.append(file.path) + elif isinstance(file, str): + image_paths.append(file) + if image_paths: message.files = image_paths if isinstance(message.timestamp, str): - # Convert timestamp string in format "YYYY-MM-DD HH:MM:SS UTC" to datetime try: timestamp = datetime.strptime(message.timestamp, "%Y-%m-%d %H:%M:%S %Z").replace(tzinfo=timezone.utc) except ValueError: - # Fallback for ISO format if the above fails timestamp = datetime.fromisoformat(message.timestamp).replace(tzinfo=timezone.utc) else: timestamp = message.timestamp + if not flow_id and message.flow_id: flow_id = message.flow_id - # If the text is not a string, it means it could be - # async iterator so we simply add it as an empty string + message_text = "" if not isinstance(message.text, str) else message.text properties = ( @@ -100,6 +101,7 @@ def from_message(cls, message: "Message", flow_id: str | UUID | None = None): if hasattr(message.properties, "model_dump_json") else message.properties ) + content_blocks = [] for content_block in message.content_blocks or []: content = content_block.model_dump_json() if hasattr(content_block, "model_dump_json") else content_block @@ -129,19 +131,22 @@ def from_message(cls, message: "Message", flow_id: str | UUID | None = None): class MessageTable(MessageBase, table=True): # type: ignore[call-arg] model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True) + __tablename__ = "message" - id: UUID = Field(default_factory=uuid4, primary_key=True) + id: UUID = Field(default_factory=uuid4, primary_key=True) flow_id: UUID | None = Field(default=None) + files: list[str] = Field(sa_column=Column(JSON)) - properties: dict | Properties = Field(default_factory=lambda: Properties().model_dump(), sa_column=Column(JSON)) # type: ignore[assignment] + properties: dict | Properties = Field( # type: ignore[assignment] + default_factory=lambda: Properties().model_dump(), + sa_column=Column(JSON), + ) category: str = Field(sa_column=Column(Text)) - content_blocks: list[dict | ContentBlock] = Field(default_factory=list, sa_column=Column(JSON)) # type: ignore[assignment] - - # We need to make sure the datetimes have timezone after running session.refresh - # because we are losing the timezone information when we save the message to the database - # and when we read it back. We use field_validator to make sure the datetimes have timezone - # after running session.refresh + content_blocks: list[dict | ContentBlock] = Field( # type: ignore[assignment] + default_factory=list, + sa_column=Column(JSON), + ) @field_validator("flow_id", mode="before") @classmethod @@ -149,30 +154,50 @@ def validate_flow_id(cls, value): if value is None: return value if isinstance(value, str): - value = UUID(value) + return UUID(value) + return value + + @staticmethod + def _sanitize_json(value): + """Replace float NaN/Infinity with None to avoid PostgreSQL jsonb rejection.""" + if isinstance(value, float): + if not math.isfinite(value): + return None + return value + + if isinstance(value, dict): + return {k: MessageTable._sanitize_json(v) for k, v in value.items()} + + if isinstance(value, list): + return [MessageTable._sanitize_json(v) for v in value] + return value @field_validator("properties", "content_blocks", mode="before") @classmethod def validate_properties_or_content_blocks(cls, value): if isinstance(value, list): - return [cls.validate_properties_or_content_blocks(item) for item in value] - if hasattr(value, "model_dump"): - return value.model_dump() - if isinstance(value, str): - return json.loads(value) - return value + value = [cls.validate_properties_or_content_blocks(item) for item in value] + elif hasattr(value, "model_dump"): + value = value.model_dump() + elif isinstance(value, str): + value = json.loads(value) + + return cls._sanitize_json(value) @field_serializer("properties", "content_blocks") @classmethod def serialize_properties_or_content_blocks(cls, value) -> dict | list[dict]: + # Redundant sanitization here acts as a defensive measure for rows + # already in the database that might contain NaN/Infinity values. if isinstance(value, list): - return [cls.serialize_properties_or_content_blocks(item) for item in value] - if hasattr(value, "model_dump"): - return value.model_dump() - if isinstance(value, str): - return json.loads(value) - return value + value = [cls.serialize_properties_or_content_blocks(item) for item in value] + elif hasattr(value, "model_dump"): + value = value.model_dump() + elif isinstance(value, str): + value = json.loads(value) + + return cls._sanitize_json(value) class MessageRead(MessageBase): diff --git a/src/backend/tests/test_messages.py b/src/backend/tests/unit/test_messages.py similarity index 81% rename from src/backend/tests/test_messages.py rename to src/backend/tests/unit/test_messages.py index 0ee2f3cde68b..af3ae92e4ad9 100644 --- a/src/backend/tests/test_messages.py +++ b/src/backend/tests/unit/test_messages.py @@ -675,9 +675,10 @@ def test_from_message_mixed_string_and_image_files(self): result = MessageTable.from_message(message, flow_id=uuid4()) - # Only the Image path is included - strings are NOT preserved when image_paths is used - # This is the current behavior: if any Image is processed, message.files = image_paths - assert len(result.files) == 1 + # Both the processed Image path AND the string file path should be preserved. + assert len(result.files) == 2 + assert any("image.png" in f for f in result.files) + assert "string/path/file.txt" in result.files assert result.files[0] == f"{session_id}/image.png" def test_from_message_with_special_characters_in_session_id(self): @@ -1142,3 +1143,222 @@ def test_from_message_missing_required_raises_error(self): with pytest.raises(ValueError, match="required fields"): MessageResponse.from_message(message) + + +# ============================================================================= +# Tests for MessageTable._sanitize_json (NaN / Infinity handling) +# ============================================================================= + + +class TestSanitizeJson: + """Unit tests for MessageTable._sanitize_json and the properties/content_blocks validators.""" + + # ------------------------------------------------------------------ + # Direct _sanitize_json unit tests + # ------------------------------------------------------------------ + + def test_sanitize_nan_float_returns_none(self): + """float('nan') must be replaced with None.""" + from langflow.services.database.models.message.model import MessageTable + + assert MessageTable._sanitize_json(float("nan")) is None + + def test_sanitize_positive_inf_returns_none(self): + """float('inf') must be replaced with None.""" + from langflow.services.database.models.message.model import MessageTable + + assert MessageTable._sanitize_json(float("inf")) is None + + def test_sanitize_negative_inf_returns_none(self): + """float('-inf') must be replaced with None.""" + from langflow.services.database.models.message.model import MessageTable + + assert MessageTable._sanitize_json(float("-inf")) is None + + def test_sanitize_normal_float_preserved(self): + """Normal finite floats must pass through unchanged.""" + from langflow.services.database.models.message.model import MessageTable + + assert MessageTable._sanitize_json(3.14) == pytest.approx(3.14) + + def test_sanitize_nan_nested_in_dict(self): + """NaN inside a dict value is replaced with None.""" + from langflow.services.database.models.message.model import MessageTable + + result = MessageTable._sanitize_json({"score": float("nan"), "label": "ok"}) + assert result["score"] is None + assert result["label"] == "ok" + + def test_sanitize_inf_nested_in_list(self): + """Infinity inside a list is replaced with None.""" + from langflow.services.database.models.message.model import MessageTable + + result = MessageTable._sanitize_json([1.0, float("inf"), 2.0]) + assert result == [1.0, None, 2.0] + + def test_sanitize_deeply_nested_nan(self): + """NaN buried inside a nested dict/list is sanitized recursively.""" + from langflow.services.database.models.message.model import MessageTable + + data = {"outer": {"inner": [float("nan"), {"deep": float("inf")}]}} + result = MessageTable._sanitize_json(data) + assert result["outer"]["inner"][0] is None + assert result["outer"]["inner"][1]["deep"] is None + + def test_sanitize_non_float_types_unchanged(self): + """Strings, ints, bools, and None must not be altered.""" + from langflow.services.database.models.message.model import MessageTable + + assert MessageTable._sanitize_json("hello") == "hello" + assert MessageTable._sanitize_json(42) == 42 + bool_true = True + assert MessageTable._sanitize_json(bool_true) is True + assert MessageTable._sanitize_json(None) is None + + def test_sanitize_decimal_nan(self): + """Decimal('NaN') should be handled gracefully. + + Decimal NaN is neither a float nor passes math.isnan/isinf, so the + current implementation passes it through as-is (it is not a float). + This test documents the current behaviour; if Decimal support is added + later (as suggested in review), update this test accordingly. + """ + import decimal + + from langflow.services.database.models.message.model import MessageTable + + d = decimal.Decimal("NaN") + # Current implementation: non-float types are returned unchanged + result = MessageTable._sanitize_json(d) + assert result is d # passed through unchanged + + # ------------------------------------------------------------------ + # Integration: validator strips NaN before reaching the DB layer + # ------------------------------------------------------------------ + + def test_properties_validator_strips_nan(self): + """NaN inside properties dict is removed by the field validator.""" + from langflow.services.database.models.message.model import MessageTable + + msg = MessageTable( + sender="AI", + sender_name="Bot", + session_id="s1", + text="hi", + properties={"confidence": float("nan"), "model": "gpt-4"}, + ) + + assert msg.properties["confidence"] is None + assert msg.properties["model"] == "gpt-4" + + def test_content_blocks_validator_strips_nan(self): + """NaN inside content_blocks list is removed by the field validator.""" + from langflow.services.database.models.message.model import MessageTable + + msg = MessageTable( + sender="AI", + sender_name="Bot", + session_id="s1", + text="hi", + content_blocks=[{"title": None, "score": float("nan"), "items": [float("inf"), 1.0]}], + ) + + block = msg.content_blocks[0] + assert block["score"] is None + assert block["items"][0] is None + assert block["items"][1] == pytest.approx(1.0) + + def test_from_message_with_nan_in_properties(self): + """from_message correctly sanitizes NaN values inside message.properties.""" + from langflow.schema.properties import Properties + from langflow.services.database.models.message.model import MessageTable + + props = Properties() + # Inject a NaN via a workaround (bypass Pydantic validation) + raw_props = props.model_dump() + raw_props["_nan_test"] = float("nan") + + message = Message( + text="Test", + sender="AI", + sender_name="Bot", + session_id="session", + ) + # Set properties via __dict__ to bypass Pydantic validation (which would drop _nan_test) + # This simulates corrupt/partial data that the sanitizer must handle. + message.__dict__["properties"] = raw_props + + result = MessageTable.from_message(message, flow_id=uuid4()) + + # After from_message + validator, no NaN should survive + + props_dict = result.properties if isinstance(result.properties, dict) else result.properties.model_dump() + assert props_dict["_nan_test"] is None + + def test_from_message_with_inf_in_content_blocks(self): + """from_message sanitizes Infinity inside content_blocks. + + TextContent.duration is typed as int | None, so Pydantic rejects + float('inf') at construction time. We inject Infinity via a raw dict + (the same pattern used by corrupt/partial data arriving from the DB or + an external source) to actually exercise the sanitization path. + """ + import json + import math + + from langflow.services.database.models.message.model import MessageTable + + # Build a raw content-block dict with float('inf') in duration — + # this bypasses Pydantic so the forbidden value reaches _sanitize_json. + raw_block = { + "title": "Block", + "allow_markdown": False, + "contents": [ + { + "type": "text", + "text": "hello", + "duration": float("inf"), # <-- the actual Infinity being tested + "header": {}, + } + ], + "media_url": [], + } + + message = Message( + text="Test", + sender="AI", + sender_name="Bot", + session_id="session", + ) + + # Use MessageTable directly with the raw dict so the field_validator + # (_sanitize_json) is triggered on the Infinity value. + msg_table = MessageTable( + sender=message.sender, + sender_name=message.sender_name, + text=message.text, + session_id=message.session_id, + content_blocks=[raw_block], + ) + + # The validator must have replaced float('inf') with None + block = msg_table.content_blocks[0] + content = block["contents"][0] + assert content["duration"] is None, ( + "float('inf') in duration must be sanitized to None before hitting PostgreSQL" + ) + + # Full JSON round-trip must succeed (no NaN/Inf would raise ValueError) + blocks_json = json.dumps(msg_table.content_blocks) + parsed = json.loads(blocks_json) + + def _has_nan_or_inf(obj): + if isinstance(obj, float): + return not math.isfinite(obj) + if isinstance(obj, dict): + return any(_has_nan_or_inf(v) for v in obj.values()) + if isinstance(obj, list): + return any(_has_nan_or_inf(item) for item in obj) + return False + + assert not _has_nan_or_inf(parsed), "No NaN/Inf values should survive sanitization" From 45114192d017793e5658c6315e02aadcd44440d4 Mon Sep 17 00:00:00 2001 From: Mendon Kissling <59585235+mendonk@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:41:13 -0400 Subject: [PATCH 14/29] docs: replace the file upload utility (#12153) * replace-upload-utility * Update docs/docs/Tutorials/chat-with-files.mdx * Apply suggestions from code review Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com> * pragma-to-pass-tests --- docs/docs/Tutorials/chat-with-files.mdx | 2 +- docs/docs/Tutorials/chat-with-rag.mdx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/docs/Tutorials/chat-with-files.mdx b/docs/docs/Tutorials/chat-with-files.mdx index 04faccc0525d..6bc9d2100a96 100644 --- a/docs/docs/Tutorials/chat-with-files.mdx +++ b/docs/docs/Tutorials/chat-with-files.mdx @@ -70,7 +70,7 @@ This example uses a local Langflow instance, and it asks the LLM to evaluate a s If you don't have a resume on hand, you can download [fake-resume.txt](/files/fake-resume.txt). :::tip -For help with constructing file upload requests in Python, JavaScript, and curl, see the [Langflow File Upload Utility](https://langflow-file-upload-examples.21cgwws14kdz.us-east.codeengine.appdomain.cloud/). +For an example of constructing file upload requests in JavaScript, see the [Create a vector RAG chatbot tutorial](/chat-with-rag#load-data-and-generate-embeddings). ::: 1. To construct the request, gather the following information: diff --git a/docs/docs/Tutorials/chat-with-rag.mdx b/docs/docs/Tutorials/chat-with-rag.mdx index 58d778157063..89bef3a829b7 100644 --- a/docs/docs/Tutorials/chat-with-rag.mdx +++ b/docs/docs/Tutorials/chat-with-rag.mdx @@ -72,8 +72,11 @@ In situations where many users load data or you need to load data programmatical To load data programmatically, use the `/v2/files/` and `/v1/run/$FLOW_ID` endpoints. The first endpoint loads a file to your Langflow server, and then returns an uploaded file path. The second endpoint runs the **Load Data Flow**, referencing the uploaded file path, to chunk, embed, and load the data into the vector store. +:::tip +For an example of constructing file upload requests in Python, see the [Create a chatbot that can ingest files tutorial](/chat-with-files#send-requests-to-your-flow-from-a-python-application). +::: + The following script demonstrates this process. -For help with creating this script, use the [Langflow File Upload Utility](https://langflow-file-upload-examples.21cgwws14kdz.us-east.codeengine.appdomain.cloud/). ```js // Node 18+ example using global fetch, FormData, and Blob From ba8dab1e469cb6739fb2879144dcf314dba06cce Mon Sep 17 00:00:00 2001 From: Mendon Kissling <59585235+mendonk@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:10:55 -0400 Subject: [PATCH 15/29] ci: allow docs-only pull requests to skip tests (#12174) * add-docs-only-to-skip-tests * coderabbit-suggestion --- .github/workflows/ci.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ba22846a34d..571ce2e37dda 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,7 +236,12 @@ jobs: if: | always() && !cancelled() && - needs.set-ci-condition.outputs.should-run-tests == 'true' + needs.set-ci-condition.outputs.should-run-tests == 'true' && + ( + inputs.run-all-tests || + (needs.path-filter.result != 'skipped' && + needs.path-filter.outputs.docs-only != 'true') + ) uses: ./.github/workflows/python_test.yml with: python-versions: ${{ inputs.python-versions || '["3.10"]' }} @@ -253,7 +258,12 @@ jobs: if: | always() && !cancelled() && - needs.set-ci-condition.outputs.should-run-tests == 'true' + needs.set-ci-condition.outputs.should-run-tests == 'true' && + ( + inputs.run-all-tests || + (needs.path-filter.result != 'skipped' && + needs.path-filter.outputs.docs-only != 'true') + ) uses: ./.github/workflows/jest_test.yml with: ref: ${{ inputs.ref || github.ref }} @@ -266,7 +276,12 @@ jobs: if: | always() && !cancelled() && - needs.set-ci-condition.outputs.should-run-tests == 'true' + needs.set-ci-condition.outputs.should-run-tests == 'true' && + ( + inputs.run-all-tests || + (needs.path-filter.result != 'skipped' && + needs.path-filter.outputs.docs-only != 'true') + ) uses: ./.github/workflows/typescript_test.yml with: tests_folder: ${{ inputs.frontend-tests-folder }} From e6d6d2e4b53aff69ae6a2ca6794a4145a5971bcd Mon Sep 17 00:00:00 2001 From: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:38:48 -0400 Subject: [PATCH 16/29] fix: use timezone=true on flow_version created_at field (#12180) * use timezone=true on flow_version created_at field * add refresh on creation of entry --- .../langflow/services/database/models/flow_version/crud.py | 1 + .../langflow/services/database/models/flow_version/model.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/services/database/models/flow_version/crud.py b/src/backend/base/langflow/services/database/models/flow_version/crud.py index 1c1823a7d597..18b104a122b7 100644 --- a/src/backend/base/langflow/services/database/models/flow_version/crud.py +++ b/src/backend/base/langflow/services/database/models/flow_version/crud.py @@ -55,6 +55,7 @@ async def create_flow_version_entry( async with session.begin_nested(): session.add(entry) await session.flush() + await session.refresh(entry) break except IntegrityError as exc: if "unique_flow_version_number" not in str(exc).lower(): diff --git a/src/backend/base/langflow/services/database/models/flow_version/model.py b/src/backend/base/langflow/services/database/models/flow_version/model.py index 70fa6490b63d..f0a4b7ada676 100644 --- a/src/backend/base/langflow/services/database/models/flow_version/model.py +++ b/src/backend/base/langflow/services/database/models/flow_version/model.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, computed_field, field_serializer from pydantic import Field as PydanticField -from sqlalchemy import CheckConstraint, Column, ForeignKey, UniqueConstraint +from sqlalchemy import CheckConstraint, Column, DateTime, ForeignKey, UniqueConstraint, func from sqlmodel import JSON, Field, SQLModel @@ -23,7 +23,9 @@ class FlowVersion(SQLModel, table=True): # type: ignore[call-arg] data: dict | None = Field(default=None, sa_column=Column(JSON)) version_number: int = Field(nullable=False, ge=1) description: str | None = Field(default=None, nullable=True, max_length=500) - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), nullable=False, index=True) + created_at: datetime = Field( + sa_column=Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True), + ) # The UniqueConstraint on (flow_id, version_number) creates an implicit composite # btree index that also covers ORDER BY version_number DESC queries filtered by From aea07965a05071563fe2ccd79f39be633aa8ee4e Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Fri, 13 Mar 2026 11:15:52 -0700 Subject: [PATCH 17/29] fix: Proper refresh of Groq models (#12158) * fix: Proper refresh of Groq models * Update groq_model_discovery.py * Update src/lfx/src/lfx/base/models/groq_model_discovery.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/lfx/src/lfx/base/models/groq_model_discovery.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add more unit tests * Update src/lfx/src/lfx/base/models/groq_model_discovery.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * More thorough unit tests * Update test_groq_model_discovery.py * Update groq_model_discovery.py * Update src/backend/tests/unit/groq/test_groq_model_discovery.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/backend/tests/unit/groq/test_groq_model_discovery.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test_groq_model_discovery.py * Update groq_model_discovery.py * Update groq.py * [autofix.ci] apply automated fixes * PR review comments * Redundant errors * [autofix.ci] apply automated fixes --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/backend/tests/unit/groq/conftest.py | 28 + .../unit/groq/test_groq_chat_completion.py | 157 +++++ .../groq/test_groq_convenience_function.py | 39 ++ .../groq/test_groq_discovery_edge_cases.py | 152 +++++ .../unit/groq/test_groq_discovery_errors.py | 151 +++++ .../unit/groq/test_groq_discovery_success.py | 234 ++++++++ .../unit/groq/test_groq_model_discovery.py | 541 ------------------ src/lfx/src/lfx/_assets/component_index.json | 6 +- .../src/lfx/_assets/stable_hash_history.json | 2 +- .../lfx/base/models/groq_model_discovery.py | 105 +++- src/lfx/src/lfx/components/groq/groq.py | 9 +- 11 files changed, 861 insertions(+), 563 deletions(-) create mode 100644 src/backend/tests/unit/groq/test_groq_chat_completion.py create mode 100644 src/backend/tests/unit/groq/test_groq_convenience_function.py create mode 100644 src/backend/tests/unit/groq/test_groq_discovery_edge_cases.py create mode 100644 src/backend/tests/unit/groq/test_groq_discovery_errors.py create mode 100644 src/backend/tests/unit/groq/test_groq_discovery_success.py delete mode 100644 src/backend/tests/unit/groq/test_groq_model_discovery.py diff --git a/src/backend/tests/unit/groq/conftest.py b/src/backend/tests/unit/groq/conftest.py index f92dbbfe9bbf..7911df40f974 100644 --- a/src/backend/tests/unit/groq/conftest.py +++ b/src/backend/tests/unit/groq/conftest.py @@ -261,6 +261,34 @@ def _create_mock_client(*_args, **_kwargs): return _create_mock_client +@pytest.fixture +def mock_groq_client_chat_not_supported(): + """Mock Groq client that raises 'does not support chat completions' error.""" + + def _create_mock_client(*_args, **_kwargs): + mock_client = MagicMock() + mock_client.chat.completions.create.side_effect = ValueError( + "Error: model 'some-model' does not support chat completions" + ) + return mock_client + + return _create_mock_client + + +@pytest.fixture +def mock_groq_client_chat_terms_required(): + """Mock Groq client that raises a terms_required error.""" + + def _create_mock_client(*_args, **_kwargs): + mock_client = MagicMock() + mock_client.chat.completions.create.side_effect = ValueError( + "Error: model_terms_required - please accept the terms" + ) + return mock_client + + return _create_mock_client + + @pytest.fixture def sample_models_metadata(): """Sample model metadata dictionary for testing.""" diff --git a/src/backend/tests/unit/groq/test_groq_chat_completion.py b/src/backend/tests/unit/groq/test_groq_chat_completion.py new file mode 100644 index 000000000000..9d495bf341f6 --- /dev/null +++ b/src/backend/tests/unit/groq/test_groq_chat_completion.py @@ -0,0 +1,157 @@ +"""Tests for Groq _test_chat_completion method.""" + +import sys +from unittest.mock import MagicMock, Mock, patch + +import pytest +from lfx.base.models.groq_model_discovery import GroqModelDiscovery + + +class TestChatCompletionDetection: + """Test _test_chat_completion method.""" + + @patch("groq.Groq") + def test_chat_completion_success(self, mock_groq, mock_api_key, mock_groq_client_tool_calling_success): + """Test successful chat completion returns True.""" + mock_groq.return_value = mock_groq_client_tool_calling_success() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + result = discovery._test_chat_completion("llama-3.1-8b-instant") + + assert result is True + + @patch("groq.Groq") + def test_chat_completion_not_supported(self, mock_groq, mock_api_key, mock_groq_client_chat_not_supported): + """Test model that does not support chat completions returns False.""" + mock_groq.return_value = mock_groq_client_chat_not_supported() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + result = discovery._test_chat_completion("speech-model") + + assert result is False + + @patch("groq.Groq") + def test_chat_completion_terms_required_returns_none( + self, mock_groq, mock_api_key, mock_groq_client_chat_terms_required + ): + """Test that access/entitlement errors cause _test_chat_completion to return None.""" + mock_groq.return_value = mock_groq_client_chat_terms_required() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + result = discovery._test_chat_completion("gated-model") + + assert result is None + + @patch("groq.Groq") + def test_chat_completion_transient_error_returns_none(self, mock_groq, mock_api_key, mock_groq_client_rate_limit): + """Test that transient errors (e.g. rate limits) return None (indeterminate).""" + mock_groq.return_value = mock_groq_client_rate_limit() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + result = discovery._test_chat_completion("llama-3.1-8b-instant") + + assert result is None + + def test_chat_completion_import_error_raises(self, mock_api_key): + """Test that ImportError propagates when the groq package is not installed.""" + # Simulate groq not being installed by hiding it from sys.modules + with patch.dict(sys.modules, {"groq": None}): + discovery = GroqModelDiscovery(api_key=mock_api_key) + with pytest.raises(ImportError): + discovery._test_chat_completion("test-model") + + @patch("lfx.base.models.groq_model_discovery.requests.get") + @patch("groq.Groq") + def test_chat_failure_marks_model_not_supported( + self, + mock_groq, + mock_get, + mock_api_key, + temp_cache_dir, + ): + """Test that a model failing the chat test is marked not_supported in get_models.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": [ + {"id": "llama-3.1-8b-instant", "object": "model"}, + {"id": "speech-model-v1", "object": "model"}, + ] + } + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + # First Groq() call: chat test for llama (succeeds) + # Second Groq() call: tool test for llama (succeeds) + # Third Groq() call: chat test for speech-model (fails with "does not support chat completions") + call_count = [0] + + def create_mock_client(*_args, **_kwargs): + mock_client = MagicMock() + if call_count[0] <= 1: + # chat + tool test for llama: succeed + mock_client.chat.completions.create.return_value = MagicMock() + else: + # chat test for speech-model: fails + mock_client.chat.completions.create.side_effect = ValueError( + "Error: model 'speech-model-v1' does not support chat completions" + ) + call_count[0] += 1 + return mock_client + + mock_groq.side_effect = create_mock_client + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" + + models = discovery.get_models(force_refresh=True) + + # llama should be a normal LLM model with tool_calling + assert "tool_calling" in models["llama-3.1-8b-instant"] + assert models["llama-3.1-8b-instant"].get("not_supported") is None + + # speech-model should be marked not_supported + assert models["speech-model-v1"]["not_supported"] is True + assert "tool_calling" not in models["speech-model-v1"] + + @patch("lfx.base.models.groq_model_discovery.requests.get") + @patch("groq.Groq") + def test_transient_chat_error_does_not_exclude_model( + self, + mock_groq, + mock_get, + mock_api_key, + temp_cache_dir, + ): + """Test that transient chat errors (rate limits) don't incorrectly exclude models.""" + mock_response = Mock() + mock_response.json.return_value = { + "data": [ + {"id": "rate-limited-model", "object": "model"}, + ] + } + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + # First Groq() call: chat test hits rate limit (transient error) + # Second Groq() call: tool test succeeds + call_count = [0] + + def create_mock_client(*_args, **_kwargs): + mock_client = MagicMock() + if call_count[0] == 0: + mock_client.chat.completions.create.side_effect = RuntimeError("Rate limit exceeded") + else: + mock_client.chat.completions.create.return_value = MagicMock() + call_count[0] += 1 + return mock_client + + mock_groq.side_effect = create_mock_client + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" + + models = discovery.get_models(force_refresh=True) + + # Model should NOT be excluded — it should be treated as a normal LLM + assert "tool_calling" in models["rate-limited-model"] + assert models["rate-limited-model"].get("not_supported") is None diff --git a/src/backend/tests/unit/groq/test_groq_convenience_function.py b/src/backend/tests/unit/groq/test_groq_convenience_function.py new file mode 100644 index 000000000000..0e50b8424109 --- /dev/null +++ b/src/backend/tests/unit/groq/test_groq_convenience_function.py @@ -0,0 +1,39 @@ +"""Tests for the get_groq_models() convenience function.""" + +from unittest.mock import patch + +from lfx.base.models.groq_model_discovery import GroqModelDiscovery, get_groq_models + + +class TestGetGroqModelsConvenienceFunction: + """Test the convenience function get_groq_models().""" + + @patch.object(GroqModelDiscovery, "get_models") + def test_get_groq_models_with_api_key(self, mock_get_models, mock_api_key): + """Test get_groq_models() function with API key.""" + mock_get_models.return_value = {"llama-3.1-8b-instant": {}} + + models = get_groq_models(api_key=mock_api_key) + + assert "llama-3.1-8b-instant" in models + mock_get_models.assert_called_once_with(force_refresh=False) + + @patch.object(GroqModelDiscovery, "get_models") + def test_get_groq_models_without_api_key(self, mock_get_models): + """Test get_groq_models() function without API key.""" + mock_get_models.return_value = {"llama-3.1-8b-instant": {}} + + models = get_groq_models() + + assert "llama-3.1-8b-instant" in models + mock_get_models.assert_called_once_with(force_refresh=False) + + @patch.object(GroqModelDiscovery, "get_models") + def test_get_groq_models_force_refresh(self, mock_get_models, mock_api_key): + """Test get_groq_models() with force_refresh.""" + mock_get_models.return_value = {"llama-3.1-8b-instant": {}} + + models = get_groq_models(api_key=mock_api_key, force_refresh=True) + + assert "llama-3.1-8b-instant" in models + mock_get_models.assert_called_once_with(force_refresh=True) diff --git a/src/backend/tests/unit/groq/test_groq_discovery_edge_cases.py b/src/backend/tests/unit/groq/test_groq_discovery_edge_cases.py new file mode 100644 index 000000000000..11d2e5567230 --- /dev/null +++ b/src/backend/tests/unit/groq/test_groq_discovery_edge_cases.py @@ -0,0 +1,152 @@ +"""Tests for edge cases in Groq model discovery.""" + +from unittest.mock import MagicMock, Mock, patch + +from lfx.base.models.groq_model_discovery import GroqModelDiscovery + + +class TestGroqModelDiscoveryEdgeCases: + """Test edge cases in model discovery.""" + + @patch("lfx.base.models.groq_model_discovery.requests.get") + def test_empty_model_list_from_api(self, mock_get, mock_api_key, temp_cache_dir): + """Test handling of empty model list from API.""" + # Mock empty response + mock_response = Mock() + mock_response.json.return_value = {"data": []} + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" + + models = discovery.get_models(force_refresh=True) + + # Should return empty dict (or potentially fallback) + assert isinstance(models, dict) + + def test_cache_file_not_exists(self, mock_api_key, temp_cache_dir): + """Test loading cache when file doesn't exist.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "nonexistent.json" + + loaded = discovery._load_cache() + + assert loaded is None + + def test_cache_directory_created_on_save(self, mock_api_key, temp_cache_dir, sample_models_metadata): + """Test that cache directory is created if it doesn't exist.""" + cache_file = temp_cache_dir / "new_dir" / ".cache" / "test_cache.json" + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = cache_file + + # Directory shouldn't exist yet + assert not cache_file.parent.exists() + + # Save cache + discovery._save_cache(sample_models_metadata) + + # Directory should be created + assert cache_file.parent.exists() + assert cache_file.exists() + + @patch("lfx.base.models.groq_model_discovery.requests.get") + @patch("groq.Groq") + def test_preview_model_detection( + self, + mock_groq, + mock_get, + mock_api_key, + mock_groq_client_tool_calling_success, + temp_cache_dir, + ): + """Test detection of preview models.""" + # Mock API with preview models + mock_response = Mock() + mock_response.json.return_value = { + "data": [ + {"id": "llama-3.2-1b-preview", "object": "model"}, + {"id": "meta-llama/llama-3.2-90b-preview", "object": "model"}, + ] + } + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + mock_groq.return_value = mock_groq_client_tool_calling_success() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" + + models = discovery.get_models(force_refresh=True) + + # Models with "preview" in name should be marked as preview + assert models["llama-3.2-1b-preview"]["preview"] is True + + # Models with "/" should be marked as preview + assert models["meta-llama/llama-3.2-90b-preview"]["preview"] is True + + @patch("lfx.base.models.groq_model_discovery.requests.get") + @patch("groq.Groq") + def test_mixed_tool_calling_support( + self, + mock_groq, + mock_get, + mock_api_key, + temp_cache_dir, + ): + """Test models with mixed tool calling support.""" + # Mock API + mock_response = Mock() + mock_response.json.return_value = { + "data": [ + {"id": "llama-3.1-8b-instant", "object": "model"}, + {"id": "gemma-7b-it", "object": "model"}, + ] + } + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + # Mock tool calling - each model goes through chat test then tool test + # Call order: chat(llama), tool(llama), chat(gemma), tool(gemma) + call_count = [0] + + def create_mock_client(*_args, **_kwargs): + mock_client = MagicMock() + if call_count[0] <= 2: + # Calls 0-2: chat test for llama (success), tool test for llama (success), + # chat test for gemma (success) + mock_client.chat.completions.create.return_value = MagicMock() + else: + # Call 3: tool test for gemma (fails) + mock_client.chat.completions.create.side_effect = ValueError("tool calling not supported") + call_count[0] += 1 + return mock_client + + mock_groq.side_effect = create_mock_client + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" + + models = discovery.get_models(force_refresh=True) + + # First model should support tools + assert models["llama-3.1-8b-instant"]["tool_calling"] is True + + # Second model should not support tools + assert models["gemma-7b-it"]["tool_calling"] is False + + def test_fallback_models_structure(self, mock_api_key): + """Test that fallback models have the correct structure.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + fallback = discovery._get_fallback_models() + + assert isinstance(fallback, dict) + assert len(fallback) == 2 + + for metadata in fallback.values(): + assert "name" in metadata + assert "provider" in metadata + assert "tool_calling" in metadata + assert "preview" in metadata + assert metadata["tool_calling"] is True # Fallback models should support tools diff --git a/src/backend/tests/unit/groq/test_groq_discovery_errors.py b/src/backend/tests/unit/groq/test_groq_discovery_errors.py new file mode 100644 index 000000000000..3c33c49baa98 --- /dev/null +++ b/src/backend/tests/unit/groq/test_groq_discovery_errors.py @@ -0,0 +1,151 @@ +"""Tests for error handling in Groq model discovery.""" + +import json +import sys +from unittest.mock import Mock, patch + +import pytest +from lfx.base.models.groq_model_discovery import GroqModelDiscovery + + +class TestGroqModelDiscoveryErrors: + """Test error handling in model discovery.""" + + def test_no_api_key_returns_fallback(self): + """Test that missing API key returns fallback models.""" + discovery = GroqModelDiscovery(api_key=None) + models = discovery.get_models(force_refresh=True) + + # Should return minimal fallback list + assert "llama-3.1-8b-instant" in models + assert "llama-3.3-70b-versatile" in models + assert len(models) == 2 + + @patch("lfx.base.models.groq_model_discovery.requests.get") + def test_api_connection_error_returns_fallback(self, mock_get, mock_api_key, mock_requests_get_failure): + """Test that API connection errors return fallback models.""" + mock_get.side_effect = mock_requests_get_failure + + discovery = GroqModelDiscovery(api_key=mock_api_key) + models = discovery.get_models(force_refresh=True) + + # Should return fallback models + assert "llama-3.1-8b-instant" in models + assert "llama-3.3-70b-versatile" in models + + @patch("lfx.base.models.groq_model_discovery.requests.get") + def test_api_timeout_returns_fallback(self, mock_get, mock_api_key, mock_requests_get_timeout): + """Test that API timeouts return fallback models.""" + mock_get.side_effect = mock_requests_get_timeout + + discovery = GroqModelDiscovery(api_key=mock_api_key) + models = discovery.get_models(force_refresh=True) + + # Should return fallback models + assert "llama-3.1-8b-instant" in models + assert "llama-3.3-70b-versatile" in models + + @patch("lfx.base.models.groq_model_discovery.requests.get") + def test_api_unauthorized_returns_fallback(self, mock_get, mock_api_key, mock_requests_get_unauthorized): + """Test that unauthorized API requests return fallback models.""" + mock_get.side_effect = mock_requests_get_unauthorized + + discovery = GroqModelDiscovery(api_key=mock_api_key) + models = discovery.get_models(force_refresh=True) + + # Should return fallback models + assert "llama-3.1-8b-instant" in models + assert "llama-3.3-70b-versatile" in models + + @patch("lfx.base.models.groq_model_discovery.requests.get") + def test_invalid_api_response_returns_fallback(self, mock_get, mock_api_key): + """Test that invalid API response structure returns fallback models.""" + # Mock response with missing 'data' field + mock_response = Mock() + mock_response.json.return_value = {"error": "invalid"} + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + discovery = GroqModelDiscovery(api_key=mock_api_key) + models = discovery.get_models(force_refresh=True) + + # Should return fallback models + assert "llama-3.1-8b-instant" in models + + def test_corrupted_cache_returns_none(self, mock_api_key, mock_corrupted_cache_file): + """Test that corrupted cache file returns None.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = mock_corrupted_cache_file + + loaded = discovery._load_cache() + + assert loaded is None + + def test_cache_missing_fields_returns_none(self, mock_api_key, temp_cache_dir): + """Test that cache with missing required fields returns None.""" + cache_file = temp_cache_dir / ".cache" / "invalid_cache.json" + cache_file.parent.mkdir(parents=True, exist_ok=True) + + # Cache missing 'cached_at' field + cache_data = {"models": {"llama-3.1-8b-instant": {}}} + + with cache_file.open("w") as f: + json.dump(cache_data, f) + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = cache_file + + loaded = discovery._load_cache() + + assert loaded is None + + def test_cache_save_failure_logs_warning(self, mock_api_key, temp_cache_dir, sample_models_metadata): + """Test that cache save failures are logged but don't crash.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + # Set cache file to a path that can't be written (directory instead of file) + discovery.CACHE_FILE = temp_cache_dir + + # This should not raise an exception + discovery._save_cache(sample_models_metadata) + + @patch("lfx.base.models.groq_model_discovery.requests.get") + def test_import_error_during_chat_test_returns_fallback(self, mock_get, mock_api_key, temp_cache_dir): + """Test that get_models returns fallback models when groq is not installed. + + Both _test_chat_completion and _test_tool_calling re-raise ImportError when + the groq package is absent. get_models catches it and falls back to hardcoded + model metadata instead of crashing. + """ + mock_response = Mock() + mock_response.json.return_value = {"data": [{"id": "llama-3.1-8b-instant", "object": "model"}]} + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + with patch.dict(sys.modules, {"groq": None}): + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" + models = discovery.get_models(force_refresh=True) + + # Should return the hardcoded fallback list, not an empty dict + assert "llama-3.1-8b-instant" in models + assert "llama-3.3-70b-versatile" in models + assert len(models) == 2 # exactly the two fallback models + + @patch("groq.Groq") + def test_tool_calling_import_error_raises(self, mock_groq, mock_api_key): + """Test that ImportError during tool calling test is re-raised.""" + mock_groq.side_effect = ImportError("groq module not found") + + discovery = GroqModelDiscovery(api_key=mock_api_key) + with pytest.raises(ImportError): + discovery._test_tool_calling("test-model") + + @patch("groq.Groq") + def test_tool_calling_rate_limit_returns_none(self, mock_groq, mock_api_key, mock_groq_client_rate_limit): + """Test that rate limit errors return None (indeterminate).""" + mock_groq.return_value = mock_groq_client_rate_limit() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + result = discovery._test_tool_calling("test-model") + + assert result is None diff --git a/src/backend/tests/unit/groq/test_groq_discovery_success.py b/src/backend/tests/unit/groq/test_groq_discovery_success.py new file mode 100644 index 000000000000..74184017537a --- /dev/null +++ b/src/backend/tests/unit/groq/test_groq_discovery_success.py @@ -0,0 +1,234 @@ +"""Tests for successful Groq model discovery operations.""" + +from unittest.mock import Mock, patch + +from lfx.base.models.groq_model_discovery import GroqModelDiscovery + + +class TestGroqModelDiscoverySuccess: + """Test successful model discovery operations.""" + + def test_init_with_api_key(self, mock_api_key): + """Test initialization with API key.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + assert discovery.api_key == mock_api_key + assert discovery.base_url == "https://api.groq.com" + + def test_init_without_api_key(self): + """Test initialization without API key.""" + discovery = GroqModelDiscovery() + assert discovery.api_key is None + assert discovery.base_url == "https://api.groq.com" + + def test_init_with_custom_base_url(self, mock_api_key): + """Test initialization with custom base URL.""" + custom_url = "https://custom.groq.com" + discovery = GroqModelDiscovery(api_key=mock_api_key, base_url=custom_url) + assert discovery.base_url == custom_url + + @patch("lfx.base.models.groq_model_discovery.requests.get") + @patch("groq.Groq") + def test_fetch_available_models_success( + self, mock_groq, mock_get, mock_api_key, mock_groq_models_response, mock_groq_client_tool_calling_success + ): + """Test successfully fetching models from API.""" + # Mock API response + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = mock_groq_models_response + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + # Mock tool calling tests + mock_groq.return_value = mock_groq_client_tool_calling_success() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + models = discovery._fetch_available_models() + + assert isinstance(models, list) + assert len(models) == 8 + assert "llama-3.1-8b-instant" in models + assert "whisper-large-v3" in models + mock_get.assert_called_once() + + @patch("lfx.base.models.groq_model_discovery.requests.get") + @patch("groq.Groq") + def test_get_models_categorizes_llm_and_non_llm( + self, + mock_groq, + mock_get, + mock_api_key, + mock_groq_models_response, + mock_groq_client_tool_calling_success, + temp_cache_dir, + ): + """Test that models are correctly categorized as LLM vs non-LLM.""" + # Mock API response + mock_response = Mock() + mock_response.json.return_value = mock_groq_models_response + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + # Mock tool calling tests to always succeed + mock_groq.return_value = mock_groq_client_tool_calling_success() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" + + models = discovery.get_models(force_refresh=True) + + # LLM models should be in the result + assert "llama-3.1-8b-instant" in models + assert "llama-3.3-70b-versatile" in models + assert "mixtral-8x7b-32768" in models + assert "gemma-7b-it" in models + + # Non-LLM models should be marked as not_supported + assert models["whisper-large-v3"]["not_supported"] is True + assert models["distil-whisper-large-v3-en"]["not_supported"] is True + assert models["meta-llama/llama-guard-4-12b"]["not_supported"] is True + assert models["meta-llama/llama-prompt-guard-2-86m"]["not_supported"] is True + + # LLM models should have tool_calling field + assert "tool_calling" in models["llama-3.1-8b-instant"] + assert "tool_calling" in models["mixtral-8x7b-32768"] + + @patch("groq.Groq") + def test_tool_calling_detection_success(self, mock_groq, mock_api_key, mock_groq_client_tool_calling_success): + """Test successful tool calling detection.""" + mock_groq.return_value = mock_groq_client_tool_calling_success() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + result = discovery._test_tool_calling("llama-3.1-8b-instant") + + assert result is True + + @patch("groq.Groq") + def test_tool_calling_detection_not_supported(self, mock_groq, mock_api_key, mock_groq_client_tool_calling_failure): + """Test tool calling detection when model doesn't support tools.""" + mock_groq.return_value = mock_groq_client_tool_calling_failure() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + result = discovery._test_tool_calling("gemma-7b-it") + + assert result is False + + def test_cache_save_and_load(self, mock_api_key, sample_models_metadata, temp_cache_dir): + """Test saving and loading cache.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" + + # Save cache + discovery._save_cache(sample_models_metadata) + + # Verify file was created + assert discovery.CACHE_FILE.exists() + + # Load cache + loaded = discovery._load_cache() + + assert loaded is not None + assert len(loaded) == len(sample_models_metadata) + assert "llama-3.1-8b-instant" in loaded + assert loaded["llama-3.1-8b-instant"]["tool_calling"] is True + + def test_cache_respects_expiration(self, mock_api_key, mock_expired_cache_file): + """Test that expired cache returns None.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = mock_expired_cache_file + + loaded = discovery._load_cache() + + assert loaded is None + + @patch("lfx.base.models.groq_model_discovery.requests.get") + @patch("groq.Groq") + def test_get_models_uses_cache_when_available(self, mock_groq, mock_get, mock_api_key, mock_cache_file): + """Test that get_models uses cache when available and not expired.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = mock_cache_file + + models = discovery.get_models(force_refresh=False) + + # Should use cache, not call API + mock_get.assert_not_called() + mock_groq.assert_not_called() + + assert "llama-3.1-8b-instant" in models + assert "llama-3.3-70b-versatile" in models + + @patch("lfx.base.models.groq_model_discovery.requests.get") + @patch("groq.Groq") + def test_force_refresh_bypasses_cache( + self, + mock_groq, + mock_get, + mock_api_key, + mock_groq_models_response, + mock_groq_client_tool_calling_success, + mock_cache_file, + ): + """Test that force_refresh bypasses cache and fetches fresh data.""" + # Mock API response + mock_response = Mock() + mock_response.json.return_value = mock_groq_models_response + mock_response.raise_for_status = Mock() + mock_get.return_value = mock_response + + # Mock tool calling + mock_groq.return_value = mock_groq_client_tool_calling_success() + + discovery = GroqModelDiscovery(api_key=mock_api_key) + discovery.CACHE_FILE = mock_cache_file + + models = discovery.get_models(force_refresh=True) + + # Should call API despite cache + mock_get.assert_called() + assert len(models) > 0 + + def test_provider_name_extraction(self, mock_api_key): + """Test provider name extraction from model IDs.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + + # Models with slash notation + assert discovery._get_provider_name("meta-llama/llama-3.1-8b") == "Meta" + assert discovery._get_provider_name("openai/gpt-oss-safeguard-20b") == "OpenAI" + assert discovery._get_provider_name("qwen/qwen3-32b") == "Alibaba Cloud" + assert discovery._get_provider_name("moonshotai/moonshot-v1") == "Moonshot AI" + assert discovery._get_provider_name("groq/groq-model") == "Groq" + + # Models with prefixes + assert discovery._get_provider_name("llama-3.1-8b-instant") == "Meta" + assert discovery._get_provider_name("llama3-70b-8192") == "Meta" + assert discovery._get_provider_name("qwen-2.5-32b") == "Alibaba Cloud" + assert discovery._get_provider_name("allam-1-13b") == "SDAIA" + + # Unknown providers default to Groq + assert discovery._get_provider_name("unknown-model") == "Groq" + + def test_skip_patterns(self, mock_api_key): + """Test that SKIP_PATTERNS correctly identify non-LLM models.""" + discovery = GroqModelDiscovery(api_key=mock_api_key) + + skip_models = [ + "whisper-large-v3", + "whisper-large-v3-turbo", + "distil-whisper-large-v3-en", + "playai-tts", + "playai-tts-arabic", + "meta-llama/llama-guard-4-12b", + "meta-llama/llama-prompt-guard-2-86m", + "openai/gpt-oss-safeguard-20b", + "mistral-saba-24b", # safeguard model + ] + + for model in skip_models: + should_skip = any(pattern in model.lower() for pattern in discovery.SKIP_PATTERNS) + assert should_skip, f"Model {model} should be skipped but wasn't" + + # LLM models should not be skipped + llm_models = ["llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma-7b-it"] + for model in llm_models: + should_skip = any(pattern in model.lower() for pattern in discovery.SKIP_PATTERNS) + assert not should_skip, f"Model {model} should not be skipped" diff --git a/src/backend/tests/unit/groq/test_groq_model_discovery.py b/src/backend/tests/unit/groq/test_groq_model_discovery.py deleted file mode 100644 index 583e90b9411b..000000000000 --- a/src/backend/tests/unit/groq/test_groq_model_discovery.py +++ /dev/null @@ -1,541 +0,0 @@ -"""Comprehensive tests for Groq model discovery system. - -Tests cover: -- Success paths: API fetching, caching, tool calling detection -- Error paths: API failures, network errors, invalid responses -- Edge cases: expired cache, corrupted cache, missing API key -""" - -import json -from unittest.mock import MagicMock, Mock, patch - -from lfx.base.models.groq_model_discovery import GroqModelDiscovery, get_groq_models - - -class TestGroqModelDiscoverySuccess: - """Test successful model discovery operations.""" - - def test_init_with_api_key(self, mock_api_key): - """Test initialization with API key.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - assert discovery.api_key == mock_api_key - assert discovery.base_url == "https://api.groq.com" - - def test_init_without_api_key(self): - """Test initialization without API key.""" - discovery = GroqModelDiscovery() - assert discovery.api_key is None - assert discovery.base_url == "https://api.groq.com" - - def test_init_with_custom_base_url(self, mock_api_key): - """Test initialization with custom base URL.""" - custom_url = "https://custom.groq.com" - discovery = GroqModelDiscovery(api_key=mock_api_key, base_url=custom_url) - assert discovery.base_url == custom_url - - @patch("lfx.base.models.groq_model_discovery.requests.get") - @patch("groq.Groq") - def test_fetch_available_models_success( - self, mock_groq, mock_get, mock_api_key, mock_groq_models_response, mock_groq_client_tool_calling_success - ): - """Test successfully fetching models from API.""" - # Mock API response - mock_response = Mock() - mock_response.status_code = 200 - mock_response.json.return_value = mock_groq_models_response - mock_response.raise_for_status = Mock() - mock_get.return_value = mock_response - - # Mock tool calling tests - mock_groq.return_value = mock_groq_client_tool_calling_success() - - discovery = GroqModelDiscovery(api_key=mock_api_key) - models = discovery._fetch_available_models() - - assert isinstance(models, list) - assert len(models) == 8 - assert "llama-3.1-8b-instant" in models - assert "whisper-large-v3" in models - mock_get.assert_called_once() - - @patch("lfx.base.models.groq_model_discovery.requests.get") - @patch("groq.Groq") - def test_get_models_categorizes_llm_and_non_llm( - self, - mock_groq, - mock_get, - mock_api_key, - mock_groq_models_response, - mock_groq_client_tool_calling_success, - temp_cache_dir, - ): - """Test that models are correctly categorized as LLM vs non-LLM.""" - # Mock API response - mock_response = Mock() - mock_response.json.return_value = mock_groq_models_response - mock_response.raise_for_status = Mock() - mock_get.return_value = mock_response - - # Mock tool calling tests to always succeed - mock_groq.return_value = mock_groq_client_tool_calling_success() - - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" - - models = discovery.get_models(force_refresh=True) - - # LLM models should be in the result - assert "llama-3.1-8b-instant" in models - assert "llama-3.3-70b-versatile" in models - assert "mixtral-8x7b-32768" in models - assert "gemma-7b-it" in models - - # Non-LLM models should be marked as not_supported - assert models["whisper-large-v3"]["not_supported"] is True - assert models["distil-whisper-large-v3-en"]["not_supported"] is True - assert models["meta-llama/llama-guard-4-12b"]["not_supported"] is True - assert models["meta-llama/llama-prompt-guard-2-86m"]["not_supported"] is True - - # LLM models should have tool_calling field - assert "tool_calling" in models["llama-3.1-8b-instant"] - assert "tool_calling" in models["mixtral-8x7b-32768"] - - @patch("groq.Groq") - def test_tool_calling_detection_success(self, mock_groq, mock_api_key, mock_groq_client_tool_calling_success): - """Test successful tool calling detection.""" - mock_groq.return_value = mock_groq_client_tool_calling_success() - - discovery = GroqModelDiscovery(api_key=mock_api_key) - result = discovery._test_tool_calling("llama-3.1-8b-instant") - - assert result is True - - @patch("groq.Groq") - def test_tool_calling_detection_not_supported(self, mock_groq, mock_api_key, mock_groq_client_tool_calling_failure): - """Test tool calling detection when model doesn't support tools.""" - mock_groq.return_value = mock_groq_client_tool_calling_failure() - - discovery = GroqModelDiscovery(api_key=mock_api_key) - result = discovery._test_tool_calling("gemma-7b-it") - - assert result is False - - def test_cache_save_and_load(self, mock_api_key, sample_models_metadata, temp_cache_dir): - """Test saving and loading cache.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" - - # Save cache - discovery._save_cache(sample_models_metadata) - - # Verify file was created - assert discovery.CACHE_FILE.exists() - - # Load cache - loaded = discovery._load_cache() - - assert loaded is not None - assert len(loaded) == len(sample_models_metadata) - assert "llama-3.1-8b-instant" in loaded - assert loaded["llama-3.1-8b-instant"]["tool_calling"] is True - - def test_cache_respects_expiration(self, mock_api_key, mock_expired_cache_file): - """Test that expired cache returns None.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = mock_expired_cache_file - - loaded = discovery._load_cache() - - assert loaded is None - - @patch("lfx.base.models.groq_model_discovery.requests.get") - @patch("groq.Groq") - def test_get_models_uses_cache_when_available(self, mock_groq, mock_get, mock_api_key, mock_cache_file): - """Test that get_models uses cache when available and not expired.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = mock_cache_file - - models = discovery.get_models(force_refresh=False) - - # Should use cache, not call API - mock_get.assert_not_called() - mock_groq.assert_not_called() - - assert "llama-3.1-8b-instant" in models - assert "llama-3.3-70b-versatile" in models - - @patch("lfx.base.models.groq_model_discovery.requests.get") - @patch("groq.Groq") - def test_force_refresh_bypasses_cache( - self, - mock_groq, - mock_get, - mock_api_key, - mock_groq_models_response, - mock_groq_client_tool_calling_success, - mock_cache_file, - ): - """Test that force_refresh bypasses cache and fetches fresh data.""" - # Mock API response - mock_response = Mock() - mock_response.json.return_value = mock_groq_models_response - mock_response.raise_for_status = Mock() - mock_get.return_value = mock_response - - # Mock tool calling - mock_groq.return_value = mock_groq_client_tool_calling_success() - - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = mock_cache_file - - models = discovery.get_models(force_refresh=True) - - # Should call API despite cache - mock_get.assert_called() - assert len(models) > 0 - - def test_provider_name_extraction(self, mock_api_key): - """Test provider name extraction from model IDs.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - - # Models with slash notation - assert discovery._get_provider_name("meta-llama/llama-3.1-8b") == "Meta" - assert discovery._get_provider_name("openai/gpt-oss-safeguard-20b") == "OpenAI" - assert discovery._get_provider_name("qwen/qwen3-32b") == "Alibaba Cloud" - assert discovery._get_provider_name("moonshotai/moonshot-v1") == "Moonshot AI" - assert discovery._get_provider_name("groq/groq-model") == "Groq" - - # Models with prefixes - assert discovery._get_provider_name("llama-3.1-8b-instant") == "Meta" - assert discovery._get_provider_name("llama3-70b-8192") == "Meta" - assert discovery._get_provider_name("qwen-2.5-32b") == "Alibaba Cloud" - assert discovery._get_provider_name("allam-1-13b") == "SDAIA" - - # Unknown providers default to Groq - assert discovery._get_provider_name("unknown-model") == "Groq" - - def test_skip_patterns(self, mock_api_key): - """Test that SKIP_PATTERNS correctly identify non-LLM models.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - - skip_models = [ - "whisper-large-v3", - "whisper-large-v3-turbo", - "distil-whisper-large-v3-en", - "playai-tts", - "playai-tts-arabic", - "meta-llama/llama-guard-4-12b", - "meta-llama/llama-prompt-guard-2-86m", - "openai/gpt-oss-safeguard-20b", - "mistral-saba-24b", # safeguard model - ] - - for model in skip_models: - should_skip = any(pattern in model.lower() for pattern in discovery.SKIP_PATTERNS) - assert should_skip, f"Model {model} should be skipped but wasn't" - - # LLM models should not be skipped - llm_models = ["llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma-7b-it"] - for model in llm_models: - should_skip = any(pattern in model.lower() for pattern in discovery.SKIP_PATTERNS) - assert not should_skip, f"Model {model} should not be skipped" - - -class TestGroqModelDiscoveryErrors: - """Test error handling in model discovery.""" - - def test_no_api_key_returns_fallback(self): - """Test that missing API key returns fallback models.""" - discovery = GroqModelDiscovery(api_key=None) - models = discovery.get_models(force_refresh=True) - - # Should return minimal fallback list - assert "llama-3.1-8b-instant" in models - assert "llama-3.3-70b-versatile" in models - assert len(models) == 2 - - @patch("lfx.base.models.groq_model_discovery.requests.get") - def test_api_connection_error_returns_fallback(self, mock_get, mock_api_key, mock_requests_get_failure): - """Test that API connection errors return fallback models.""" - mock_get.side_effect = mock_requests_get_failure - - discovery = GroqModelDiscovery(api_key=mock_api_key) - models = discovery.get_models(force_refresh=True) - - # Should return fallback models - assert "llama-3.1-8b-instant" in models - assert "llama-3.3-70b-versatile" in models - - @patch("lfx.base.models.groq_model_discovery.requests.get") - def test_api_timeout_returns_fallback(self, mock_get, mock_api_key, mock_requests_get_timeout): - """Test that API timeouts return fallback models.""" - mock_get.side_effect = mock_requests_get_timeout - - discovery = GroqModelDiscovery(api_key=mock_api_key) - models = discovery.get_models(force_refresh=True) - - # Should return fallback models - assert "llama-3.1-8b-instant" in models - assert "llama-3.3-70b-versatile" in models - - @patch("lfx.base.models.groq_model_discovery.requests.get") - def test_api_unauthorized_returns_fallback(self, mock_get, mock_api_key, mock_requests_get_unauthorized): - """Test that unauthorized API requests return fallback models.""" - mock_get.side_effect = mock_requests_get_unauthorized - - discovery = GroqModelDiscovery(api_key=mock_api_key) - models = discovery.get_models(force_refresh=True) - - # Should return fallback models - assert "llama-3.1-8b-instant" in models - assert "llama-3.3-70b-versatile" in models - - @patch("lfx.base.models.groq_model_discovery.requests.get") - def test_invalid_api_response_returns_fallback(self, mock_get, mock_api_key): - """Test that invalid API response structure returns fallback models.""" - # Mock response with missing 'data' field - mock_response = Mock() - mock_response.json.return_value = {"error": "invalid"} - mock_response.raise_for_status = Mock() - mock_get.return_value = mock_response - - discovery = GroqModelDiscovery(api_key=mock_api_key) - models = discovery.get_models(force_refresh=True) - - # Should return fallback models - assert "llama-3.1-8b-instant" in models - - def test_corrupted_cache_returns_none(self, mock_api_key, mock_corrupted_cache_file): - """Test that corrupted cache file returns None.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = mock_corrupted_cache_file - - loaded = discovery._load_cache() - - assert loaded is None - - def test_cache_missing_fields_returns_none(self, mock_api_key, temp_cache_dir): - """Test that cache with missing required fields returns None.""" - cache_file = temp_cache_dir / ".cache" / "invalid_cache.json" - cache_file.parent.mkdir(parents=True, exist_ok=True) - - # Cache missing 'cached_at' field - cache_data = {"models": {"llama-3.1-8b-instant": {}}} - - with cache_file.open("w") as f: - json.dump(cache_data, f) - - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = cache_file - - loaded = discovery._load_cache() - - assert loaded is None - - def test_cache_save_failure_logs_warning(self, mock_api_key, temp_cache_dir, sample_models_metadata): - """Test that cache save failures are logged but don't crash.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - # Set cache file to a path that can't be written (directory instead of file) - discovery.CACHE_FILE = temp_cache_dir - - # This should not raise an exception - discovery._save_cache(sample_models_metadata) - - @patch("groq.Groq") - def test_tool_calling_import_error_returns_false(self, mock_groq, mock_api_key): - """Test that ImportError during tool calling test returns False.""" - mock_groq.side_effect = ImportError("groq module not found") - - discovery = GroqModelDiscovery(api_key=mock_api_key) - result = discovery._test_tool_calling("test-model") - - assert result is False - - @patch("groq.Groq") - def test_tool_calling_rate_limit_returns_false(self, mock_groq, mock_api_key, mock_groq_client_rate_limit): - """Test that rate limit errors return False conservatively.""" - mock_groq.return_value = mock_groq_client_rate_limit() - - discovery = GroqModelDiscovery(api_key=mock_api_key) - result = discovery._test_tool_calling("test-model") - - assert result is False - - -class TestGroqModelDiscoveryEdgeCases: - """Test edge cases in model discovery.""" - - @patch("lfx.base.models.groq_model_discovery.requests.get") - def test_empty_model_list_from_api(self, mock_get, mock_api_key, temp_cache_dir): - """Test handling of empty model list from API.""" - # Mock empty response - mock_response = Mock() - mock_response.json.return_value = {"data": []} - mock_response.raise_for_status = Mock() - mock_get.return_value = mock_response - - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" - - models = discovery.get_models(force_refresh=True) - - # Should return empty dict (or potentially fallback) - assert isinstance(models, dict) - - def test_cache_file_not_exists(self, mock_api_key, temp_cache_dir): - """Test loading cache when file doesn't exist.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = temp_cache_dir / ".cache" / "nonexistent.json" - - loaded = discovery._load_cache() - - assert loaded is None - - def test_cache_directory_created_on_save(self, mock_api_key, temp_cache_dir, sample_models_metadata): - """Test that cache directory is created if it doesn't exist.""" - cache_file = temp_cache_dir / "new_dir" / ".cache" / "test_cache.json" - - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = cache_file - - # Directory shouldn't exist yet - assert not cache_file.parent.exists() - - # Save cache - discovery._save_cache(sample_models_metadata) - - # Directory should be created - assert cache_file.parent.exists() - assert cache_file.exists() - - @patch("lfx.base.models.groq_model_discovery.requests.get") - @patch("groq.Groq") - def test_preview_model_detection( - self, - mock_groq, - mock_get, - mock_api_key, - mock_groq_client_tool_calling_success, - temp_cache_dir, - ): - """Test detection of preview models.""" - # Mock API with preview models - mock_response = Mock() - mock_response.json.return_value = { - "data": [ - {"id": "llama-3.2-1b-preview", "object": "model"}, - {"id": "meta-llama/llama-3.2-90b-preview", "object": "model"}, - ] - } - mock_response.raise_for_status = Mock() - mock_get.return_value = mock_response - - mock_groq.return_value = mock_groq_client_tool_calling_success() - - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" - - models = discovery.get_models(force_refresh=True) - - # Models with "preview" in name should be marked as preview - assert models["llama-3.2-1b-preview"]["preview"] is True - - # Models with "/" should be marked as preview - assert models["meta-llama/llama-3.2-90b-preview"]["preview"] is True - - @patch("lfx.base.models.groq_model_discovery.requests.get") - @patch("groq.Groq") - def test_mixed_tool_calling_support( - self, - mock_groq, - mock_get, - mock_api_key, - temp_cache_dir, - ): - """Test models with mixed tool calling support.""" - # Mock API - mock_response = Mock() - mock_response.json.return_value = { - "data": [ - {"id": "llama-3.1-8b-instant", "object": "model"}, - {"id": "gemma-7b-it", "object": "model"}, - ] - } - mock_response.raise_for_status = Mock() - mock_get.return_value = mock_response - - # Mock tool calling - first succeeds, second fails - call_count = [0] - - def create_mock_client(*_args, **_kwargs): - mock_client = MagicMock() - if call_count[0] == 0: - # First call succeeds - mock_client.chat.completions.create.return_value = MagicMock() - else: - # Second call fails with tool error - mock_client.chat.completions.create.side_effect = ValueError("tool calling not supported") - call_count[0] += 1 - return mock_client - - mock_groq.side_effect = create_mock_client - - discovery = GroqModelDiscovery(api_key=mock_api_key) - discovery.CACHE_FILE = temp_cache_dir / ".cache" / "test_cache.json" - - models = discovery.get_models(force_refresh=True) - - # First model should support tools - assert models["llama-3.1-8b-instant"]["tool_calling"] is True - - # Second model should not support tools - assert models["gemma-7b-it"]["tool_calling"] is False - - def test_fallback_models_structure(self, mock_api_key): - """Test that fallback models have the correct structure.""" - discovery = GroqModelDiscovery(api_key=mock_api_key) - fallback = discovery._get_fallback_models() - - assert isinstance(fallback, dict) - assert len(fallback) == 2 - - for metadata in fallback.values(): - assert "name" in metadata - assert "provider" in metadata - assert "tool_calling" in metadata - assert "preview" in metadata - assert metadata["tool_calling"] is True # Fallback models should support tools - - -class TestGetGroqModelsConvenienceFunction: - """Test the convenience function get_groq_models().""" - - @patch.object(GroqModelDiscovery, "get_models") - def test_get_groq_models_with_api_key(self, mock_get_models, mock_api_key): - """Test get_groq_models() function with API key.""" - mock_get_models.return_value = {"llama-3.1-8b-instant": {}} - - models = get_groq_models(api_key=mock_api_key) - - assert "llama-3.1-8b-instant" in models - mock_get_models.assert_called_once_with(force_refresh=False) - - @patch.object(GroqModelDiscovery, "get_models") - def test_get_groq_models_without_api_key(self, mock_get_models): - """Test get_groq_models() function without API key.""" - mock_get_models.return_value = {"llama-3.1-8b-instant": {}} - - models = get_groq_models() - - assert "llama-3.1-8b-instant" in models - mock_get_models.assert_called_once_with(force_refresh=False) - - @patch.object(GroqModelDiscovery, "get_models") - def test_get_groq_models_force_refresh(self, mock_get_models, mock_api_key): - """Test get_groq_models() with force_refresh.""" - mock_get_models.return_value = {"llama-3.1-8b-instant": {}} - - models = get_groq_models(api_key=mock_api_key, force_refresh=True) - - assert "llama-3.1-8b-instant" in models - mock_get_models.assert_called_once_with(force_refresh=True) diff --git a/src/lfx/src/lfx/_assets/component_index.json b/src/lfx/src/lfx/_assets/component_index.json index 61189ff9d9cc..ab3ea80f9336 100644 --- a/src/lfx/src/lfx/_assets/component_index.json +++ b/src/lfx/src/lfx/_assets/component_index.json @@ -75104,7 +75104,7 @@ "icon": "Groq", "legacy": false, "metadata": { - "code_hash": "2aee7de6ee88", + "code_hash": "8a55d3fa8173", "dependencies": { "dependencies": [ { @@ -75227,7 +75227,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from pydantic.v1 import SecretStr\n\nfrom lfx.base.models.groq_constants import GROQ_MODELS\nfrom lfx.base.models.groq_model_discovery import get_groq_models\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput, SliderInput\nfrom lfx.log.logger import logger\n\n\nclass GroqModel(LCModelComponent):\n display_name: str = \"Groq\"\n description: str = \"Generate text using Groq.\"\n icon = \"Groq\"\n name = \"GroqModel\"\n\n inputs = [\n *LCModelComponent.get_base_inputs(),\n SecretStrInput(\n name=\"api_key\", display_name=\"Groq API Key\", info=\"API key for the Groq API.\", real_time_refresh=True\n ),\n MessageTextInput(\n name=\"base_url\",\n display_name=\"Groq API Base\",\n info=\"Base URL path for API requests, leave blank if not using a proxy or service emulator.\",\n advanced=True,\n value=\"https://api.groq.com\",\n real_time_refresh=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Output Tokens\",\n info=\"The maximum number of tokens to generate.\",\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Run inference with this temperature. Must by in the closed interval [0.0, 1.0].\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"n\",\n display_name=\"N\",\n info=\"Number of chat completions to generate for each prompt. \"\n \"Note that the API may not return the full n completions if duplicates are generated.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model\",\n info=\"The name of the model to use. Add your Groq API key to access additional available models.\",\n options=GROQ_MODELS,\n value=GROQ_MODELS[0],\n refresh_button=True,\n combobox=True,\n ),\n BoolInput(\n name=\"tool_model_enabled\",\n display_name=\"Enable Tool Models\",\n info=(\n \"Select if you want to use models that can work with tools. If yes, only those models will be shown.\"\n ),\n advanced=False,\n value=False,\n real_time_refresh=True,\n ),\n ]\n\n def get_models(self, *, tool_model_enabled: bool | None = None) -> list[str]:\n \"\"\"Get available Groq models using the dynamic discovery system.\n\n This method uses the groq_model_discovery module which:\n - Fetches models directly from Groq API\n - Automatically tests tool calling support\n - Caches results for 24 hours\n - Falls back to hardcoded list if API fails\n\n Args:\n tool_model_enabled: If True, only return models that support tool calling\n\n Returns:\n List of available model IDs\n \"\"\"\n try:\n # Get models with metadata from dynamic discovery system\n api_key = self.api_key if hasattr(self, \"api_key\") and self.api_key else None\n models_metadata = get_groq_models(api_key=api_key)\n\n # Filter out non-LLM models (audio, TTS, guards)\n model_ids = [\n model_id for model_id, metadata in models_metadata.items() if not metadata.get(\"not_supported\", False)\n ]\n\n # Filter by tool calling support if requested\n if tool_model_enabled:\n model_ids = [model_id for model_id in model_ids if models_metadata[model_id].get(\"tool_calling\", False)]\n logger.info(f\"Loaded {len(model_ids)} Groq models with tool calling support\")\n else:\n logger.info(f\"Loaded {len(model_ids)} Groq models\")\n except (ValueError, KeyError, TypeError, ImportError) as e:\n logger.exception(f\"Error getting model names: {e}\")\n # Fallback to hardcoded list from groq_constants.py\n return GROQ_MODELS\n else:\n return model_ids\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n if field_name in {\"base_url\", \"model_name\", \"tool_model_enabled\", \"api_key\"} and field_value:\n try:\n if len(self.api_key) != 0:\n try:\n ids = self.get_models(tool_model_enabled=self.tool_model_enabled)\n except (ValueError, KeyError, TypeError, ImportError) as e:\n logger.exception(f\"Error getting model names: {e}\")\n ids = GROQ_MODELS\n build_config.setdefault(\"model_name\", {})\n build_config[\"model_name\"][\"options\"] = ids\n build_config[\"model_name\"].setdefault(\"value\", ids[0])\n except (ValueError, KeyError, TypeError, AttributeError) as e:\n msg = f\"Error getting model names: {e}\"\n raise ValueError(msg) from e\n return build_config\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n try:\n from langchain_groq import ChatGroq\n except ImportError as e:\n msg = \"langchain-groq is not installed. Please install it with `pip install langchain-groq`.\"\n raise ImportError(msg) from e\n\n return ChatGroq(\n model=self.model_name,\n max_tokens=self.max_tokens or None,\n temperature=self.temperature,\n base_url=self.base_url,\n n=self.n or 1,\n api_key=SecretStr(self.api_key).get_secret_value(),\n streaming=self.stream,\n )\n" + "value": "from pydantic.v1 import SecretStr\n\nfrom lfx.base.models.groq_constants import GROQ_MODELS\nfrom lfx.base.models.groq_model_discovery import get_groq_models\nfrom lfx.base.models.model import LCModelComponent\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.io import BoolInput, DropdownInput, IntInput, MessageTextInput, SecretStrInput, SliderInput\nfrom lfx.log.logger import logger\n\n\nclass GroqModel(LCModelComponent):\n display_name: str = \"Groq\"\n description: str = \"Generate text using Groq.\"\n icon = \"Groq\"\n name = \"GroqModel\"\n\n inputs = [\n *LCModelComponent.get_base_inputs(),\n SecretStrInput(\n name=\"api_key\", display_name=\"Groq API Key\", info=\"API key for the Groq API.\", real_time_refresh=True\n ),\n MessageTextInput(\n name=\"base_url\",\n display_name=\"Groq API Base\",\n info=\"Base URL path for API requests, leave blank if not using a proxy or service emulator.\",\n advanced=True,\n value=\"https://api.groq.com\",\n real_time_refresh=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Output Tokens\",\n info=\"The maximum number of tokens to generate.\",\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Run inference with this temperature. Must by in the closed interval [0.0, 1.0].\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"n\",\n display_name=\"N\",\n info=\"Number of chat completions to generate for each prompt. \"\n \"Note that the API may not return the full n completions if duplicates are generated.\",\n advanced=True,\n ),\n DropdownInput(\n name=\"model_name\",\n display_name=\"Model\",\n info=\"The name of the model to use. Add your Groq API key to access additional available models.\",\n options=GROQ_MODELS,\n value=GROQ_MODELS[0],\n refresh_button=True,\n combobox=True,\n ),\n BoolInput(\n name=\"tool_model_enabled\",\n display_name=\"Enable Tool Models\",\n info=(\n \"Select if you want to use models that can work with tools. If yes, only those models will be shown.\"\n ),\n advanced=False,\n value=False,\n real_time_refresh=True,\n ),\n ]\n\n def get_models(self, *, tool_model_enabled: bool | None = None) -> list[str]:\n \"\"\"Get available Groq models using the dynamic discovery system.\n\n This method uses the groq_model_discovery module which:\n - Fetches models directly from Groq API\n - Automatically tests tool calling support\n - Caches results for 24 hours\n - Falls back to hardcoded list if API fails\n\n Args:\n tool_model_enabled: If True, only return models that support tool calling\n\n Returns:\n List of available model IDs\n \"\"\"\n try:\n # Get models with metadata from dynamic discovery system\n api_key = self.api_key if hasattr(self, \"api_key\") and self.api_key else None\n models_metadata = get_groq_models(api_key=api_key)\n\n # Filter out non-LLM models (audio, TTS, guards)\n model_ids = [\n model_id for model_id, metadata in models_metadata.items() if not metadata.get(\"not_supported\", False)\n ]\n\n # Filter by tool calling support if requested\n if tool_model_enabled:\n model_ids = [model_id for model_id in model_ids if models_metadata[model_id].get(\"tool_calling\", False)]\n logger.info(f\"Loaded {len(model_ids)} Groq models with tool calling support\")\n else:\n logger.info(f\"Loaded {len(model_ids)} Groq models\")\n except (ValueError, KeyError, TypeError, ImportError):\n logger.exception(\"Error getting model names\")\n # Fallback to hardcoded list from groq_constants.py\n return GROQ_MODELS\n else:\n return model_ids\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n if field_name in {\"base_url\", \"model_name\", \"tool_model_enabled\", \"api_key\"} and field_value:\n try:\n if len(self.api_key) != 0:\n try:\n ids = self.get_models(tool_model_enabled=self.tool_model_enabled)\n except (ValueError, KeyError, TypeError, ImportError):\n logger.exception(\"Error getting model names\")\n ids = GROQ_MODELS\n ids = ids or GROQ_MODELS\n build_config.setdefault(\"model_name\", {})\n build_config[\"model_name\"][\"options\"] = ids\n build_config[\"model_name\"].setdefault(\"value\", ids[0])\n except (ValueError, KeyError, TypeError, AttributeError) as e:\n msg = f\"Error getting model names: {e}\"\n raise ValueError(msg) from e\n return build_config\n\n def build_model(self) -> LanguageModel: # type: ignore[type-var]\n try:\n from langchain_groq import ChatGroq\n except ImportError as e:\n msg = \"langchain-groq is not installed. Please install it with `pip install langchain-groq`.\"\n raise ImportError(msg) from e\n\n return ChatGroq(\n model=self.model_name,\n max_tokens=self.max_tokens or None,\n temperature=self.temperature,\n base_url=self.base_url,\n n=self.n or 1,\n api_key=SecretStr(self.api_key).get_secret_value(),\n streaming=self.stream,\n )\n" }, "input_value": { "_input_type": "MessageInput", @@ -118487,6 +118487,6 @@ "num_components": 359, "num_modules": 97 }, - "sha256": "da998986ea45f043e65e13a6b57bdc130bcdcb8d6548d92857d6817dc929e802", + "sha256": "e08a909c9484aa9dec5cead73cc03ef97a5fc2e344399f2143de60e92cde3aef", "version": "0.3.0" } \ No newline at end of file diff --git a/src/lfx/src/lfx/_assets/stable_hash_history.json b/src/lfx/src/lfx/_assets/stable_hash_history.json index d3a89fc9445d..866546f2d330 100644 --- a/src/lfx/src/lfx/_assets/stable_hash_history.json +++ b/src/lfx/src/lfx/_assets/stable_hash_history.json @@ -911,7 +911,7 @@ }, "GroqModel": { "versions": { - "0.3.0": "2aee7de6ee88" + "0.3.0": "8a55d3fa8173" } }, "HomeAssistantControl": { diff --git a/src/lfx/src/lfx/base/models/groq_model_discovery.py b/src/lfx/src/lfx/base/models/groq_model_discovery.py index 7afcc2814338..56f201564a53 100644 --- a/src/lfx/src/lfx/base/models/groq_model_discovery.py +++ b/src/lfx/src/lfx/base/models/groq_model_discovery.py @@ -22,8 +22,11 @@ class GroqModelDiscovery: CACHE_FILE = Path(__file__).parent / ".cache" / "groq_models_cache.json" CACHE_DURATION = timedelta(hours=24) # Refresh cache every 24 hours - # Models to skip from LLM list (audio, TTS, guards) - SKIP_PATTERNS = ["whisper", "tts", "guard", "safeguard", "prompt-guard", "saba"] + # Models to skip from LLM list (audio, TTS, guards, speech) + SKIP_PATTERNS = ["whisper", "tts", "guard", "safeguard", "prompt-guard", "saba", "orpheus", "playai"] + + # Phrases that indicate an access/entitlement error rather than a capability error + ACCESS_ERROR_PHRASES = ["terms acceptance", "terms_required", "model_terms_required", "not available"] def __init__(self, api_key: str | None = None, base_url: str = "https://api.groq.com"): """Initialize discovery with optional API key for testing. @@ -83,10 +86,23 @@ def get_models(self, *, force_refresh: bool = False) -> dict[str, dict[str, Any] else: llm_models.append(model_id) - # Step 3: Test LLM models for tool calling - logger.info(f"Testing {len(llm_models)} LLM models for tool calling support...") + # Step 3: Test LLM models for chat completion and tool calling + logger.info(f"Testing {len(llm_models)} LLM models for capabilities...") for model_id in llm_models: + supports_chat = self._test_chat_completion(model_id) + if supports_chat is False: + # Model doesn't support chat completions at all (e.g. speech models) + non_llm_models.append(model_id) + logger.debug(f"{model_id}: does not support chat completions, skipping") + continue + if supports_chat is None: + # Transient/access error - assume chat is supported (benefit of the doubt) + logger.info(f"{model_id}: chat test indeterminate, assuming chat supported") supports_tools = self._test_tool_calling(model_id) + if supports_tools is None: + # Transient/access error on tool test - skip to avoid caching a false negative + logger.info(f"{model_id}: tool test indeterminate, skipping (will retry next refresh)") + continue models_metadata[model_id] = { "name": model_id, "provider": self._get_provider_name(model_id), @@ -108,12 +124,16 @@ def get_models(self, *, force_refresh: bool = False) -> dict[str, dict[str, Any] # Save to cache self._save_cache(models_metadata) - except (requests.RequestException, KeyError, ValueError, ImportError) as e: - logger.exception(f"Error discovering models: {e}") + except (requests.RequestException, KeyError, ValueError, ImportError): + logger.exception("Error discovering models") return self._get_fallback_models() else: return models_metadata + def _is_access_error(self, error_msg: str) -> bool: + """Return True if the lowercased error message indicates an access/entitlement issue.""" + return any(phrase in error_msg for phrase in self.ACCESS_ERROR_PHRASES) + def _fetch_available_models(self) -> list[str]: """Fetch list of available models from Groq API.""" url = f"{self.base_url}/openai/v1/models" @@ -126,19 +146,66 @@ def _fetch_available_models(self) -> list[str]: # Use direct access to raise KeyError if 'data' is missing return [model["id"] for model in model_list["data"]] - def _test_tool_calling(self, model_id: str) -> bool: + def _test_chat_completion(self, model_id: str) -> bool | None: + """Test if a model supports basic chat completions. + + This filters out non-chat models (e.g. TTS, speech, embedding models) + that appear in the API model list but cannot handle chat requests. + + Args: + model_id: The model ID to test + + Returns: + True if model supports chat completions, False if it does not, + None if the result is indeterminate (transient/access errors). + """ + try: + import groq + + client = groq.Groq(api_key=self.api_key, base_url=self.base_url) + messages = [{"role": "user", "content": "test"}] + client.chat.completions.create(model=model_id, messages=messages, max_tokens=1) + + except ImportError: + logger.warning("groq package not installed, cannot test chat completion") + # Propagate the ImportError so callers can fall back to hardcoded model metadata + raise + except Exception as e: # noqa: BLE001 + # The groq SDK does not expose a stable public exception hierarchy: errors can arrive as + # groq.APIStatusError, groq.BadRequestError, plain ValueError, or even undocumented + # runtime exceptions depending on the SDK version and the model being probed. We + # therefore catch Exception broadly and discriminate solely on the error message text, + # which is the only reliable signal available across SDK versions. + error_msg = str(e).lower() + # Genuine capability error: model does not support chat completions + if "does not support chat completions" in error_msg: + logger.debug(f"{model_id}: does not support chat completions") + return False + # Access/entitlement errors: model likely supports chat but is not accessible for this key + if self._is_access_error(error_msg): + logger.info(f"{model_id}: chat completion not accessible for this API key ({e})") + # Do not mark the model as non-chat; assume chat is supported but not usable with this key + return None + # Other errors (rate limits, transient failures) - indeterminate + logger.warning(f"Error testing chat for {model_id}: {e}") + return None + else: + return True + + def _test_tool_calling(self, model_id: str) -> bool | None: """Test if a model supports tool calling. Args: model_id: The model ID to test Returns: - True if model supports tool calling, False otherwise + True if model supports tool calling, False if it does not, + None if the result is indeterminate (transient/access errors). """ try: import groq - client = groq.Groq(api_key=self.api_key) + client = groq.Groq(api_key=self.api_key, base_url=self.base_url) # Simple tool definition tools = [ @@ -163,14 +230,24 @@ def _test_tool_calling(self, model_id: str) -> bool: model=model_id, messages=messages, tools=tools, tool_choice="auto", max_tokens=10 ) - except (ImportError, AttributeError, TypeError, ValueError, RuntimeError, KeyError) as e: + except ImportError: + logger.warning("groq package not installed, cannot test tool calling") + raise + except Exception as e: # noqa: BLE001 + # Same rationale as _test_chat_completion: the groq SDK's exception types are not + # stable across versions, so broad catching with message-based discrimination is the + # only portable approach. See _test_chat_completion for a full explanation. error_msg = str(e).lower() - # If error mentions tool calling, model doesn't support it + # Genuine capability error: model does not support tools if "tool" in error_msg: return False - # Other errors might be rate limits, etc - be conservative - logger.warning(f"Error testing {model_id}: {e}") - return False + # Access/entitlement errors: model may support tools but is not accessible for this key + if self._is_access_error(error_msg): + logger.info(f"{model_id}: tool calling not testable for this API key ({e})") + return None + # Any other API error (rate limits, transient failures, etc) - indeterminate + logger.warning(f"Error testing tool calling for {model_id}: {e}") + return None else: return True diff --git a/src/lfx/src/lfx/components/groq/groq.py b/src/lfx/src/lfx/components/groq/groq.py index 22b3a2d644f8..4b0d8e541848 100644 --- a/src/lfx/src/lfx/components/groq/groq.py +++ b/src/lfx/src/lfx/components/groq/groq.py @@ -101,8 +101,8 @@ def get_models(self, *, tool_model_enabled: bool | None = None) -> list[str]: logger.info(f"Loaded {len(model_ids)} Groq models with tool calling support") else: logger.info(f"Loaded {len(model_ids)} Groq models") - except (ValueError, KeyError, TypeError, ImportError) as e: - logger.exception(f"Error getting model names: {e}") + except (ValueError, KeyError, TypeError, ImportError): + logger.exception("Error getting model names") # Fallback to hardcoded list from groq_constants.py return GROQ_MODELS else: @@ -114,9 +114,10 @@ def update_build_config(self, build_config: dict, field_value: str, field_name: if len(self.api_key) != 0: try: ids = self.get_models(tool_model_enabled=self.tool_model_enabled) - except (ValueError, KeyError, TypeError, ImportError) as e: - logger.exception(f"Error getting model names: {e}") + except (ValueError, KeyError, TypeError, ImportError): + logger.exception("Error getting model names") ids = GROQ_MODELS + ids = ids or GROQ_MODELS build_config.setdefault("model_name", {}) build_config["model_name"]["options"] = ids build_config["model_name"].setdefault("value", ids[0]) From f553896a1fdec2dc85073df8c6592eab895f1758 Mon Sep 17 00:00:00 2001 From: Adam-Aghili <149833988+Adam-Aghili@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:42:00 -0400 Subject: [PATCH 18/29] chore: merge branch release-1.8.1 into main (#12185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Fixes Kubernetes deployment crash on runtime_port parsing (#11968) (#11975) * feat: add runtime port validation for Kubernetes service discovery * test: add unit tests for runtime port validation in Settings * fix: improve runtime port validation to handle exceptions and edge cases Co-authored-by: Gabriel Luiz Freitas Almeida * fix(frontend): show delete option for default session when it has messages (#11969) * feat: add documentation link to Guardrails component (#11978) * feat: add documentation link to Guardrails component * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * feat: traces v0 (#11689) (#11983) * feat: traces v0 v0 for traces includes: - filters: status, token usage range and datatime - accordian rows per trace Could add: - more filter options. Ecamples: session_id, trace_id and latency range * fix: token range * feat: create sidebar buttons for logs and trace add sidebar buttons for logs and trace remove lods canvas control * fix: fix duplicate trace ID insertion hopefully fix duplicate trace ID insertion on windows * fix: update tests and alembic tables for uts update tests and alembic tables for uts * chore: add session_id * chore: allo grouping by session_id and flow_id * chore: update race input output * chore: change run name to flow_name - flow_id was flow_name - trace_id now flow_name - flow_id * facelift * clean up and add testcases * clean up and add testcases * merge Alembic detected multiple heads * [autofix.ci] apply automated fixes * improve testcases * remodel files * chore: address gabriel simple changes address gabriel simple changes in traces.py and native.py * clean up and testcases * chore: address OTel and PG status comments https://github.com/langflow-ai/langflow/pull/11689#discussion_r2854630438 https://github.com/langflow-ai/langflow/pull/11689#discussion_r2854630446 * chore: OTel span naming convention model name is now set using name = f"{operation} {model_name}" if model_name else operation * add traces * feat: use uv sources for CPU-only PyTorch (#11884) * feat: use uv sources for CPU-only PyTorch Configure [tool.uv.sources] with pytorch-cpu index to avoid ~6GB CUDA dependencies in Docker images. This replaces hardcoded wheel URLs with a cleaner index-based approach. - Add pytorch-cpu index with explicit = true - Add torch/torchvision to [tool.uv.sources] - Add explicit torch/torchvision deps to trigger source override - Regenerate lockfile without nvidia/cuda/triton packages - Add required-environments for multi-platform support * fix: update regex to only replace name in [project] section The previous regex matched all lines starting with `name = "..."`, which incorrectly renamed the UV index `pytorch-cpu` to `langflow-nightly` during nightly builds. This caused `uv lock` to fail with: "Package torch references an undeclared index: pytorch-cpu" The new regex specifically targets the name field within the [project] section only, avoiding unintended replacements in other sections like [[tool.uv.index]]. * style: fix ruff quote style * fix: remove required-environments to fix Python 3.13 macOS x86_64 CI The required-environments setting was causing hard failures when packages like torch didn't have wheels for specific platform/Python combinations. Without this setting, uv resolves optimistically and handles missing wheels gracefully at runtime instead of failing during resolution. --------- * LE-270: Hydration and Console Log error (#11628) * LE-270: add fix hydration issues * LE-270: fix disable field on max token on language model --------- * test: add wait for selector in mcp server tests (#11883) * Add wait for selector in mcp server tests * [autofix.ci] apply automated fixes * Add more awit for selectors * [autofix.ci] apply automated fixes --------- * fix: reduce visual lag in frontend (#11686) * Reduce lag in frontend by batching react events and reducing minimval visual build time * Cleanup * [autofix.ci] apply automated fixes * add tests and improve code read * [autofix.ci] apply automated fixes * Remove debug log --------- * feat: lazy load imports for language model component (#11737) * Lazy load imports for language model component Ensures that only the necessary dependencies are required. For example, if OpenAI provider is used, it will now only import langchain_openai, rather than requiring langchain_anthropic, langchain_ibm, etc. * Add backwards-compat functions * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Add exception handling * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * comp index * docs: azure default temperature (#11829) * change-azure-openai-default-temperature-to-1.0 * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * [autofix.ci] apply automated fixes --------- * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * fix unit test? * add no-group dev to docker builds * [autofix.ci] apply automated fixes --------- * feat: generate requirements.txt from dependencies (#11810) * Base script to generate requirements Dymanically picks dependency for LanguageM Comp. Requires separate change to remove eager loading. * Lazy load imports for language model component Ensures that only the necessary dependencies are required. For example, if OpenAI provider is used, it will now only import langchain_openai, rather than requiring langchain_anthropic, langchain_ibm, etc. * Add backwards-compat functions * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Add exception handling * Add CLI command to create reqs * correctly exclude langchain imports * Add versions to reqs * dynamically resolve provider imports for language model comp * Lazy load imports for reqs, some ruff fixes * Add dynamic resolves for embedding model comp * Add install hints * Add missing provider tests; add warnings in reqs script * Add a few warnings and fix install hint * update comments add logging * Package hints, warnings, comments, tests * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * Add alias for watsonx * Fix anthropic for basic prompt, azure mapping * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * ruff * [autofix.ci] apply automated fixes * test formatting * ruff * [autofix.ci] apply automated fixes --------- * fix: add handle to file input to be able to receive text (#11825) * changed base file and file components to support muitiple files and files from messages * update component index * update input file component to clear value and show placeholder * updated starter projects * [autofix.ci] apply automated fixes * updated base file, file and video file to share robust file verification method * updated component index * updated templates * fix whitespaces * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * add file upload test for files fed through the handle * [autofix.ci] apply automated fixes * added tests and fixed things pointed out by revies * update component index * fixed test * ruff fixes * Update component_index.json * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * updated component index * updated component index * removed handle from file input * Added functionality to use multiple files on the File Path, and to allow files on the langflow file system. * [autofix.ci] apply automated fixes * fixed lfx test * build component index --------- * docs: Add AGENTS.md development guide (#11922) * add AGENTS.md rule to project * change to agents-example * remove agents.md * add example description * chore: address cris I1 comment address cris I1 comment * chore: address cris I5 address cris I5 * chore: address cris I6 address cris I6 * chore: address cris R7 address cris R7 * fix testcase * chore: address cris R2 address cris R2 * restructure insight page into sidenav * added header and total run node * restructing branch * chore: address gab otel model changes address gab otel model changes will need no migration tables * chore: update alembic migration tables update alembic migration tables after model changes * add empty state for gropu sessions * remove invalid mock * test: update and add backend tests update and add backend tests * chore: address backend code rabbit comments address backend code rabbit comments * chore: address code rabbit frontend comments address code rabbit frontend comments * chore: test_native_tracer minor fix address c1 test_native_tracer minor fix address c1 * chore: address C2 + C3 address C2 + C3 * chore: address H1-H5 address H1-H5 * test: update test_native_tracer update test_native_tracer * fixes * chore: address M2 address m2 * chore: address M1 address M1 * dry changes, factorization * chore: fix 422 spam and clean comments fix 422 spam and clean comments * chore: address M12 address M12 * chore: address M3 address M3 * chore: address M4 address M4 * chore: address M5 address M5 * chore: clean up for M7, M9, M11 clean up for M7, M9, M11 * chore: address L2,L4,L5,L6 + any test address L2,L4,L5 and L6 + any test * chore: alembic + comment clean up alembic + comment clean up * chore: remove depricated test_traces file remove depricated test_traces file. test have all been moved to test_traces_api.py * fix datetime * chore: fix test_trace_api ge=0 is allowed now fix test_trace_api ge=0 is allowed now * chore: remove unused traces cost flow remove unused traces cost flow * fix traces test * fix traces test * fix traces test * fix traces test * fix traces test * chore: address gabriels otel coment address gabriels otel coment latest --------- Co-authored-by: Olayinka Adelakun Co-authored-by: Olayinka Adelakun Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Ram Gopal Srikar Katakam <44802869+RamGopalSrikar@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 Co-authored-by: olayinkaadelakun Co-authored-by: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com> Co-authored-by: cristhianzl Co-authored-by: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com> Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com> Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Co-authored-by: Edwin Jose Co-authored-by: Himavarsha <40851462+HimavarshaVS@users.noreply.github.com> * fix(test): Fix superuser timeout test errors by replacing heavy clien… (#11982) fix(test): Fix superuser timeout test errors by replacing heavy client fixture (#11972) * fix super user timeout test error * fix fixture db test * remove canary test * [autofix.ci] apply automated fixes * flaky test --------- Co-authored-by: Cristhian Zanforlin Lousa Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * refactor(components): Replace eager import with lazy loading in agentics module (#11974) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix: add ondelete=CASCADE to TraceBase.flow_id to match migration (#12002) * fix: add ondelete=CASCADE to TraceBase.flow_id to match migration The migration file creates the trace table's flow_id foreign key with ondelete="CASCADE", but the model was missing this parameter. This mismatch caused the migration validator to block startup. Co-Authored-By: Claude Opus 4.5 * fix: add defensive migration to ensure trace.flow_id has CASCADE Adds a migration that ensures the trace.flow_id foreign key has ondelete=CASCADE. While the original migration already creates it with CASCADE, this provides a safety net for any databases that may have gotten into an inconsistent state. * fix: dynamically find FK constraint name in migration The original migration did not name the FK constraint, so it gets an auto-generated name that varies by database. This fix queries the database to find the actual constraint name before dropping it. --------- Co-authored-by: Claude Opus 4.5 * fix: LE-456 - Update ButtonSendWrapper to handle building state and improve button functionality (#12000) * fix: Update ButtonSendWrapper to handle building state and improve button functionality * fix(frontend): rename stop button title to avoid Playwright selector conflict The "Stop building" title caused getByRole('button', { name: 'Stop' }) to match two elements, breaking Playwright tests in shards 19, 20, 22, 25. Renamed to "Cancel" to avoid the collision with the no-input stop button. * Fix: pydantic fail because output is list, instead of a dict (#11987) pydantic fail because output is list, instead of a dict Co-authored-by: Olayinka Adelakun * refactor: Update guardrails icons (#12016) * Update guardrails.py Changing the heuristic threshold icons. The field was using the default icons. I added icons related to the security theme. * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com> * feat(ui): Replace Show column toggle with eye icon in advanced dialog (#12028) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ui): Prevent auto-focus and tooltip on dialog close button (#12027) * fix: reset button (#12024) fix reset button Co-authored-by: Olayinka Adelakun * fix: Handle message inputs when ingesting knowledge (#11988) * fix: Handle message inputs when ingesting knowledge * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) * Update test_ingestion.py * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix(ui): add error handling for invalid JSON uploads via upload button (#11985) * fix(ui): add error handling for invalid JSON uploads via upload button * feat(frontend): added new test for file upload * feat(frontend): added new test for file upload * fix(ui): Add array validation for provider variables mapping (#12032) * fix: LM span is now properly parent of ChatOpenAI (#12012) * fix: LM span is now properly parent of ChatOpenAI Before LM span and ChatOpenAI span where both considered parents so they where being counted twice in token counts and other sumations Now LM span is properly the parent of ChatOpenAI span so they are not accidently counted twice * chore: clean up comments clean up comments * chore: incase -> incase incase -> incase * fix: Design fix for traces (#12021) * fix: LM span is now properly parent of ChatOpenAI Before LM span and ChatOpenAI span where both considered parents so they where being counted twice in token counts and other sumations Now LM span is properly the parent of ChatOpenAI span so they are not accidently counted twice * chore: clean up comments clean up comments * chore: incase -> incase incase -> incase * design fix * fix testcases * fix header * fix testcase --------- Co-authored-by: Adam Aghili Co-authored-by: Olayinka Adelakun Co-authored-by: Olayinka Adelakun * fix: Add file upload extension filter for multi-select and folders (#12034) * fix: plaground - inspection panel feedback (#12013) * fix: update layout and variant for file previews in chat messages * fix: update background color to 'bg-muted' in chat header and input wrapper components * refactor(CanvasControls): remove unused inspection panel logic and clean up code * fix: remove 'bg-muted' class from chat header and add 'bg-primary-foreground' to chat sidebar * fix: add Escape key functionality to close sidebar * fix: playground does not scroll down to the latest user message upon … (#12040) fix: playground does not scroll down to the latest user message upon sending (Regression) (#12006) * fixes scroll is on input message * feat: re-engage Safari sticky scroll mode when user sends message Add custom event 'langflow-scroll-to-bottom' to force SafariScrollFix back into sticky mode when user sends a new message. This ensures the chat scrolls to bottom even if user had scrolled up, fixing behavior where Safari's scroll fix would remain disengaged after manual scrolling. Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com> * fix: knowledge Base Table — Row Icon Appears Clipped/Cut for Some Ent… (#12039) fix: knowledge Base Table — Row Icon Appears Clipped/Cut for Some Entries (#12009) * removed book and added file. makes more sense * feat: add accent-blue color to design system and update knowledge base file icon - Add accent-blue color variables to light and dark themes in CSS - Register accent-blue in Tailwind config with DEFAULT and foreground variants - Update knowledge base file icon fallback color from hardcoded text-blue-500 to text-accent-blue-foreground Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com> * fix: MCP Server Modal Improvements (#12017) (#12038) * fixes to the mcp modal for style * style: convert double quotes to single quotes in baseModal component * style: convert double quotes to single quotes in addMcpServerModal component Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com> * fix: change loop description (#12018) (#12037) * fix: change loop description (#12018) * docs: simplify Loop component description in starter project and component index * [autofix.ci] apply automated fixes * style: format Loop component description to comply with line length limits * fixed component index * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * [autofix.ci] apply automated fixes --------- Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * feat: add mutual exclusivity between ChatInput and Webhook components (#12036) * feat: add mutual exclusivity between ChatInput and Webhook components * [autofix.ci] apply automated fixes * refactor: address PR feedback - add comprehensive tests and constants * [autofix.ci] apply automated fixes * refactor: address PR feedback - add comprehensive tests and constants * [autofix.ci] apply automated fixes --------- Co-authored-by: Janardan S Kavia Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix: mcp config issue (#12045) * Only process dict template fields In json_schema_from_flow, guard access to template field properties by checking isinstance(field_data, dict) before calling .get(). This replaces the previous comparison to the string "Component" and prevents attribute errors when template entries are non-dict values, ensuring only dict-type fields with show=True and not advanced are included in the generated schema. * Check and handle MCP server URL changes When skipping creation of an existing MCP server for a user's starter projects, first compute the expected project URL and compare it to URLs found in the existing config args. If the URL matches, keep skipping and log that the server is correctly configured; if the URL differs (e.g., port changed on restart), log the difference and allow the flow to update the server configuration. Adds URL extraction and improved debug messages to support automatic updates when server endpoints change. --------- Co-authored-by: Ram Gopal Srikar Katakam <44802869+RamGopalSrikar@users.noreply.github.com> * fix: langflow breaks when we click on the last level of the chain (#12044) Langflow breaks when we click on the last level of the chain. Co-authored-by: Olayinka Adelakun * fix: standardize "README" title and update API key configuration note… (#12051) fix: standardize "README" title and update API key configuration notes in 3 main flow templates (#12005) * updated for README * chore: update secrets baseline with new line numbers * fixed test Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com> * fix: Cherry-pick Knowledge Base Improvements (le-480) into release-1.8.0 (#12052) * fix: improve knowledge base UI consistency and pagination handling - Change quote style from double to single quotes throughout knowledge base components - Update "Hide Sources" button label to "Hide Configuration" for clarity - Restructure SourceChunksPage layout to use xl:container for consistent spacing - Add controlled page input state with validation on blur and Enter key - Synchronize page input field with pagination controls to prevent state drift - Reset page input to "1" when changing page * refactor: extract page input commit logic into reusable function Extract page input validation and commit logic from handlePageInputBlur and handlePageInputKeyDown into a shared commitPageInput function to eliminate code duplication. * fix(ui): ensure session deletion properly clears backend and cache (#12043) * fix(ui): ensure session deletion properly clears backend and cache * fix: resolved PR comments and add new regression test * fix: resolved PR comments and add new regression test * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix: Check template field is dict before access (#12035) Only process dict template fields In json_schema_from_flow, guard access to template field properties by checking isinstance(field_data, dict) before calling .get(). This replaces the previous comparison to the string "Component" and prevents attribute errors when template entries are non-dict values, ensuring only dict-type fields with show=True and not advanced are included in the generated schema. Co-authored-by: Ram Gopal Srikar Katakam <44802869+RamGopalSrikar@users.noreply.github.com> * fix: hide Knowledge Ingestion component and rename Retrieval to Knowledge Base (#12054) * fix: hide Knowledge Ingestion component and rename Retrieval to Knowledge Base Move ingestion component to deactivated folder so it's excluded from dynamic discovery. Rename KnowledgeRetrievalComponent to KnowledgeBaseComponent with display_name "Knowledge Base". Update all exports, component index, starter project, frontend sidebar filter, and tests. * fix: update test_ingestion import to use deactivated module path * fix: skip deactivated KnowledgeIngestion test suite * [autofix.ci] apply automated fixes * fix: standardize formatting and indentation in StepperModal component --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix: Embedding Model Field Stuck in Infinite Loading When No Model Provider is Configured (release-1.8.0) (#12053) * fix: add showEmptyState prop to ModelInputComponent for better UX when no models are enabled * style: convert double quotes to single quotes in modelInputComponent * fixes refresh and kb blocker * style: convert double quotes to single quotes in ModelTrigger component * style: convert double quotes to single quotes in model provider components - Convert all double quotes to single quotes in use-get-model-providers.ts and ModelProvidersContent.tsx - Remove try-catch block in getModelProvidersFn to let errors propagate for React Query retry and stale data preservation - Add flex-shrink-0 to provider list container to prevent layout issues * fix: Close model dropdown popover before refresh to prevent width glitch (#12067) fix(test): Reduce response length assertions in flaky integration tests (#12057) * feat: Add PDF and DOCX ingestion support for Knowledge Bases (#12064) * add pdf and docx for knowledge bases * ruff style checker fix * fix jest test * fix: Use global LLM in knowledge retrieval (#11989) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Cristhian Zanforlin Lousa fix(test): Reduce response length assertions in flaky integration tests (#12057) * fix: Regenerate the knowledge retrieval template (#12070) * fix: refactor KnowledgeBaseEmptyState to use optimistic updates hook (#12069) * fix: refactor KnowledgeBaseEmptyState to use optimistic updates hook * updated tst * fix: Apply provider variable config to Agent build_config (#12050) * Apply provider variable config to Agent build_config Import and use apply_provider_variable_config_to_build_config in the Agent component so provider-specific variable settings (advanced/required/info/env fallbacks) are applied to the build_config. Provider-specific fields (e.g. base_url_ibm_watsonx, project_id) are hidden/disabled by default before applying the provider config. Updated embedded agent code in starter project JSONs and bumped their code_hashes accordingly. * [autofix.ci] apply automated fixes * update tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Himavarsha <40851462+HimavarshaVS@users.noreply.github.com> Co-authored-by: himavarshagoutham * LE-489: KB Metrics calculation batch caculator (#12049) Fixed metric calculator to be more robust and scalable. * fix(ui): Correct AstraDB icon size to use relative units (#12137) * fix(api): Handle Windows ChromaDB file locks when deleting Knowledge Bases (#12132) Co-authored-by: Claude Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix: Fix image preview for Windows paths in playground (#12136) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * chore: update fastapi dep (#12141) update fastapi dependency * fix: Properly propagate max tokens param to Agent (#12151) * fix: Properly Propagate max_tokens param * Update tests and templates * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes (attempt 3/3) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * fix: include uv/uvx in runtime Docker image (#12127) * fix: include uv/uvx in runtime Docker image add uv/uvx to runtime image so uvx is available in container i did this for all images which might be too much * chore: address supply chain attack addres ram's supply chain attack comment * chore: upgrade pyproject versions upgrade pyproject versions * fix: preserve api key configuration on flow export (#12129) * fix: preserve api key configuration on flow export Made-with: Cursor * fix individual component's field * [autofix.ci] apply automated fixes * unhide var name * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * fetch relevant provider keys * update starter projects * update based on env var * [autofix.ci] apply automated fixes * fetch only env variables * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * update starter projects * fix ruff errors * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * don't remove api keys if chosen by user * remove redundant code * [autofix.ci] apply automated fixes * fix update build config * remove api keys refactor * only load values when exists in db * modify other components * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Template updates * [autofix.ci] apply automated fixes * Component index update * Fix frontend test * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * preserve var names * [autofix.ci] apply automated fixes * update caution for saving api keys --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Hare * Fix: Tweaks override ENV VARIABLES (#12152) Modified tweak behaviour to be overridable if env variable is set on the GUI. * fix(mcp): Handle missing config file in MCP client availability detection (#12172) * Handle missing config file in MCP client availability detection * code improvements * [autofix.ci] apply automated fixes * code improvements review * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> * chore: clean up after merge * [autofix.ci] apply automated fixes * Component index update * [autofix.ci] apply automated fixes --------- Co-authored-by: Gabriel Luiz Freitas Almeida Co-authored-by: keval shah Co-authored-by: Antônio Alexandre Borges Lima <104531655+AntonioABLima@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Olayinka Adelakun Co-authored-by: Olayinka Adelakun Co-authored-by: Ram Gopal Srikar Katakam <44802869+RamGopalSrikar@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 Co-authored-by: olayinkaadelakun Co-authored-by: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com> Co-authored-by: cristhianzl Co-authored-by: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com> Co-authored-by: Mendon Kissling <59585235+mendonk@users.noreply.github.com> Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Co-authored-by: Edwin Jose Co-authored-by: Himavarsha <40851462+HimavarshaVS@users.noreply.github.com> Co-authored-by: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com> Co-authored-by: Lucas Democh Co-authored-by: Eric Hare Co-authored-by: Debojit Kaushik Co-authored-by: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com> Co-authored-by: Janardan Singh Kavia Co-authored-by: Janardan S Kavia Co-authored-by: himavarshagoutham --- .secrets.baseline | 1538 +++++++---------- docker/build_and_push.Dockerfile | 2 + docker/build_and_push_backend.Dockerfile | 3 +- docker/build_and_push_base.Dockerfile | 2 + docker/build_and_push_ep.Dockerfile | 2 + docker/build_and_push_with_extras.Dockerfile | 2 + pyproject.toml | 4 +- src/backend/base/langflow/api/utils/core.py | 11 + .../base/langflow/api/utils/kb_helpers.py | 113 +- .../base/langflow/api/v1/knowledge_bases.py | 31 +- .../base/langflow/api/v1/mcp_projects.py | 7 + .../Basic Prompt Chaining.json | 6 +- .../starter_projects/Basic Prompting.json | 2 +- .../starter_projects/Blog Writer.json | 2 +- .../Custom Component Generator.json | 2 +- .../starter_projects/Document Q&A.json | 2 +- .../starter_projects/Hybrid Search RAG.json | 2 +- .../Image Sentiment Analysis.json | 2 +- .../Instagram Copywriter.json | 8 +- .../starter_projects/Invoice Summarizer.json | 4 +- .../starter_projects/Knowledge Retrieval.json | 2 +- .../starter_projects/Market Research.json | 4 +- .../starter_projects/Meeting Summary.json | 4 +- .../starter_projects/Memory Chatbot.json | 2 +- .../starter_projects/News Aggregator.json | 4 +- .../starter_projects/Nvidia Remix.json | 8 +- .../Pok\303\251dex Agent.json" | 4 +- .../Portfolio Website Code Generator.json | 2 +- .../starter_projects/Price Deal Finder.json | 4 +- .../starter_projects/Research Agent.json | 8 +- .../Research Translation Loop.json | 247 +-- .../SEO Keyword Generator.json | 2 +- .../starter_projects/SaaS Pricing.json | 4 +- .../starter_projects/Search agent.json | 4 +- .../Sequential Tasks Agents.json | 12 +- .../starter_projects/Simple Agent.json | 4 +- .../starter_projects/Social Media Agent.json | 4 +- .../Text Sentiment Analysis.json | 6 +- .../Travel Planning Agents.json | 12 +- .../Twitter Thread Generator.json | 2 +- .../starter_projects/Vector Store RAG.json | 6 +- .../starter_projects/Youtube Analysis.json | 4 +- .../base/langflow/processing/process.py | 17 + .../base/langflow/utils/kb_constants.py | 4 + src/backend/base/pyproject.toml | 6 +- .../tests/unit/api/v1/test_mcp_projects.py | 212 +++ .../tests/unit/test_kb_storage_deletion.py | 392 +++++ .../tests/unit/test_knowledge_bases_api.py | 12 +- src/frontend/package-lock.json | 4 +- src/frontend/package.json | 2 +- .../hooks/use-fetch-data-on-mount.ts | 32 +- .../components/inputGlobalComponent/index.tsx | 19 +- .../components/ui/__tests__/dialog.test.tsx | 2 +- src/frontend/src/constants/constants.ts | 2 +- src/frontend/src/icons/AstraDB/AstraDB.jsx | 5 +- src/frontend/src/modals/exportModal/index.tsx | 4 +- .../src/utils/__tests__/removeApiKeys.test.ts | 79 + src/frontend/src/utils/reactflowUtils.ts | 29 +- src/frontend/tests/assets/outdated_flow.json | 2 +- .../session-deletion-data-leakage.spec.ts | 10 +- src/lfx/pyproject.toml | 4 +- src/lfx/src/lfx/_assets/component_index.json | 40 +- .../src/lfx/_assets/stable_hash_history.json | 1077 ++++++++---- src/lfx/src/lfx/base/models/unified_models.py | 143 +- .../llm_operations/lambda_filter.py | 17 +- .../llm_operations/llm_conditional_router.py | 17 +- .../lfx/components/models_and_agents/agent.py | 55 +- .../models_and_agents/embedding_model.py | 30 +- .../models_and_agents/language_model.py | 21 +- src/lfx/src/lfx/processing/process.py | 17 + .../inputs/test_max_tokens_propagation.py | 346 ++++ uv.lock | 776 +++++---- 72 files changed, 3366 insertions(+), 2104 deletions(-) create mode 100644 src/backend/tests/unit/test_kb_storage_deletion.py create mode 100644 src/frontend/src/utils/__tests__/removeApiKeys.test.ts create mode 100644 src/lfx/tests/unit/inputs/test_max_tokens_propagation.py diff --git a/.secrets.baseline b/.secrets.baseline index 23f92df054b1..f75b4573d541 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1325,7 +1325,7 @@ "filename": "src/backend/base/langflow/api/utils/core.py", "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", "is_verified": false, - "line_number": 429, + "line_number": 440, "is_secret": false } ], @@ -1335,15 +1335,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 390, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 657, + "line_number": 392, "is_secret": false }, { @@ -1351,7 +1343,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1502, + "line_number": 1506, "is_secret": false }, { @@ -1359,7 +1351,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json", "hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a", "is_verified": false, - "line_number": 1637, + "line_number": 1641, "is_secret": false }, { @@ -1367,7 +1359,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1697, + "line_number": 1701, "is_secret": false }, { @@ -1375,7 +1367,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json", "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", "is_verified": false, - "line_number": 1762, + "line_number": 1766, "is_secret": false } ], @@ -1385,15 +1377,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 122, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 618, + "line_number": 124, "is_secret": false }, { @@ -1401,7 +1385,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 900, + "line_number": 904, "is_secret": false }, { @@ -1409,7 +1393,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1093, + "line_number": 1097, "is_secret": false }, { @@ -1417,7 +1401,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json", "hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a", "is_verified": false, - "line_number": 1228, + "line_number": 1232, "is_secret": false }, { @@ -1425,7 +1409,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1288, + "line_number": 1292, "is_secret": false }, { @@ -1433,7 +1417,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json", "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", "is_verified": false, - "line_number": 1353, + "line_number": 1357, "is_secret": false } ], @@ -1441,17 +1425,9 @@ { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 532, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json", - "hashed_secret": "898a6c0a313f6e776b073bbc1b1e6010381c5d2b", + "hashed_secret": "b87e1fbb6e7bc22eafe7983b42d1b2bb7e4a60c2", "is_verified": false, - "line_number": 830, + "line_number": 1035, "is_secret": false }, { @@ -1459,7 +1435,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 1422, + "line_number": 1432, "is_secret": false } ], @@ -1469,15 +1445,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 1984, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 2262, + "line_number": 1993, "is_secret": false }, { @@ -1485,7 +1453,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 2547, + "line_number": 2558, "is_secret": false } ], @@ -1495,15 +1463,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 151, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 412, + "line_number": 153, "is_secret": false }, { @@ -1511,7 +1471,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 918, + "line_number": 922, "is_secret": false }, { @@ -1519,25 +1479,17 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json", "hashed_secret": "8c21d79a6f6a5080d3521470b90b316c89080f83", "is_verified": false, - "line_number": 1308, + "line_number": 1313, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 124, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 408, + "line_number": 414, "is_secret": false }, { @@ -1545,7 +1497,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json", "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", "is_verified": false, - "line_number": 741, + "line_number": 749, "is_secret": false }, { @@ -1553,7 +1505,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 888, + "line_number": 896, "is_secret": false }, { @@ -1561,7 +1513,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json", "hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a", "is_verified": false, - "line_number": 1023, + "line_number": 1031, "is_secret": false }, { @@ -1569,7 +1521,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1083, + "line_number": 1091, "is_secret": false }, { @@ -1577,15 +1529,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json", "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", "is_verified": false, - "line_number": 1148, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Financial Report Parser.json", - "hashed_secret": "898a6c0a313f6e776b073bbc1b1e6010381c5d2b", - "is_verified": false, - "line_number": 1391, + "line_number": 1156, "is_secret": false } ], @@ -1595,23 +1539,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 202, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json", - "hashed_secret": "898a6c0a313f6e776b073bbc1b1e6010381c5d2b", - "is_verified": false, - "line_number": 475, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 664, + "line_number": 208, "is_secret": false }, { @@ -1619,7 +1547,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json", "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", "is_verified": false, - "line_number": 1156, + "line_number": 1170, "is_secret": false }, { @@ -1627,7 +1555,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1303, + "line_number": 1317, "is_secret": false }, { @@ -1635,7 +1563,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1438, + "line_number": 1452, "is_secret": false }, { @@ -1643,7 +1571,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json", "hashed_secret": "e054a834b866b974a3c4802bab3a96e64226dc2e", "is_verified": false, - "line_number": 1732, + "line_number": 1748, "is_secret": false } ], @@ -1653,15 +1581,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 179, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 450, + "line_number": 183, "is_secret": false }, { @@ -1669,7 +1589,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json", "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", "is_verified": false, - "line_number": 1097, + "line_number": 1107, "is_secret": false }, { @@ -1677,7 +1597,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1244, + "line_number": 1254, "is_secret": false }, { @@ -1685,7 +1605,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1322, + "line_number": 1332, "is_secret": false }, { @@ -1693,7 +1613,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 1574, + "line_number": 1584, "is_secret": false } ], @@ -1703,23 +1623,15 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 318, + "line_number": 320, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 1120, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json", - "hashed_secret": "1579aca9caa27162a684e977c56693b37243d1b4", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 1650, + "line_number": 2074, "is_secret": false }, { @@ -1727,7 +1639,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 2644, + "line_number": 2652, "is_secret": false } ], @@ -1735,43 +1647,35 @@ { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "f16b56e2e46c4df6bf412a7a9b90c86957016575", "is_verified": false, - "line_number": 338, + "line_number": 677, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json", - "hashed_secret": "f16b56e2e46c4df6bf412a7a9b90c86957016575", + "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 673, + "line_number": 896, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json", - "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 892, + "line_number": 1182, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 255, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json", "hashed_secret": "1f01a7c11bde62eaf153d74394c282aa11574f2a", "is_verified": false, - "line_number": 534, + "line_number": 539, "is_secret": false } ], @@ -1781,31 +1685,15 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Market Research.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 179, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Market Research.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 446, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Market Research.json", - "hashed_secret": "1579aca9caa27162a684e977c56693b37243d1b4", - "is_verified": false, - "line_number": 768, + "line_number": 183, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Market Research.json", - "hashed_secret": "898a6c0a313f6e776b073bbc1b1e6010381c5d2b", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 1753, + "line_number": 1194, "is_secret": false }, { @@ -1813,25 +1701,17 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Market Research.json", "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", "is_verified": false, - "line_number": 1941, + "line_number": 1955, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 672, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 2079, + "line_number": 2097, "is_secret": false }, { @@ -1839,7 +1719,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 3035, + "line_number": 3056, "is_secret": false } ], @@ -1849,15 +1729,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 149, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 421, + "line_number": 151, "is_secret": false }, { @@ -1865,41 +1737,33 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 1296, + "line_number": 1301, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json", - "hashed_secret": "1be2449adf6092e0729be455a98c93034cc90bc8", - "is_verified": false, - "line_number": 209, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 576, + "line_number": 581, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 879, + "line_number": 1176, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json", - "hashed_secret": "53d87de97f77c9ea8b7795228a6ce24ed3dc0781", + "hashed_secret": "f1bd76c4aa6a65698214508669f07eb4c000a08b", "is_verified": false, - "line_number": 1745, + "line_number": 1755, "is_secret": false } ], @@ -1909,41 +1773,33 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 233, + "line_number": 235, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 510, + "line_number": 804, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json", - "hashed_secret": "591e20afc4fe981a10fed4cff9ea150e520d8585", + "hashed_secret": "327c06fdd2c0f5c179499c1702ab323443093c42", "is_verified": false, - "line_number": 1740, + "line_number": 1749, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 322, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json", "hashed_secret": "8c21d79a6f6a5080d3521470b90b316c89080f83", "is_verified": false, - "line_number": 914, + "line_number": 922, "is_secret": false }, { @@ -1951,7 +1807,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 1590, + "line_number": 1599, "is_secret": false }, { @@ -1959,7 +1815,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1781, + "line_number": 1790, "is_secret": false }, { @@ -1967,7 +1823,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json", "hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a", "is_verified": false, - "line_number": 1916, + "line_number": 1925, "is_secret": false }, { @@ -1975,7 +1831,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1976, + "line_number": 1985, "is_secret": false }, { @@ -1983,7 +1839,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json", "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", "is_verified": false, - "line_number": 2041, + "line_number": 2050, "is_secret": false }, { @@ -1991,7 +1847,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json", "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", "is_verified": false, - "line_number": 2309, + "line_number": 2320, "is_secret": false } ], @@ -2001,31 +1857,15 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 138, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 416, + "line_number": 140, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json", - "hashed_secret": "1579aca9caa27162a684e977c56693b37243d1b4", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 706, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json", - "hashed_secret": "1be2449adf6092e0729be455a98c93034cc90bc8", - "is_verified": false, - "line_number": 1120, + "line_number": 1610, "is_secret": false } ], @@ -2035,49 +1875,33 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 508, + "line_number": 510, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json", - "hashed_secret": "1579aca9caa27162a684e977c56693b37243d1b4", + "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 1351, + "line_number": 2052, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 1763, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json", - "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", - "is_verified": false, - "line_number": 2047, + "line_number": 2813, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 364, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 644, + "line_number": 585, "is_secret": false }, { @@ -2085,7 +1909,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 952, + "line_number": 879, "is_secret": false }, { @@ -2093,7 +1917,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1143, + "line_number": 1064, "is_secret": false }, { @@ -2101,33 +1925,17 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1221, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json", - "hashed_secret": "898a6c0a313f6e776b073bbc1b1e6010381c5d2b", - "is_verified": false, - "line_number": 1594, + "line_number": 1138, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 624, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 930, + "line_number": 934, "is_secret": false } ], @@ -2135,43 +1943,27 @@ { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 401, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json", - "hashed_secret": "236783f531bb4cc03a0f4a3e892b5c89e9f45881", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 715, + "line_number": 895, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/Search agent.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Search agent.json", - "hashed_secret": "f59912210d43c78fe803463f6bfb35688508a2bf", - "is_verified": false, - "line_number": 106, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Search agent.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 291, + "line_number": 294, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Search agent.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 566, + "line_number": 942, "is_secret": false } ], @@ -2179,59 +1971,43 @@ { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json", - "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 2069, + "line_number": 360, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json", - "hashed_secret": "236783f531bb4cc03a0f4a3e892b5c89e9f45881", - "is_verified": false, - "line_number": 3192, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json", - "hashed_secret": "1579aca9caa27162a684e977c56693b37243d1b4", + "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 3363, + "line_number": 2077, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "b8e5d31cffa4e410fe6b03a0855c592bceb48d82", "is_verified": false, - "line_number": 3774, + "line_number": 2970, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json", - "hashed_secret": "236783f531bb4cc03a0f4a3e892b5c89e9f45881", - "is_verified": false, - "line_number": 200, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 368, + "line_number": 371, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 647, + "line_number": 941, "is_secret": false }, { @@ -2239,7 +2015,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1273, + "line_number": 1278, "is_secret": false }, { @@ -2247,7 +2023,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json", "hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a", "is_verified": false, - "line_number": 1405, + "line_number": 1410, "is_secret": false }, { @@ -2255,7 +2031,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1465, + "line_number": 1470, "is_secret": false }, { @@ -2263,7 +2039,15 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json", "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", "is_verified": false, - "line_number": 1530, + "line_number": 1535, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json", + "hashed_secret": "b87e1fbb6e7bc22eafe7983b42d1b2bb7e4a60c2", + "is_verified": false, + "line_number": 1838, "is_secret": false } ], @@ -2273,7 +2057,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json", "hashed_secret": "59d43c509612f89c187f862266890ae0dd5fbb9a", "is_verified": false, - "line_number": 147, + "line_number": 150, "is_secret": false }, { @@ -2281,33 +2065,25 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 694, + "line_number": 698, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 970, + "line_number": 1291, "is_secret": false } ], "src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 820, - "is_secret": false - }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 1472, + "line_number": 1480, "is_secret": false }, { @@ -2315,7 +2091,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1663, + "line_number": 1671, "is_secret": false }, { @@ -2323,7 +2099,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json", "hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a", "is_verified": false, - "line_number": 1798, + "line_number": 1806, "is_secret": false }, { @@ -2331,7 +2107,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 1858, + "line_number": 1866, "is_secret": false }, { @@ -2339,7 +2115,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json", "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", "is_verified": false, - "line_number": 1923, + "line_number": 1931, "is_secret": false }, { @@ -2347,7 +2123,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json", "hashed_secret": "8c21d79a6f6a5080d3521470b90b316c89080f83", "is_verified": false, - "line_number": 3567, + "line_number": 3575, "is_secret": false } ], @@ -2357,31 +2133,15 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 232, + "line_number": 234, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 499, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json", - "hashed_secret": "236783f531bb4cc03a0f4a3e892b5c89e9f45881", - "is_verified": false, - "line_number": 1217, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json", - "hashed_secret": "5bf984f56eac13589ac2369cb0bae2f61869810a", - "is_verified": false, - "line_number": 1384, + "line_number": 1669, "is_secret": false } ], @@ -2391,15 +2151,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 284, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 702, + "line_number": 286, "is_secret": false }, { @@ -2407,7 +2159,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 2015, + "line_number": 2019, "is_secret": false } ], @@ -2417,7 +2169,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 237, + "line_number": 241, "is_secret": false }, { @@ -2425,23 +2177,15 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", "hashed_secret": "377e839f86c1529c656c82599fb225b4a1261ed5", "is_verified": false, - "line_number": 502, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", - "hashed_secret": "c2dc8a1d72a39ee9da360d47dcadfd7a5560ee7f", - "is_verified": false, - "line_number": 681, + "line_number": 506, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "76d1ef48b3fa8990b7a1aff6a177e5a5b2d018f8", "is_verified": false, - "line_number": 1000, + "line_number": 686, "is_secret": false }, { @@ -2449,7 +2193,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", "hashed_secret": "8c21d79a6f6a5080d3521470b90b316c89080f83", "is_verified": false, - "line_number": 1538, + "line_number": 1549, "is_secret": false }, { @@ -2457,7 +2201,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", "hashed_secret": "2317af15ade380e78be36f9ffdc6415d596a8715", "is_verified": false, - "line_number": 2213, + "line_number": 2225, "is_secret": false }, { @@ -2465,7 +2209,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 2402, + "line_number": 2414, "is_secret": false }, { @@ -2473,7 +2217,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", "hashed_secret": "d3d6fe3f7d33d0f4aa28c49544a865982a48a00a", "is_verified": false, - "line_number": 2537, + "line_number": 2549, "is_secret": false }, { @@ -2481,7 +2225,7 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json", "hashed_secret": "e054a834b866b974a3c4802bab3a96e64226dc2e", "is_verified": false, - "line_number": 2835, + "line_number": 2849, "is_secret": false } ], @@ -2491,39 +2235,31 @@ "filename": "src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json", "hashed_secret": "ef3435e29e3a2c5dcbbb633856c85561848cd995", "is_verified": false, - "line_number": 262, - "is_secret": false - }, - { - "type": "Secret Keyword", - "filename": "src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json", - "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", - "is_verified": false, - "line_number": 832, + "line_number": 268, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 1443, + "line_number": 503, "is_secret": false }, { - "type": "Hex High Entropy String", + "type": "Secret Keyword", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json", - "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", + "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 2195, + "line_number": 838, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json", - "hashed_secret": "bd773713e294c76ec00f052b4aa03f8501b74ee7", + "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 2472, + "line_number": 2210, "is_secret": false } ], @@ -3743,7 +3479,7 @@ "filename": "src/backend/tests/unit/test_user.py", "hashed_secret": "8bb6118f8fd6935ad0876a3be34a717d32708ffd", "is_verified": false, - "line_number": 71, + "line_number": 73, "is_secret": false }, { @@ -3751,7 +3487,7 @@ "filename": "src/backend/tests/unit/test_user.py", "hashed_secret": "f2c57870308dc87f432e5912d4de6f8e322721ba", "is_verified": false, - "line_number": 209, + "line_number": 211, "is_secret": false } ], @@ -4167,7 +3903,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "5717a1ee406aa657a2dacc80e2816c8f7dcae7e2", "is_verified": false, - "line_number": 930, + "line_number": 863, "is_secret": false }, { @@ -4175,7 +3911,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "d43f7dd3e51ce7cb8b9f3c26531a9e4c3a685785", "is_verified": false, - "line_number": 1464, + "line_number": 1360, "is_secret": false }, { @@ -4183,7 +3919,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "f261488408e7c6c4f5e9721426e652052ff36092", "is_verified": false, - "line_number": 1594, + "line_number": 1484, "is_secret": false }, { @@ -4191,7 +3927,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "e47929f0dc35b0d4eea6b4c80fa8fcdedd506d23", "is_verified": false, - "line_number": 1980, + "line_number": 1852, "is_secret": false }, { @@ -4199,15 +3935,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "a1d4fff4042a2dcb8c40293e53611f28a8721d8d", "is_verified": false, - "line_number": 2385, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "1be2449adf6092e0729be455a98c93034cc90bc8", - "is_verified": false, - "line_number": 2771, + "line_number": 2239, "is_secret": false }, { @@ -4215,7 +3943,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "42a810efde880424b1aec6d80360d8befa6c6521", "is_verified": false, - "line_number": 3088, + "line_number": 2908, "is_secret": false }, { @@ -4223,15 +3951,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "7014798bb60656a38da4a856545a06c773976112", "is_verified": false, - "line_number": 5025, + "line_number": 4784, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "a45df4ec5e76a1eb1199091a12fa8ee5e7af12a8", + "hashed_secret": "36ec1308f12e9f599e3845a6ad07d6591fb0c301", "is_verified": false, - "line_number": 5448, + "line_number": 5195, "is_secret": false }, { @@ -4239,7 +3967,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "b664327352fbd206a6ab38a8903fcabf1b1036a9", "is_verified": false, - "line_number": 5687, + "line_number": 5424, "is_secret": false }, { @@ -4247,15 +3975,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "59d43c509612f89c187f862266890ae0dd5fbb9a", "is_verified": false, - "line_number": 6029, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "c2258af5c2c23419d7469b26f77c954af427b4b8", - "is_verified": false, - "line_number": 8524, + "line_number": 5753, "is_secret": false }, { @@ -4263,7 +3983,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "597868714ac401a26b57be0f857457eeb984be18", "is_verified": false, - "line_number": 9261, + "line_number": 8824, "is_secret": false }, { @@ -4271,7 +3991,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "a178830480afc434270a7a53512d97758ec6d139", "is_verified": false, - "line_number": 9521, + "line_number": 9069, "is_secret": false }, { @@ -4279,7 +3999,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "6c7724fbb114bfc616ee7bbbb3214e58907abaf1", "is_verified": false, - "line_number": 10001, + "line_number": 9524, "is_secret": false }, { @@ -4287,7 +4007,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "794ae8fea8a51838b63423486552f5398a47e6fc", "is_verified": false, - "line_number": 10436, + "line_number": 9941, "is_secret": false }, { @@ -4295,7 +4015,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "97e68220b094141268772b8b601fa6cd7432de92", "is_verified": false, - "line_number": 10724, + "line_number": 10204, "is_secret": false }, { @@ -4303,7 +4023,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "a5af47522dc8a08746c380da81917bdd6eda057a", "is_verified": false, - "line_number": 11364, + "line_number": 10809, "is_secret": false }, { @@ -4311,7 +4031,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "9f66cbc518bb79dc6f0a78af0aa52bbadefe2399", "is_verified": false, - "line_number": 11840, + "line_number": 11269, "is_secret": false }, { @@ -4319,7 +4039,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "b3c2f9fda15f2d3816c7edc667bb24267be41a58", "is_verified": false, - "line_number": 12086, + "line_number": 11505, "is_secret": false }, { @@ -4327,7 +4047,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "72be8a21dd766c795332576419e6864eddc5db4e", "is_verified": false, - "line_number": 12317, + "line_number": 11727, "is_secret": false }, { @@ -4335,7 +4055,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "1659f95bebec345a9e20e32fa71e8eac4f32f6a2", "is_verified": false, - "line_number": 14636, + "line_number": 13992, "is_secret": false }, { @@ -4343,7 +4063,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "15e5f792860e53987a756bed19fba1204a671e19", "is_verified": false, - "line_number": 15289, + "line_number": 14639, "is_secret": false }, { @@ -4351,7 +4071,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "91700b2378ff5d682d1d57cff40818586609015d", "is_verified": false, - "line_number": 16595, + "line_number": 15933, "is_secret": false }, { @@ -4359,7 +4079,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "4b9838e8ff9ae89c3d23d3c853e0d07935618f00", "is_verified": false, - "line_number": 18554, + "line_number": 17874, "is_secret": false }, { @@ -4367,7 +4087,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "1aa0d90add98cf00965a327eed79bf65d589e3ce", "is_verified": false, - "line_number": 19207, + "line_number": 18521, "is_secret": false }, { @@ -4375,7 +4095,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "3698dc86868353e8ff5ed4564f78d45f1e6c08b7", "is_verified": false, - "line_number": 19860, + "line_number": 19168, "is_secret": false }, { @@ -4383,7 +4103,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "def35d315dd1ab5b0b4a05fc66847f6b73d0d853", "is_verified": false, - "line_number": 23125, + "line_number": 22403, "is_secret": false }, { @@ -4391,7 +4111,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "932fd84fba062a90506c3086945b53d4a6a3f169", "is_verified": false, - "line_number": 24431, + "line_number": 23697, "is_secret": false }, { @@ -4399,7 +4119,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "d1a66c6f4de1b56cc6e24cb0a9c78f5ba0230f56", "is_verified": false, - "line_number": 25084, + "line_number": 24344, "is_secret": false }, { @@ -4407,7 +4127,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "ddd35c43ce79e9b7ffc5f2894a1a92ad4da3297d", "is_verified": false, - "line_number": 25737, + "line_number": 24991, "is_secret": false }, { @@ -4415,7 +4135,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "bfa2c52c96d82a086f93287e90c3c889e292989e", "is_verified": false, - "line_number": 26390, + "line_number": 25638, "is_secret": false }, { @@ -4423,7 +4143,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "ac40271e91c0d84c26bf3613a94545872a801998", "is_verified": false, - "line_number": 29655, + "line_number": 28873, "is_secret": false }, { @@ -4431,7 +4151,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "691ee8aa156c92e8ae67859d9463020d1d5bec11", "is_verified": false, - "line_number": 32267, + "line_number": 31461, "is_secret": false }, { @@ -4439,7 +4159,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "5c33c0e3b39aa99ab095bf885b5f0688a9332b95", "is_verified": false, - "line_number": 32920, + "line_number": 32108, "is_secret": false }, { @@ -4447,7 +4167,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "7bfbc3a0161bb7553a4e14c1eb459d30cf104fdf", "is_verified": false, - "line_number": 33573, + "line_number": 32755, "is_secret": false }, { @@ -4455,7 +4175,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "da7592fd328658e5e783f4d16c62d1d6f9d3acd4", "is_verified": false, - "line_number": 34226, + "line_number": 33402, "is_secret": false }, { @@ -4463,7 +4183,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "f0e0ec0ff365d37b4fe860d63a9625ae529d3079", "is_verified": false, - "line_number": 34879, + "line_number": 34049, "is_secret": false }, { @@ -4471,7 +4191,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "23ce66526235ae0035cd8da3920a63c12c1c137a", "is_verified": false, - "line_number": 36838, + "line_number": 35990, "is_secret": false }, { @@ -4479,7 +4199,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "a75703e0eb9d3a13d977bf04fa3cc42e9d3c94a2", "is_verified": false, - "line_number": 40103, + "line_number": 39225, "is_secret": false }, { @@ -4487,7 +4207,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "2efc38920659af83e871e71004839171d3eaeba4", "is_verified": false, - "line_number": 42062, + "line_number": 41166, "is_secret": false }, { @@ -4495,7 +4215,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "4f514a159d49488561a2efe8585871ce25141548", "is_verified": false, - "line_number": 42715, + "line_number": 41813, "is_secret": false }, { @@ -4503,7 +4223,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "adb1d675969fb13f1d752232026b9872475aca4b", "is_verified": false, - "line_number": 45980, + "line_number": 45048, "is_secret": false }, { @@ -4511,7 +4231,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "99b6e13d3c63e4f323776aec40dda0551bc0aa56", "is_verified": false, - "line_number": 46633, + "line_number": 45695, "is_secret": false }, { @@ -4519,7 +4239,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "914bd29a063d63f5cda65b9193612041bf1b04e9", "is_verified": false, - "line_number": 49245, + "line_number": 48283, "is_secret": false }, { @@ -4527,7 +4247,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "dca20b45dc15f99f985e0f87aacf5569b014ede8", "is_verified": false, - "line_number": 49898, + "line_number": 48930, "is_secret": false }, { @@ -4535,7 +4255,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "9d48b00c8700d1dcab9108609465af7112840243", "is_verified": false, - "line_number": 50551, + "line_number": 49577, "is_secret": false }, { @@ -4543,15 +4263,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "e72cb4e0e589831cbbd71514f5b6db7f0d09fd37", "is_verified": false, - "line_number": 53163, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "03546202d2aee0b0998d1518625a6b271c345de1", - "is_verified": false, - "line_number": 53803, + "line_number": 52165, "is_secret": false }, { @@ -4559,7 +4271,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "753c0fdfc1e518b8c44cd464fb28080f3f94a9f4", "is_verified": false, - "line_number": 54048, + "line_number": 53039, "is_secret": false }, { @@ -4567,7 +4279,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "ab9b46808af9e1164b7a21d946a2cefcbfa9b769", "is_verified": false, - "line_number": 54726, + "line_number": 53690, "is_secret": false }, { @@ -4575,7 +4287,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "f4a6791157ee757125b9f46c2cf72ea19cdfb50e", "is_verified": false, - "line_number": 55174, + "line_number": 54110, "is_secret": false }, { @@ -4583,31 +4295,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "23a1f3524f7b992e6a225072ec63fc780f21da34", "is_verified": false, - "line_number": 55408, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "85080cbcb6a89304476c8a35d0c1e522afc56c47", - "is_verified": false, - "line_number": 56861, + "line_number": 54336, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "3179ea06ef24aee254dce7a4a3d7a02bcc6cb77f", - "is_verified": false, - "line_number": 57247, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "6ea8490b9c5872990ccc69e5d54fe850c28796b0", + "hashed_secret": "ed86f90a2d697ce47e0cff9b805f9761c4390048", "is_verified": false, - "line_number": 57428, + "line_number": 55727, "is_secret": false }, { @@ -4615,7 +4311,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "9a96eb0a8598688b358bdb4b37cdd0019f9934c7", "is_verified": false, - "line_number": 57586, + "line_number": 56392, "is_secret": false }, { @@ -4623,7 +4319,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "f846d79058594083280ddae8a1dbce083aaf6427", "is_verified": false, - "line_number": 57942, + "line_number": 56731, "is_secret": false }, { @@ -4631,7 +4327,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "fb0e32db4013340e8e096da4d7cba00c099d9542", "is_verified": false, - "line_number": 58075, + "line_number": 56858, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/lfx/src/lfx/_assets/component_index.json", + "hashed_secret": "b87e1fbb6e7bc22eafe7983b42d1b2bb7e4a60c2", + "is_verified": false, + "line_number": 57035, "is_secret": false }, { @@ -4639,7 +4343,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "d33546b1bd9d0542435f0f0946a6231edc175701", "is_verified": false, - "line_number": 59082, + "line_number": 57815, "is_secret": false }, { @@ -4647,7 +4351,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "f9ca36cde6942f27b76eac83290189854ff3acd5", "is_verified": false, - "line_number": 59304, + "line_number": 58022, "is_secret": false }, { @@ -4655,7 +4359,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "cf2179b851fcddc8328e4f40e46bec14a56747f8", "is_verified": false, - "line_number": 59379, + "line_number": 58093, "is_secret": false }, { @@ -4663,7 +4367,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "cc008700c5e02d5c9a7ca24219677922a3f82f17", "is_verified": false, - "line_number": 59578, + "line_number": 58280, "is_secret": false }, { @@ -4671,7 +4375,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "e054a834b866b974a3c4802bab3a96e64226dc2e", "is_verified": false, - "line_number": 60001, + "line_number": 58677, "is_secret": false }, { @@ -4679,7 +4383,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "7863a3a0eb2ed4e19329374549df3cef1ab7ed16", "is_verified": false, - "line_number": 60860, + "line_number": 59511, "is_secret": false }, { @@ -4687,7 +4391,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "41da17b522aa582bfb292d52e8dd307bada14400", "is_verified": false, - "line_number": 62056, + "line_number": 60681, "is_secret": false }, { @@ -4695,7 +4399,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "3632913dea26578a835e7c77ab7f4293d6ec1fe6", "is_verified": false, - "line_number": 62718, + "line_number": 61325, "is_secret": false }, { @@ -4703,7 +4407,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "0321ad34ab13e2dee03faa30b7645b932f24c4d6", "is_verified": false, - "line_number": 63904, + "line_number": 62466, "is_secret": false }, { @@ -4711,7 +4415,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "cb2623c527dbce4b4e4ac56407979cad7149ea9a", "is_verified": false, - "line_number": 64166, + "line_number": 62710, "is_secret": false }, { @@ -4719,23 +4423,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "427a8b3d029b9d8020cf1648330b5b0a01eb7e65", "is_verified": false, - "line_number": 65878, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "a0e9cb28c049bc9f6680cd51dbef7f227f556e50", - "is_verified": false, - "line_number": 66266, + "line_number": 64348, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "b5c86792f89b5c8eb61c92e9940a014475247b23", + "hashed_secret": "2fd7f5b7d8a18e5143661e9f7b51a5d9d36a917a", "is_verified": false, - "line_number": 67089, + "line_number": 65495, "is_secret": false }, { @@ -4743,7 +4439,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "5bc62a0f48f3bd1f4c9aa548fba2a0b0234fbbd8", "is_verified": false, - "line_number": 68588, + "line_number": 66913, "is_secret": false }, { @@ -4751,7 +4447,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "af246ca4758a5700d172533c40ff71522ae42d99", "is_verified": false, - "line_number": 68718, + "line_number": 67033, "is_secret": false }, { @@ -4759,7 +4455,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "8c21d79a6f6a5080d3521470b90b316c89080f83", "is_verified": false, - "line_number": 69175, + "line_number": 67478, "is_secret": false }, { @@ -4767,31 +4463,31 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "1f01a7c11bde62eaf153d74394c282aa11574f2a", "is_verified": false, - "line_number": 69862, + "line_number": 68150, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "53d87de97f77c9ea8b7795228a6ce24ed3dc0781", + "hashed_secret": "f1bd76c4aa6a65698214508669f07eb4c000a08b", "is_verified": false, - "line_number": 70121, + "line_number": 68403, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "70fb06614f8b86a3daac0c88f0409b40d689689c", + "hashed_secret": "a0378f1e9fbdee2f5792824f32acc16f95534180", "is_verified": false, - "line_number": 70989, + "line_number": 68841, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "21c64dba6f59dad4f7f4934d4416f2805cefbd5a", + "hashed_secret": "38245225908230e946fe961530d7b9bceca0d939", "is_verified": false, - "line_number": 71176, + "line_number": 69418, "is_secret": false }, { @@ -4799,15 +4495,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "1d3051aec8271f45991f72a68fc9be099d3e92c1", "is_verified": false, - "line_number": 71380, + "line_number": 69612, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "9f99b00169e0298e86716cdca88d9e546f9de36c", + "hashed_secret": "fb5c8ecafee0b2788e395c40f95b13eb45b464d8", "is_verified": false, - "line_number": 72245, + "line_number": 70415, "is_secret": false }, { @@ -4815,23 +4511,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "d377ef5b36367a118f28c20eb126e6ec376e02ea", "is_verified": false, - "line_number": 72509, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "b521ee08d1454bfeda09d831eaae591d8c12404c", - "is_verified": false, - "line_number": 72929, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "face7337620d002b928dc0088e5617aafb67b966", - "is_verified": false, - "line_number": 73171, + "line_number": 70660, "is_secret": false }, { @@ -4839,15 +4519,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "f0b2022fc412b5599ddcb48c6f8f87c5a53c26af", "is_verified": false, - "line_number": 73372, + "line_number": 71467, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "19b7d99d9b41aa84e4779f676bd2b22ce574906f", + "hashed_secret": "78f473648929d4b9dd716a3874df23eca9a2d9ed", "is_verified": false, - "line_number": 73520, + "line_number": 71603, "is_secret": false }, { @@ -4855,7 +4535,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "446fa65c4cc6c235fabac8cb7d9241fb018514b8", "is_verified": false, - "line_number": 74891, + "line_number": 72899, "is_secret": false }, { @@ -4863,7 +4543,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "9cc81943eb951dbf87e0fbb52da90903304b8db9", "is_verified": false, - "line_number": 75398, + "line_number": 73383, "is_secret": false }, { @@ -4871,7 +4551,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "c69107ff29daaa4b30788f9cecd01d67bfc29b71", "is_verified": false, - "line_number": 75581, + "line_number": 73555, "is_secret": false }, { @@ -4879,7 +4559,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "54be28b91891ca9ef7b85502a59b32a2a03a5cb9", "is_verified": false, - "line_number": 75747, + "line_number": 73711, "is_secret": false }, { @@ -4887,7 +4567,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "60f948a394e2811370ba0bb6849777f217ab5274", "is_verified": false, - "line_number": 75920, + "line_number": 73875, "is_secret": false }, { @@ -4895,23 +4575,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 77434, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 77709, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "303d5144ff32301287cc201ecc9243e2d73850bf", - "is_verified": false, - "line_number": 78205, + "line_number": 75355, "is_secret": false }, { @@ -4919,7 +4583,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "8b7be7f7fae86960989b939578d36ce617b498c6", "is_verified": false, - "line_number": 78369, + "line_number": 76231, "is_secret": false }, { @@ -4927,7 +4591,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "a6c79dfeb177d34d195c2be48cc62800e629f115", "is_verified": false, - "line_number": 78892, + "line_number": 76722, "is_secret": false }, { @@ -4935,7 +4599,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "ef417aa1e71aee527bd6fa12f4490f7d960ec54f", "is_verified": false, - "line_number": 79092, + "line_number": 76918, "is_secret": false }, { @@ -4943,71 +4607,71 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "a356ce34c2d87126e0170adbec7077e4421af5a5", "is_verified": false, - "line_number": 79948, + "line_number": 77714, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "d3acb69a725a514fb55033e2920abcc24e0162cc", + "hashed_secret": "ccc2dbe82ed3362b249ba70e1bb009cebe0896da", "is_verified": false, - "line_number": 81217, + "line_number": 78622, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "871ca8e6c9f88aba0a0e921f9d2f47120b55bdfc", + "hashed_secret": "6adc7a1a0903ddb6445918094b91181d7a0ad97b", "is_verified": false, - "line_number": 81631, + "line_number": 78914, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "f746c3a4610d3b777453c50c95dc93598c8ad694", + "hashed_secret": "871ca8e6c9f88aba0a0e921f9d2f47120b55bdfc", "is_verified": false, - "line_number": 82508, + "line_number": 79301, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "1f7c6ecf67ba34903861aad770957fdbfa774269", + "hashed_secret": "aa89465172bb84c366111aeeac493739fa195482", "is_verified": false, - "line_number": 83313, + "line_number": 79935, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "cd37616882a8287de17e49c9f91ecad00e0b0eae", + "hashed_secret": "f746c3a4610d3b777453c50c95dc93598c8ad694", "is_verified": false, - "line_number": 83484, + "line_number": 80127, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "9373f1ccd9980640fbcec9c685d34eac3c4b9867", + "hashed_secret": "cd37616882a8287de17e49c9f91ecad00e0b0eae", "is_verified": false, - "line_number": 84280, + "line_number": 81057, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "227af0d6a86c8c8619233794dcb4ea5ed1195be3", + "hashed_secret": "9373f1ccd9980640fbcec9c685d34eac3c4b9867", "is_verified": false, - "line_number": 84385, + "line_number": 81802, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "797b61cd33f73538a622541ccdb8eee79c4b51c2", + "hashed_secret": "227af0d6a86c8c8619233794dcb4ea5ed1195be3", "is_verified": false, - "line_number": 84786, + "line_number": 81897, "is_secret": false }, { @@ -5015,79 +4679,79 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "e907fb1ce9090d3555f18d6b2f2ea364d94c6217", "is_verified": false, - "line_number": 85349, + "line_number": 82813, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "bd773713e294c76ec00f052b4aa03f8501b74ee7", + "hashed_secret": "a71266907512ba33211f8ee38accedd3b84bf81a", "is_verified": false, - "line_number": 87198, + "line_number": 84827, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "a71266907512ba33211f8ee38accedd3b84bf81a", + "hashed_secret": "7bdcea8d073c580f79a0a1982007a226a2439dbb", "is_verified": false, - "line_number": 87464, + "line_number": 85102, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "7bdcea8d073c580f79a0a1982007a226a2439dbb", + "hashed_secret": "436a12a91365f61ddddfeb89b28089218f76b339", "is_verified": false, - "line_number": 87756, + "line_number": 85596, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "34e009441a3d84fe6d22ef3faceb9229532f0c69", + "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", "is_verified": false, - "line_number": 88029, + "line_number": 85866, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "15978126ab20054ba1215d2250564590cb6ba403", + "hashed_secret": "3b991cdd2510d7fd1de8b025f0c7cbb9ac84b931", "is_verified": false, - "line_number": 88285, + "line_number": 86155, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", + "hashed_secret": "573b6322edd45ab8e47491791f0909764e4a2f37", "is_verified": false, - "line_number": 88565, + "line_number": 86658, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "3b991cdd2510d7fd1de8b025f0c7cbb9ac84b931", + "hashed_secret": "8712dea5a30e8180a3f3cfb94b94dd2dce7db414", "is_verified": false, - "line_number": 88878, + "line_number": 86919, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "573b6322edd45ab8e47491791f0909764e4a2f37", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 89399, + "line_number": 88297, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "591e20afc4fe981a10fed4cff9ea150e520d8585", + "hashed_secret": "327c06fdd2c0f5c179499c1702ab323443093c42", "is_verified": false, - "line_number": 91659, + "line_number": 88825, "is_secret": false }, { @@ -5095,7 +4759,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "47ce443fa2c6d2894c896af5bf215e058b9211a7", "is_verified": false, - "line_number": 92984, + "line_number": 90090, "is_secret": false }, { @@ -5103,7 +4767,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "4676cd86733e19676c0704d55f548833f5273643", "is_verified": false, - "line_number": 93144, + "line_number": 90243, "is_secret": false }, { @@ -5111,7 +4775,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_verified": false, - "line_number": 93223, + "line_number": 90318, "is_secret": false }, { @@ -5119,7 +4783,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "f16b56e2e46c4df6bf412a7a9b90c86957016575", "is_verified": false, - "line_number": 93628, + "line_number": 90699, "is_secret": false }, { @@ -5127,7 +4791,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "a60fc256aaca59a332b08d58bd88404348a8bcb9", "is_verified": false, - "line_number": 93805, + "line_number": 90867, "is_secret": false }, { @@ -5135,7 +4799,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "04d0a3a2f4c5f2e29f293507958a27b53728c4e8", "is_verified": false, - "line_number": 94667, + "line_number": 91683, "is_secret": false }, { @@ -5143,7 +4807,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "dede8930d7418d092a12d114de08e444bf0dd82e", "is_verified": false, - "line_number": 95741, + "line_number": 92730, "is_secret": false }, { @@ -5151,7 +4815,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "2a6863fb102cdb7c5f83b6afd00a794efb701566", "is_verified": false, - "line_number": 96068, + "line_number": 93039, "is_secret": false }, { @@ -5159,7 +4823,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "4048123bacfc4d262ce85016a54ae55c8063edeb", "is_verified": false, - "line_number": 96301, + "line_number": 93252, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/lfx/src/lfx/_assets/component_index.json", + "hashed_secret": "89a477e59f5dec443fcb5e0487bc4816bba70cad", + "is_verified": false, + "line_number": 93428, "is_secret": false }, { @@ -5167,7 +4839,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "3de7722ca43ab9676c384eb479950083fb2385bb", "is_verified": false, - "line_number": 97314, + "line_number": 94224, "is_secret": false }, { @@ -5175,7 +4847,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "5ab5903f6c15a46a71c8db55e70119352304cc15", "is_verified": false, - "line_number": 98630, + "line_number": 95497, "is_secret": false }, { @@ -5183,15 +4855,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "4311a7e1eaf728d4f31467084f690eff7493a9e4", "is_verified": false, - "line_number": 99212, + "line_number": 96054, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "c6654393d0b0f14057873630031d040e3dea115d", + "hashed_secret": "778447ca63a67ae59fea8307b5c436abe9ebeb4f", "is_verified": false, - "line_number": 99539, + "line_number": 96363, "is_secret": false }, { @@ -5199,31 +4871,31 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "a229317aa176166d90f06d566b71932cff018638", "is_verified": false, - "line_number": 99723, + "line_number": 96528, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "08e984ad7dce0d92490e6fc8fe01910c8951109e", + "hashed_secret": "b4e24e4256b2c7bc502403f3155d8c5b70dc490c", "is_verified": false, - "line_number": 100611, + "line_number": 97380, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "3d442b2ea6e64698db1e44f7bd5ecb36daebc8a9", + "hashed_secret": "f11f7e5870cc432ebcfeeca740aa4d4c351a3d95", "is_verified": false, - "line_number": 101047, + "line_number": 98141, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "ef4b28ff7563e530637c74c37555b1fb5a6966f0", + "hashed_secret": "39a0fb1555a8bcd605b65b7b76dc3af2652d1bb8", "is_verified": false, - "line_number": 101423, + "line_number": 98258, "is_secret": false }, { @@ -5231,15 +4903,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "6516fc2579d674314a52e49462a84159df8479d9", "is_verified": false, - "line_number": 101732, + "line_number": 98428, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "8ab07507a1c24711ad94bb37308e838447d4a5ca", + "hashed_secret": "9d8a13c0d939f6b5790c6cb248c86f0a4aecc3e1", "is_verified": false, - "line_number": 101897, + "line_number": 98582, "is_secret": false }, { @@ -5247,71 +4919,71 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "c89fdd11b805574e2ba8910cf63c4273044b887c", "is_verified": false, - "line_number": 102126, + "line_number": 98785, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "f2dd454db702c939d54193f0be69d772368ac676", + "hashed_secret": "d7b281574944c11832ceba7860689cc5a6606a56", "is_verified": false, - "line_number": 102416, + "line_number": 98895, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "898a6c0a313f6e776b073bbc1b1e6010381c5d2b", + "hashed_secret": "29f233f1b444c5844c32773ad0e4db968a2e60ae", "is_verified": false, - "line_number": 102701, + "line_number": 99188, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "30ddcbfccd38de28196e92b6fcf77e65d122294d", + "hashed_secret": "7f06239cd274b6930ed82b15c617c6bee4e455aa", "is_verified": false, - "line_number": 103015, + "line_number": 99462, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "c2dc8a1d72a39ee9da360d47dcadfd7a5560ee7f", + "hashed_secret": "162232a42f9158e82bb9cd3bac15ff2b2d7efc59", "is_verified": false, - "line_number": 103146, + "line_number": 99587, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "efa90513d8e6348d4005c33485f2981bb2cc3411", + "hashed_secret": "76d1ef48b3fa8990b7a1aff6a177e5a5b2d018f8", "is_verified": false, - "line_number": 103389, + "line_number": 99710, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "eb2f1f46999a581c6a1b8a2279963002e4effd2d", + "hashed_secret": "efa90513d8e6348d4005c33485f2981bb2cc3411", "is_verified": false, - "line_number": 103605, + "line_number": 99936, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "25118f28f0772791b1febea557df6f8eb10d0dd8", + "hashed_secret": "df722750079d5f4eafa6946825f9dbc2dbb50330", "is_verified": false, - "line_number": 104184, + "line_number": 100138, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "240cd2b6629abde66f97f1955dd87fab8e045258", + "hashed_secret": "cb9c3fb93bf299a1eeaa8085a06e791f72d1c245", "is_verified": false, - "line_number": 104331, + "line_number": 100862, "is_secret": false }, { @@ -5319,7 +4991,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "cd50293b35634a61add9cbfeb9e48fbd44e78bc3", "is_verified": false, - "line_number": 105398, + "line_number": 101875, "is_secret": false }, { @@ -5327,207 +4999,207 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "b016c72dac43dd6eec034d8b49aa1ded1cc0c6fa", "is_verified": false, - "line_number": 105640, + "line_number": 102108, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "f59912210d43c78fe803463f6bfb35688508a2bf", + "hashed_secret": "72979300428bd6d483243f5d1cb7430ed1ebf6d3", "is_verified": false, - "line_number": 106099, + "line_number": 102433, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "b0e82a9a7bedac4135f97637be0c11faa2122599", + "hashed_secret": "08509f7cd25c76dc5ca97e64d21478a617fa193b", "is_verified": false, - "line_number": 106220, + "line_number": 102656, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "5bf984f56eac13589ac2369cb0bae2f61869810a", + "hashed_secret": "2acd680fbb8b14e98aea68cfef28ce81eba86c71", "is_verified": false, - "line_number": 106375, + "line_number": 104025, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "9f29336453dfa317f190f570b08116937a529f0b", + "hashed_secret": "2dd96ae1cb8802018fb2f6a27926bb5f78957fb0", "is_verified": false, - "line_number": 107111, + "line_number": 104148, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "1579aca9caa27162a684e977c56693b37243d1b4", + "hashed_secret": "3b61d62768cfb3c63d994d7988306f1ebd2acd6b", "is_verified": false, - "line_number": 107290, + "line_number": 104325, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "2acd680fbb8b14e98aea68cfef28ce81eba86c71", + "hashed_secret": "d17b2d823c9310229ad18c83ffe543f49406ff9b", "is_verified": false, - "line_number": 107668, + "line_number": 104807, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "2dd96ae1cb8802018fb2f6a27926bb5f78957fb0", + "hashed_secret": "e7d0065af9edfc8b2de193bbe26faf5a636e0e9f", "is_verified": false, - "line_number": 107802, + "line_number": 104964, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "3b61d62768cfb3c63d994d7988306f1ebd2acd6b", + "hashed_secret": "1bfed9fbd700374425b35a35ddf0f49a1e2469c2", "is_verified": false, - "line_number": 107988, + "line_number": 105370, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "d17b2d823c9310229ad18c83ffe543f49406ff9b", + "hashed_secret": "28ab1b1b9c8f05c055b6741bcaeab7337f5b5dc7", "is_verified": false, - "line_number": 108505, + "line_number": 105585, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "e7d0065af9edfc8b2de193bbe26faf5a636e0e9f", + "hashed_secret": "76377d63ef7d864c0cefc5b38c762e16d3ab39b5", "is_verified": false, - "line_number": 108675, + "line_number": 105959, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "1bfed9fbd700374425b35a35ddf0f49a1e2469c2", + "hashed_secret": "562c0bc758bca6446fabf1aacf71f63d47bc62ed", "is_verified": false, - "line_number": 109101, + "line_number": 106083, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "28ab1b1b9c8f05c055b6741bcaeab7337f5b5dc7", + "hashed_secret": "0113110e3d49f7b3a48e00192d478584449800e7", "is_verified": false, - "line_number": 109327, + "line_number": 106276, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "76377d63ef7d864c0cefc5b38c762e16d3ab39b5", + "hashed_secret": "8c8c106382a84413372980d0991df3e2a20c3797", "is_verified": false, - "line_number": 109729, + "line_number": 106469, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "562c0bc758bca6446fabf1aacf71f63d47bc62ed", + "hashed_secret": "1fd6ae619425c7c342273faad31539a8d2f61787", "is_verified": false, - "line_number": 109864, + "line_number": 107014, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "0113110e3d49f7b3a48e00192d478584449800e7", + "hashed_secret": "4ffc5d8cd514be957c9b87ac84c66205ab6d08d3", "is_verified": false, - "line_number": 110074, + "line_number": 107966, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "8e201f749e20ab2d51d0de3da73effa5f616448d", + "hashed_secret": "c0576697d180e97695dd29883a4e1ccb01b2f653", "is_verified": false, - "line_number": 110860, + "line_number": 108354, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "4ffc5d8cd514be957c9b87ac84c66205ab6d08d3", + "hashed_secret": "497af5dcf573db44fc30ac071ebb008e7ac37669", "is_verified": false, - "line_number": 111863, + "line_number": 108671, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "236783f531bb4cc03a0f4a3e892b5c89e9f45881", + "hashed_secret": "6a5f46048b547457e72572c2d38fb1046591ca71", "is_verified": false, - "line_number": 112179, + "line_number": 109670, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "c0576697d180e97695dd29883a4e1ccb01b2f653", + "hashed_secret": "7d770d0728208206c486b536b06077c9953d21f2", "is_verified": false, - "line_number": 112276, + "line_number": 110039, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "497af5dcf573db44fc30ac071ebb008e7ac37669", + "hashed_secret": "6fb5a96582d72c338a3f3a7d8144190630d64133", "is_verified": false, - "line_number": 112615, + "line_number": 110427, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "6a5f46048b547457e72572c2d38fb1046591ca71", + "hashed_secret": "270c9abba84329e1be2fa7130b44134c23891f1f", "is_verified": false, - "line_number": 113660, + "line_number": 110756, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "7d770d0728208206c486b536b06077c9953d21f2", + "hashed_secret": "a781a6064ef5e2cb085282bb1912e65232fb55d1", "is_verified": false, - "line_number": 114044, + "line_number": 111151, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "6fb5a96582d72c338a3f3a7d8144190630d64133", + "hashed_secret": "4943819ce50b5209882a4b91dd4b6140260ec428", "is_verified": false, - "line_number": 114446, + "line_number": 111686, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "270c9abba84329e1be2fa7130b44134c23891f1f", + "hashed_secret": "12690ce26e17773a811f28f4ce4fc91b7e42a747", "is_verified": false, - "line_number": 114784, + "line_number": 111978, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "a781a6064ef5e2cb085282bb1912e65232fb55d1", + "hashed_secret": "b8e5d31cffa4e410fe6b03a0855c592bceb48d82", "is_verified": false, - "line_number": 115189, + "line_number": 112473, "is_secret": false }, { @@ -5535,7 +5207,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "ef3435e29e3a2c5dcbbb633856c85561848cd995", "is_verified": false, - "line_number": 116957, + "line_number": 112838, "is_secret": false }, { @@ -5543,7 +5215,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "be1df677c309419f4efa0ac48afb2a573beeb95d", "is_verified": false, - "line_number": 117672, + "line_number": 113508, "is_secret": false }, { @@ -5551,7 +5223,7 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "5d65cf087adec89fb18354508030304fc3809586", "is_verified": false, - "line_number": 117939, + "line_number": 113771, "is_secret": false }, { @@ -5559,15 +5231,15 @@ "filename": "src/lfx/src/lfx/_assets/component_index.json", "hashed_secret": "76913f65d6da6c5660de587c8a3e807aafa039dd", "is_verified": false, - "line_number": 118151, + "line_number": 113972, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/component_index.json", - "hashed_secret": "ee52396d431bc5b88b6f5e9480b6586862988b17", + "hashed_secret": "388f5f398b4e4d7f3f069b4be2743b563c6618ba", "is_verified": false, - "line_number": 118315, + "line_number": 114125, "is_secret": false } ], @@ -5577,7 +5249,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "5717a1ee406aa657a2dacc80e2816c8f7dcae7e2", "is_verified": false, - "line_number": 14, + "line_number": 16, "is_secret": false }, { @@ -5585,7 +5257,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "d43f7dd3e51ce7cb8b9f3c26531a9e4c3a685785", "is_verified": false, - "line_number": 29, + "line_number": 34, "is_secret": false }, { @@ -5593,7 +5265,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "1be2449adf6092e0729be455a98c93034cc90bc8", "is_verified": false, - "line_number": 49, + "line_number": 58, "is_secret": false }, { @@ -5601,7 +5273,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "42a810efde880424b1aec6d80360d8befa6c6521", "is_verified": false, - "line_number": 59, + "line_number": 70, "is_secret": false }, { @@ -5609,7 +5281,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "7014798bb60656a38da4a856545a06c773976112", "is_verified": false, - "line_number": 79, + "line_number": 94, "is_secret": false }, { @@ -5617,7 +5289,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a45df4ec5e76a1eb1199091a12fa8ee5e7af12a8", "is_verified": false, - "line_number": 84, + "line_number": 100, "is_secret": false }, { @@ -5625,7 +5297,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "b664327352fbd206a6ab38a8903fcabf1b1036a9", "is_verified": false, - "line_number": 89, + "line_number": 106, "is_secret": false }, { @@ -5633,7 +5305,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "59d43c509612f89c187f862266890ae0dd5fbb9a", "is_verified": false, - "line_number": 94, + "line_number": 112, "is_secret": false }, { @@ -5641,7 +5313,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "c2258af5c2c23419d7469b26f77c954af427b4b8", "is_verified": false, - "line_number": 144, + "line_number": 172, "is_secret": false }, { @@ -5649,7 +5321,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "597868714ac401a26b57be0f857457eeb984be18", "is_verified": false, - "line_number": 154, + "line_number": 184, "is_secret": false }, { @@ -5657,7 +5329,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a178830480afc434270a7a53512d97758ec6d139", "is_verified": false, - "line_number": 159, + "line_number": 190, "is_secret": false }, { @@ -5665,7 +5337,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "6c7724fbb114bfc616ee7bbbb3214e58907abaf1", "is_verified": false, - "line_number": 164, + "line_number": 196, "is_secret": false }, { @@ -5673,7 +5345,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "794ae8fea8a51838b63423486552f5398a47e6fc", "is_verified": false, - "line_number": 169, + "line_number": 202, "is_secret": false }, { @@ -5681,7 +5353,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "97e68220b094141268772b8b601fa6cd7432de92", "is_verified": false, - "line_number": 174, + "line_number": 208, "is_secret": false }, { @@ -5689,7 +5361,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a5af47522dc8a08746c380da81917bdd6eda057a", "is_verified": false, - "line_number": 184, + "line_number": 220, "is_secret": false }, { @@ -5697,7 +5369,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "9f66cbc518bb79dc6f0a78af0aa52bbadefe2399", "is_verified": false, - "line_number": 189, + "line_number": 226, "is_secret": false }, { @@ -5705,7 +5377,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "b3c2f9fda15f2d3816c7edc667bb24267be41a58", "is_verified": false, - "line_number": 194, + "line_number": 232, "is_secret": false }, { @@ -5713,7 +5385,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "72be8a21dd766c795332576419e6864eddc5db4e", "is_verified": false, - "line_number": 199, + "line_number": 238, "is_secret": false }, { @@ -5721,7 +5393,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "1659f95bebec345a9e20e32fa71e8eac4f32f6a2", "is_verified": false, - "line_number": 224, + "line_number": 268, "is_secret": false }, { @@ -5729,7 +5401,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "15e5f792860e53987a756bed19fba1204a671e19", "is_verified": false, - "line_number": 229, + "line_number": 274, "is_secret": false }, { @@ -5737,7 +5409,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "91700b2378ff5d682d1d57cff40818586609015d", "is_verified": false, - "line_number": 239, + "line_number": 286, "is_secret": false }, { @@ -5745,7 +5417,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "4b9838e8ff9ae89c3d23d3c853e0d07935618f00", "is_verified": false, - "line_number": 254, + "line_number": 304, "is_secret": false }, { @@ -5753,7 +5425,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "1aa0d90add98cf00965a327eed79bf65d589e3ce", "is_verified": false, - "line_number": 259, + "line_number": 310, "is_secret": false }, { @@ -5761,7 +5433,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "3698dc86868353e8ff5ed4564f78d45f1e6c08b7", "is_verified": false, - "line_number": 264, + "line_number": 316, "is_secret": false }, { @@ -5769,7 +5441,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "def35d315dd1ab5b0b4a05fc66847f6b73d0d853", "is_verified": false, - "line_number": 294, + "line_number": 352, "is_secret": false }, { @@ -5777,7 +5449,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "932fd84fba062a90506c3086945b53d4a6a3f169", "is_verified": false, - "line_number": 304, + "line_number": 364, "is_secret": false }, { @@ -5785,7 +5457,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "d1a66c6f4de1b56cc6e24cb0a9c78f5ba0230f56", "is_verified": false, - "line_number": 309, + "line_number": 370, "is_secret": false }, { @@ -5793,7 +5465,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "ddd35c43ce79e9b7ffc5f2894a1a92ad4da3297d", "is_verified": false, - "line_number": 314, + "line_number": 376, "is_secret": false }, { @@ -5801,7 +5473,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "bfa2c52c96d82a086f93287e90c3c889e292989e", "is_verified": false, - "line_number": 319, + "line_number": 382, "is_secret": false }, { @@ -5809,7 +5481,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "ac40271e91c0d84c26bf3613a94545872a801998", "is_verified": false, - "line_number": 344, + "line_number": 412, "is_secret": false }, { @@ -5817,7 +5489,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "691ee8aa156c92e8ae67859d9463020d1d5bec11", "is_verified": false, - "line_number": 364, + "line_number": 436, "is_secret": false }, { @@ -5825,7 +5497,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f0e0ec0ff365d37b4fe860d63a9625ae529d3079", "is_verified": false, - "line_number": 369, + "line_number": 442, "is_secret": false }, { @@ -5833,7 +5505,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "5c33c0e3b39aa99ab095bf885b5f0688a9332b95", "is_verified": false, - "line_number": 374, + "line_number": 448, "is_secret": false }, { @@ -5841,7 +5513,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "7bfbc3a0161bb7553a4e14c1eb459d30cf104fdf", "is_verified": false, - "line_number": 384, + "line_number": 460, "is_secret": false }, { @@ -5849,7 +5521,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "da7592fd328658e5e783f4d16c62d1d6f9d3acd4", "is_verified": false, - "line_number": 389, + "line_number": 466, "is_secret": false }, { @@ -5857,7 +5529,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "23ce66526235ae0035cd8da3920a63c12c1c137a", "is_verified": false, - "line_number": 399, + "line_number": 478, "is_secret": false }, { @@ -5865,7 +5537,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a75703e0eb9d3a13d977bf04fa3cc42e9d3c94a2", "is_verified": false, - "line_number": 424, + "line_number": 508, "is_secret": false }, { @@ -5873,7 +5545,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "2efc38920659af83e871e71004839171d3eaeba4", "is_verified": false, - "line_number": 439, + "line_number": 526, "is_secret": false }, { @@ -5881,7 +5553,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "4f514a159d49488561a2efe8585871ce25141548", "is_verified": false, - "line_number": 444, + "line_number": 532, "is_secret": false }, { @@ -5889,7 +5561,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "adb1d675969fb13f1d752232026b9872475aca4b", "is_verified": false, - "line_number": 469, + "line_number": 562, "is_secret": false }, { @@ -5897,7 +5569,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "99b6e13d3c63e4f323776aec40dda0551bc0aa56", "is_verified": false, - "line_number": 474, + "line_number": 568, "is_secret": false }, { @@ -5905,7 +5577,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "914bd29a063d63f5cda65b9193612041bf1b04e9", "is_verified": false, - "line_number": 494, + "line_number": 592, "is_secret": false }, { @@ -5913,7 +5585,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "dca20b45dc15f99f985e0f87aacf5569b014ede8", "is_verified": false, - "line_number": 499, + "line_number": 598, "is_secret": false }, { @@ -5921,7 +5593,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "9d48b00c8700d1dcab9108609465af7112840243", "is_verified": false, - "line_number": 504, + "line_number": 604, "is_secret": false }, { @@ -5929,7 +5601,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "e72cb4e0e589831cbbd71514f5b6db7f0d09fd37", "is_verified": false, - "line_number": 524, + "line_number": 628, "is_secret": false }, { @@ -5937,7 +5609,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "03546202d2aee0b0998d1518625a6b271c345de1", "is_verified": false, - "line_number": 529, + "line_number": 634, "is_secret": false }, { @@ -5945,7 +5617,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "753c0fdfc1e518b8c44cd464fb28080f3f94a9f4", "is_verified": false, - "line_number": 534, + "line_number": 640, "is_secret": false }, { @@ -5953,7 +5625,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "ab9b46808af9e1164b7a21d946a2cefcbfa9b769", "is_verified": false, - "line_number": 544, + "line_number": 652, "is_secret": false }, { @@ -5961,7 +5633,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f4a6791157ee757125b9f46c2cf72ea19cdfb50e", "is_verified": false, - "line_number": 554, + "line_number": 664, "is_secret": false }, { @@ -5969,7 +5641,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "23a1f3524f7b992e6a225072ec63fc780f21da34", "is_verified": false, - "line_number": 564, + "line_number": 676, "is_secret": false }, { @@ -5977,7 +5649,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "85080cbcb6a89304476c8a35d0c1e522afc56c47", "is_verified": false, - "line_number": 579, + "line_number": 694, "is_secret": false }, { @@ -5985,7 +5657,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "3179ea06ef24aee254dce7a4a3d7a02bcc6cb77f", "is_verified": false, - "line_number": 584, + "line_number": 700, "is_secret": false }, { @@ -5993,7 +5665,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "6ea8490b9c5872990ccc69e5d54fe850c28796b0", "is_verified": false, - "line_number": 589, + "line_number": 706, "is_secret": false }, { @@ -6001,7 +5673,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "9a96eb0a8598688b358bdb4b37cdd0019f9934c7", "is_verified": false, - "line_number": 594, + "line_number": 712, "is_secret": false }, { @@ -6009,7 +5681,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f846d79058594083280ddae8a1dbce083aaf6427", "is_verified": false, - "line_number": 604, + "line_number": 724, "is_secret": false }, { @@ -6017,7 +5689,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "fb0e32db4013340e8e096da4d7cba00c099d9542", "is_verified": false, - "line_number": 609, + "line_number": 730, "is_secret": false }, { @@ -6025,7 +5697,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "cc008700c5e02d5c9a7ca24219677922a3f82f17", "is_verified": false, - "line_number": 624, + "line_number": 748, "is_secret": false }, { @@ -6033,7 +5705,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "7863a3a0eb2ed4e19329374549df3cef1ab7ed16", "is_verified": false, - "line_number": 634, + "line_number": 760, "is_secret": false }, { @@ -6041,7 +5713,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "41da17b522aa582bfb292d52e8dd307bada14400", "is_verified": false, - "line_number": 639, + "line_number": 766, "is_secret": false }, { @@ -6049,15 +5721,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "3632913dea26578a835e7c77ab7f4293d6ec1fe6", "is_verified": false, - "line_number": 644, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "e054a834b866b974a3c4802bab3a96e64226dc2e", - "is_verified": false, - "line_number": 654, + "line_number": 772, "is_secret": false }, { @@ -6065,7 +5729,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "d33546b1bd9d0542435f0f0946a6231edc175701", "is_verified": false, - "line_number": 664, + "line_number": 796, "is_secret": false }, { @@ -6073,7 +5737,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "0321ad34ab13e2dee03faa30b7645b932f24c4d6", "is_verified": false, - "line_number": 684, + "line_number": 820, "is_secret": false }, { @@ -6081,7 +5745,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "cb2623c527dbce4b4e4ac56407979cad7149ea9a", "is_verified": false, - "line_number": 689, + "line_number": 826, "is_secret": false }, { @@ -6089,7 +5753,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f9ca36cde6942f27b76eac83290189854ff3acd5", "is_verified": false, - "line_number": 694, + "line_number": 832, "is_secret": false }, { @@ -6097,7 +5761,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "cf2179b851fcddc8328e4f40e46bec14a56747f8", "is_verified": false, - "line_number": 699, + "line_number": 838, "is_secret": false }, { @@ -6105,7 +5769,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "427a8b3d029b9d8020cf1648330b5b0a01eb7e65", "is_verified": false, - "line_number": 719, + "line_number": 862, "is_secret": false }, { @@ -6113,7 +5777,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a0e9cb28c049bc9f6680cd51dbef7f227f556e50", "is_verified": false, - "line_number": 724, + "line_number": 868, "is_secret": false }, { @@ -6121,7 +5785,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "b5c86792f89b5c8eb61c92e9940a014475247b23", "is_verified": false, - "line_number": 739, + "line_number": 886, "is_secret": false }, { @@ -6129,7 +5793,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "5bc62a0f48f3bd1f4c9aa548fba2a0b0234fbbd8", "is_verified": false, - "line_number": 754, + "line_number": 904, "is_secret": false }, { @@ -6137,7 +5801,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "af246ca4758a5700d172533c40ff71522ae42d99", "is_verified": false, - "line_number": 759, + "line_number": 910, "is_secret": false }, { @@ -6145,7 +5809,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "8c21d79a6f6a5080d3521470b90b316c89080f83", "is_verified": false, - "line_number": 769, + "line_number": 922, "is_secret": false }, { @@ -6153,7 +5817,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "baacde28e4cf5095a02fd332813556fb52842d7b", "is_verified": false, - "line_number": 779, + "line_number": 933, "is_secret": false }, { @@ -6161,7 +5825,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "53d87de97f77c9ea8b7795228a6ce24ed3dc0781", "is_verified": false, - "line_number": 784, + "line_number": 938, "is_secret": false }, { @@ -6169,7 +5833,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "70fb06614f8b86a3daac0c88f0409b40d689689c", "is_verified": false, - "line_number": 799, + "line_number": 956, "is_secret": false }, { @@ -6177,7 +5841,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "21c64dba6f59dad4f7f4934d4416f2805cefbd5a", "is_verified": false, - "line_number": 804, + "line_number": 962, "is_secret": false }, { @@ -6185,7 +5849,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "1d3051aec8271f45991f72a68fc9be099d3e92c1", "is_verified": false, - "line_number": 809, + "line_number": 968, "is_secret": false }, { @@ -6193,7 +5857,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "9f99b00169e0298e86716cdca88d9e546f9de36c", "is_verified": false, - "line_number": 834, + "line_number": 998, "is_secret": false }, { @@ -6201,7 +5865,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "d377ef5b36367a118f28c20eb126e6ec376e02ea", "is_verified": false, - "line_number": 844, + "line_number": 1010, "is_secret": false }, { @@ -6209,7 +5873,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "b521ee08d1454bfeda09d831eaae591d8c12404c", "is_verified": false, - "line_number": 854, + "line_number": 1022, "is_secret": false }, { @@ -6217,7 +5881,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "face7337620d002b928dc0088e5617aafb67b966", "is_verified": false, - "line_number": 864, + "line_number": 1034, "is_secret": false }, { @@ -6225,7 +5889,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "19b7d99d9b41aa84e4779f676bd2b22ce574906f", "is_verified": false, - "line_number": 869, + "line_number": 1040, "is_secret": false }, { @@ -6233,7 +5897,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f0b2022fc412b5599ddcb48c6f8f87c5a53c26af", "is_verified": false, - "line_number": 874, + "line_number": 1046, "is_secret": false }, { @@ -6241,7 +5905,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "446fa65c4cc6c235fabac8cb7d9241fb018514b8", "is_verified": false, - "line_number": 909, + "line_number": 1088, "is_secret": false }, { @@ -6249,7 +5913,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "9cc81943eb951dbf87e0fbb52da90903304b8db9", "is_verified": false, - "line_number": 919, + "line_number": 1100, "is_secret": false }, { @@ -6257,7 +5921,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "c69107ff29daaa4b30788f9cecd01d67bfc29b71", "is_verified": false, - "line_number": 924, + "line_number": 1106, "is_secret": false }, { @@ -6265,7 +5929,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "60f948a394e2811370ba0bb6849777f217ab5274", "is_verified": false, - "line_number": 929, + "line_number": 1112, "is_secret": false }, { @@ -6273,7 +5937,15 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "54be28b91891ca9ef7b85502a59b32a2a03a5cb9", "is_verified": false, - "line_number": 934, + "line_number": 1118, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", + "hashed_secret": "b6df3a01285e2f59424c8ded9d38ecf39c0af1b7", + "is_verified": false, + "line_number": 1130, "is_secret": false }, { @@ -6281,7 +5953,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", "is_verified": false, - "line_number": 954, + "line_number": 1142, "is_secret": false }, { @@ -6289,7 +5961,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", "is_verified": false, - "line_number": 959, + "line_number": 1148, "is_secret": false }, { @@ -6297,7 +5969,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "303d5144ff32301287cc201ecc9243e2d73850bf", "is_verified": false, - "line_number": 974, + "line_number": 1166, "is_secret": false }, { @@ -6305,7 +5977,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "8b7be7f7fae86960989b939578d36ce617b498c6", "is_verified": false, - "line_number": 979, + "line_number": 1172, "is_secret": false }, { @@ -6313,7 +5985,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a6c79dfeb177d34d195c2be48cc62800e629f115", "is_verified": false, - "line_number": 994, + "line_number": 1190, "is_secret": false }, { @@ -6321,7 +5993,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "ef417aa1e71aee527bd6fa12f4490f7d960ec54f", "is_verified": false, - "line_number": 999, + "line_number": 1196, "is_secret": false }, { @@ -6329,7 +6001,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a356ce34c2d87126e0170adbec7077e4421af5a5", "is_verified": false, - "line_number": 1019, + "line_number": 1220, "is_secret": false }, { @@ -6337,7 +6009,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "d3acb69a725a514fb55033e2920abcc24e0162cc", "is_verified": false, - "line_number": 1054, + "line_number": 1262, "is_secret": false }, { @@ -6345,7 +6017,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "797b61cd33f73538a622541ccdb8eee79c4b51c2", "is_verified": false, - "line_number": 1074, + "line_number": 1286, "is_secret": false }, { @@ -6353,7 +6025,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "871ca8e6c9f88aba0a0e921f9d2f47120b55bdfc", "is_verified": false, - "line_number": 1079, + "line_number": 1292, "is_secret": false }, { @@ -6361,7 +6033,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f746c3a4610d3b777453c50c95dc93598c8ad694", "is_verified": false, - "line_number": 1094, + "line_number": 1310, "is_secret": false }, { @@ -6369,7 +6041,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "1f7c6ecf67ba34903861aad770957fdbfa774269", "is_verified": false, - "line_number": 1104, + "line_number": 1322, "is_secret": false }, { @@ -6377,7 +6049,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "cd37616882a8287de17e49c9f91ecad00e0b0eae", "is_verified": false, - "line_number": 1109, + "line_number": 1328, "is_secret": false }, { @@ -6385,7 +6057,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "9373f1ccd9980640fbcec9c685d34eac3c4b9867", "is_verified": false, - "line_number": 1134, + "line_number": 1358, "is_secret": false }, { @@ -6393,7 +6065,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "227af0d6a86c8c8619233794dcb4ea5ed1195be3", "is_verified": false, - "line_number": 1139, + "line_number": 1364, "is_secret": false }, { @@ -6401,7 +6073,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "e907fb1ce9090d3555f18d6b2f2ea364d94c6217", "is_verified": false, - "line_number": 1144, + "line_number": 1370, "is_secret": false }, { @@ -6409,63 +6081,63 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "bd773713e294c76ec00f052b4aa03f8501b74ee7", "is_verified": false, - "line_number": 1169, + "line_number": 1400, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "34e009441a3d84fe6d22ef3faceb9229532f0c69", + "hashed_secret": "436a12a91365f61ddddfeb89b28089218f76b339", "is_verified": false, - "line_number": 1174, + "line_number": 1412, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "15978126ab20054ba1215d2250564590cb6ba403", + "hashed_secret": "7bdcea8d073c580f79a0a1982007a226a2439dbb", "is_verified": false, - "line_number": 1179, + "line_number": 1418, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "7bdcea8d073c580f79a0a1982007a226a2439dbb", + "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", "is_verified": false, - "line_number": 1184, + "line_number": 1424, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", + "hashed_secret": "3b991cdd2510d7fd1de8b025f0c7cbb9ac84b931", "is_verified": false, - "line_number": 1189, + "line_number": 1430, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "3b991cdd2510d7fd1de8b025f0c7cbb9ac84b931", + "hashed_secret": "573b6322edd45ab8e47491791f0909764e4a2f37", "is_verified": false, - "line_number": 1194, + "line_number": 1442, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "573b6322edd45ab8e47491791f0909764e4a2f37", + "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", "is_verified": false, - "line_number": 1204, + "line_number": 1472, "is_secret": false }, { "type": "Hex High Entropy String", "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "591e20afc4fe981a10fed4cff9ea150e520d8585", + "hashed_secret": "327c06fdd2c0f5c179499c1702ab323443093c42", "is_verified": false, - "line_number": 1234, + "line_number": 1478, "is_secret": false }, { @@ -6473,7 +6145,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "47ce443fa2c6d2894c896af5bf215e058b9211a7", "is_verified": false, - "line_number": 1254, + "line_number": 1502, "is_secret": false }, { @@ -6481,7 +6153,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "4676cd86733e19676c0704d55f548833f5273643", "is_verified": false, - "line_number": 1259, + "line_number": 1508, "is_secret": false }, { @@ -6489,7 +6161,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f16b56e2e46c4df6bf412a7a9b90c86957016575", "is_verified": false, - "line_number": 1264, + "line_number": 1514, "is_secret": false }, { @@ -6497,7 +6169,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a60fc256aaca59a332b08d58bd88404348a8bcb9", "is_verified": false, - "line_number": 1269, + "line_number": 1520, "is_secret": false }, { @@ -6505,7 +6177,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "04d0a3a2f4c5f2e29f293507958a27b53728c4e8", "is_verified": false, - "line_number": 1279, + "line_number": 1532, "is_secret": false }, { @@ -6513,7 +6185,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "dede8930d7418d092a12d114de08e444bf0dd82e", "is_verified": false, - "line_number": 1294, + "line_number": 1550, "is_secret": false }, { @@ -6521,7 +6193,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "2a6863fb102cdb7c5f83b6afd00a794efb701566", "is_verified": false, - "line_number": 1304, + "line_number": 1562, "is_secret": false }, { @@ -6529,7 +6201,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "4048123bacfc4d262ce85016a54ae55c8063edeb", "is_verified": false, - "line_number": 1314, + "line_number": 1574, "is_secret": false }, { @@ -6537,7 +6209,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "3de7722ca43ab9676c384eb479950083fb2385bb", "is_verified": false, - "line_number": 1319, + "line_number": 1580, "is_secret": false }, { @@ -6545,7 +6217,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "5ab5903f6c15a46a71c8db55e70119352304cc15", "is_verified": false, - "line_number": 1334, + "line_number": 1598, "is_secret": false }, { @@ -6553,7 +6225,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "4311a7e1eaf728d4f31467084f690eff7493a9e4", "is_verified": false, - "line_number": 1344, + "line_number": 1610, "is_secret": false }, { @@ -6561,7 +6233,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "c6654393d0b0f14057873630031d040e3dea115d", "is_verified": false, - "line_number": 1349, + "line_number": 1616, "is_secret": false }, { @@ -6569,7 +6241,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a229317aa176166d90f06d566b71932cff018638", "is_verified": false, - "line_number": 1354, + "line_number": 1622, "is_secret": false }, { @@ -6577,7 +6249,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "25118f28f0772791b1febea557df6f8eb10d0dd8", "is_verified": false, - "line_number": 1359, + "line_number": 1628, "is_secret": false }, { @@ -6585,7 +6257,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "08e984ad7dce0d92490e6fc8fe01910c8951109e", "is_verified": false, - "line_number": 1374, + "line_number": 1646, "is_secret": false }, { @@ -6593,7 +6265,15 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "3d442b2ea6e64698db1e44f7bd5ecb36daebc8a9", "is_verified": false, - "line_number": 1379, + "line_number": 1652, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", + "hashed_secret": "244a01453acc60cca4380edd62539519c250d395", + "is_verified": false, + "line_number": 1658, "is_secret": false }, { @@ -6601,7 +6281,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "ef4b28ff7563e530637c74c37555b1fb5a6966f0", "is_verified": false, - "line_number": 1399, + "line_number": 1676, "is_secret": false }, { @@ -6609,7 +6289,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "6516fc2579d674314a52e49462a84159df8479d9", "is_verified": false, - "line_number": 1409, + "line_number": 1688, "is_secret": false }, { @@ -6617,7 +6297,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "8ab07507a1c24711ad94bb37308e838447d4a5ca", "is_verified": false, - "line_number": 1414, + "line_number": 1694, "is_secret": false }, { @@ -6625,7 +6305,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "c89fdd11b805574e2ba8910cf63c4273044b887c", "is_verified": false, - "line_number": 1424, + "line_number": 1706, "is_secret": false }, { @@ -6633,7 +6313,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f2dd454db702c939d54193f0be69d772368ac676", "is_verified": false, - "line_number": 1434, + "line_number": 1718, "is_secret": false }, { @@ -6641,7 +6321,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "898a6c0a313f6e776b073bbc1b1e6010381c5d2b", "is_verified": false, - "line_number": 1444, + "line_number": 1730, "is_secret": false }, { @@ -6649,7 +6329,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "30ddcbfccd38de28196e92b6fcf77e65d122294d", "is_verified": false, - "line_number": 1454, + "line_number": 1742, "is_secret": false }, { @@ -6657,7 +6337,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "c2dc8a1d72a39ee9da360d47dcadfd7a5560ee7f", "is_verified": false, - "line_number": 1459, + "line_number": 1748, "is_secret": false }, { @@ -6665,7 +6345,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "efa90513d8e6348d4005c33485f2981bb2cc3411", "is_verified": false, - "line_number": 1464, + "line_number": 1754, "is_secret": false }, { @@ -6673,7 +6353,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "eb2f1f46999a581c6a1b8a2279963002e4effd2d", "is_verified": false, - "line_number": 1469, + "line_number": 1760, "is_secret": false }, { @@ -6681,7 +6361,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "240cd2b6629abde66f97f1955dd87fab8e045258", "is_verified": false, - "line_number": 1474, + "line_number": 1766, "is_secret": false }, { @@ -6689,7 +6369,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "cd50293b35634a61add9cbfeb9e48fbd44e78bc3", "is_verified": false, - "line_number": 1494, + "line_number": 1790, "is_secret": false }, { @@ -6697,7 +6377,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "b016c72dac43dd6eec034d8b49aa1ded1cc0c6fa", "is_verified": false, - "line_number": 1499, + "line_number": 1796, "is_secret": false }, { @@ -6705,7 +6385,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f59912210d43c78fe803463f6bfb35688508a2bf", "is_verified": false, - "line_number": 1509, + "line_number": 1808, "is_secret": false }, { @@ -6713,7 +6393,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "b0e82a9a7bedac4135f97637be0c11faa2122599", "is_verified": false, - "line_number": 1514, + "line_number": 1814, "is_secret": false }, { @@ -6721,7 +6401,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "5bf984f56eac13589ac2369cb0bae2f61869810a", "is_verified": false, - "line_number": 1519, + "line_number": 1820, "is_secret": false }, { @@ -6729,7 +6409,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "9f29336453dfa317f190f570b08116937a529f0b", "is_verified": false, - "line_number": 1534, + "line_number": 1838, "is_secret": false }, { @@ -6737,7 +6417,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "1579aca9caa27162a684e977c56693b37243d1b4", "is_verified": false, - "line_number": 1539, + "line_number": 1844, "is_secret": false }, { @@ -6745,7 +6425,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "2acd680fbb8b14e98aea68cfef28ce81eba86c71", "is_verified": false, - "line_number": 1544, + "line_number": 1850, "is_secret": false }, { @@ -6753,7 +6433,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "2dd96ae1cb8802018fb2f6a27926bb5f78957fb0", "is_verified": false, - "line_number": 1549, + "line_number": 1856, "is_secret": false }, { @@ -6761,7 +6441,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "3b61d62768cfb3c63d994d7988306f1ebd2acd6b", "is_verified": false, - "line_number": 1554, + "line_number": 1862, "is_secret": false }, { @@ -6769,7 +6449,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "d17b2d823c9310229ad18c83ffe543f49406ff9b", "is_verified": false, - "line_number": 1564, + "line_number": 1874, "is_secret": false }, { @@ -6777,7 +6457,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "e7d0065af9edfc8b2de193bbe26faf5a636e0e9f", "is_verified": false, - "line_number": 1574, + "line_number": 1886, "is_secret": false }, { @@ -6785,7 +6465,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "1bfed9fbd700374425b35a35ddf0f49a1e2469c2", "is_verified": false, - "line_number": 1579, + "line_number": 1892, "is_secret": false }, { @@ -6793,7 +6473,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "28ab1b1b9c8f05c055b6741bcaeab7337f5b5dc7", "is_verified": false, - "line_number": 1584, + "line_number": 1898, "is_secret": false }, { @@ -6801,7 +6481,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "76377d63ef7d864c0cefc5b38c762e16d3ab39b5", "is_verified": false, - "line_number": 1589, + "line_number": 1904, "is_secret": false }, { @@ -6809,7 +6489,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "562c0bc758bca6446fabf1aacf71f63d47bc62ed", "is_verified": false, - "line_number": 1594, + "line_number": 1910, "is_secret": false }, { @@ -6817,7 +6497,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "0113110e3d49f7b3a48e00192d478584449800e7", "is_verified": false, - "line_number": 1599, + "line_number": 1916, "is_secret": false }, { @@ -6825,7 +6505,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "8e201f749e20ab2d51d0de3da73effa5f616448d", "is_verified": false, - "line_number": 1609, + "line_number": 1928, "is_secret": false }, { @@ -6833,7 +6513,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "4ffc5d8cd514be957c9b87ac84c66205ab6d08d3", "is_verified": false, - "line_number": 1644, + "line_number": 1970, "is_secret": false }, { @@ -6841,7 +6521,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "236783f531bb4cc03a0f4a3e892b5c89e9f45881", "is_verified": false, - "line_number": 1649, + "line_number": 1976, "is_secret": false }, { @@ -6849,7 +6529,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "c0576697d180e97695dd29883a4e1ccb01b2f653", "is_verified": false, - "line_number": 1654, + "line_number": 1982, "is_secret": false }, { @@ -6857,7 +6537,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "497af5dcf573db44fc30ac071ebb008e7ac37669", "is_verified": false, - "line_number": 1669, + "line_number": 2000, "is_secret": false }, { @@ -6865,7 +6545,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "7d770d0728208206c486b536b06077c9953d21f2", "is_verified": false, - "line_number": 1684, + "line_number": 2018, "is_secret": false }, { @@ -6873,7 +6553,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "6a5f46048b547457e72572c2d38fb1046591ca71", "is_verified": false, - "line_number": 1689, + "line_number": 2024, "is_secret": false }, { @@ -6881,7 +6561,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "270c9abba84329e1be2fa7130b44134c23891f1f", "is_verified": false, - "line_number": 1694, + "line_number": 2030, "is_secret": false }, { @@ -6889,7 +6569,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "6fb5a96582d72c338a3f3a7d8144190630d64133", "is_verified": false, - "line_number": 1699, + "line_number": 2036, "is_secret": false }, { @@ -6897,7 +6577,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a781a6064ef5e2cb085282bb1912e65232fb55d1", "is_verified": false, - "line_number": 1704, + "line_number": 2042, "is_secret": false }, { @@ -6905,7 +6585,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "ef3435e29e3a2c5dcbbb633856c85561848cd995", "is_verified": false, - "line_number": 1744, + "line_number": 2090, "is_secret": false }, { @@ -6913,7 +6593,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "be1df677c309419f4efa0ac48afb2a573beeb95d", "is_verified": false, - "line_number": 1759, + "line_number": 2108, "is_secret": false }, { @@ -6921,7 +6601,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "5d65cf087adec89fb18354508030304fc3809586", "is_verified": false, - "line_number": 1764, + "line_number": 2114, "is_secret": false }, { @@ -6929,7 +6609,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "76913f65d6da6c5660de587c8a3e807aafa039dd", "is_verified": false, - "line_number": 1774, + "line_number": 2126, "is_secret": false }, { @@ -6937,7 +6617,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a71266907512ba33211f8ee38accedd3b84bf81a", "is_verified": false, - "line_number": 1779, + "line_number": 2132, "is_secret": false }, { @@ -6945,7 +6625,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "f261488408e7c6c4f5e9721426e652052ff36092", "is_verified": false, - "line_number": 1789, + "line_number": 2144, "is_secret": false }, { @@ -6953,7 +6633,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "e47929f0dc35b0d4eea6b4c80fa8fcdedd506d23", "is_verified": false, - "line_number": 1794, + "line_number": 2150, "is_secret": false }, { @@ -6961,7 +6641,7 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "a1d4fff4042a2dcb8c40293e53611f28a8721d8d", "is_verified": false, - "line_number": 1799, + "line_number": 2156, "is_secret": false }, { @@ -6969,7 +6649,15 @@ "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", "hashed_secret": "1f01a7c11bde62eaf153d74394c282aa11574f2a", "is_verified": false, - "line_number": 1804, + "line_number": 2162, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", + "hashed_secret": "697ccfba2c15c7cd8cf6307fd83a491b5c2c9e3e", + "is_verified": false, + "line_number": 2173, "is_secret": false } ], @@ -6979,7 +6667,7 @@ "filename": "src/lfx/src/lfx/base/models/unified_models.py", "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", "is_verified": false, - "line_number": 1125, + "line_number": 1185, "is_secret": false }, { @@ -6987,7 +6675,7 @@ "filename": "src/lfx/src/lfx/base/models/unified_models.py", "hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a", "is_verified": false, - "line_number": 1136, + "line_number": 1196, "is_secret": false }, { @@ -6995,7 +6683,7 @@ "filename": "src/lfx/src/lfx/base/models/unified_models.py", "hashed_secret": "d4c3d66fd0c38547a3c7a4c6bdc29c36911bc030", "is_verified": false, - "line_number": 1150, + "line_number": 1210, "is_secret": false } ], @@ -7255,6 +6943,32 @@ "is_secret": false } ], + "src/lfx/tests/unit/inputs/test_max_tokens_propagation.py": [ + { + "type": "Secret Keyword", + "filename": "src/lfx/tests/unit/inputs/test_max_tokens_propagation.py", + "hashed_secret": "665b1e3851eefefa3fb878654292f16597d25155", + "is_verified": false, + "line_number": 107, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "src/lfx/tests/unit/inputs/test_max_tokens_propagation.py", + "hashed_secret": "e9b4dce312643ee0e1bd0561a50d9d5a7e5a2be1", + "is_verified": false, + "line_number": 140, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "src/lfx/tests/unit/inputs/test_max_tokens_propagation.py", + "hashed_secret": "3f2df46921dd8e2c36e2ce85238705ac0774c74a", + "is_verified": false, + "line_number": 220, + "is_secret": false + } + ], "src/lfx/tests/unit/run/test_base.py": [ { "type": "Secret Keyword", @@ -7378,5 +7092,5 @@ } ] }, - "generated_at": "2026-03-12T02:41:29Z" + "generated_at": "2026-03-13T17:17:05Z" } diff --git a/docker/build_and_push.Dockerfile b/docker/build_and_push.Dockerfile index 3b8f4d8bde98..eb4e07d722a4 100644 --- a/docker/build_and_push.Dockerfile +++ b/docker/build_and_push.Dockerfile @@ -81,6 +81,8 @@ RUN apt-get update \ && apt-get install --no-install-recommends -y curl git libpq5 gnupg xz-utils \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/local/bin/uv /usr/local/bin/uv +COPY --from=builder /usr/local/bin/uvx /usr/local/bin/uvx RUN ARCH=$(dpkg --print-architecture) \ && if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; \ elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \ diff --git a/docker/build_and_push_backend.Dockerfile b/docker/build_and_push_backend.Dockerfile index 232c05af89c1..cab058545692 100644 --- a/docker/build_and_push_backend.Dockerfile +++ b/docker/build_and_push_backend.Dockerfile @@ -55,7 +55,8 @@ RUN apt-get update \ xz-utils \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* - +COPY --from=builder /usr/local/bin/uv /usr/local/bin/uv +COPY --from=builder /usr/local/bin/uvx /usr/local/bin/uvx # Install Node.js (required for npx-based MCP stdio servers) RUN ARCH=$(dpkg --print-architecture) \ && if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; \ diff --git a/docker/build_and_push_base.Dockerfile b/docker/build_and_push_base.Dockerfile index 1b7c9cde05a0..36dd50c0b706 100644 --- a/docker/build_and_push_base.Dockerfile +++ b/docker/build_and_push_base.Dockerfile @@ -82,6 +82,8 @@ RUN apt-get update \ && apt-get install --no-install-recommends -y curl git libpq5 gnupg xz-utils \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/local/bin/uv /usr/local/bin/uv +COPY --from=builder /usr/local/bin/uvx /usr/local/bin/uvx RUN ARCH=$(dpkg --print-architecture) \ && if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; \ elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \ diff --git a/docker/build_and_push_ep.Dockerfile b/docker/build_and_push_ep.Dockerfile index 36b6c74da732..346fded7132f 100644 --- a/docker/build_and_push_ep.Dockerfile +++ b/docker/build_and_push_ep.Dockerfile @@ -77,6 +77,8 @@ RUN apt-get update \ && apt-get install --no-install-recommends -y curl git libpq5 gnupg xz-utils \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/local/bin/uv /usr/local/bin/uv +COPY --from=builder /usr/local/bin/uvx /usr/local/bin/uvx RUN ARCH=$(dpkg --print-architecture) \ && if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; \ elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \ diff --git a/docker/build_and_push_with_extras.Dockerfile b/docker/build_and_push_with_extras.Dockerfile index 409691e351ed..dae3fbd6e479 100644 --- a/docker/build_and_push_with_extras.Dockerfile +++ b/docker/build_and_push_with_extras.Dockerfile @@ -78,6 +78,8 @@ RUN apt-get update \ && apt-get install --no-install-recommends -y curl git libpq5 gnupg xz-utils \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/local/bin/uv /usr/local/bin/uv +COPY --from=builder /usr/local/bin/uvx /usr/local/bin/uvx RUN ARCH=$(dpkg --print-architecture) \ && if [ "$ARCH" = "amd64" ]; then NODE_ARCH="x64"; \ elif [ "$ARCH" = "arm64" ]; then NODE_ARCH="arm64"; \ diff --git a/pyproject.toml b/pyproject.toml index 46b739e86c1a..fe79631d0950 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langflow" -version = "1.8.0" +version = "1.8.1" description = "A Python package with a built-in web application" requires-python = ">=3.10,<3.14" license = "MIT" @@ -17,7 +17,7 @@ maintainers = [ ] # Define your main dependencies here dependencies = [ - "langflow-base[complete]~=0.8.0", + "langflow-base[complete]~=0.8.1", ] diff --git a/src/backend/base/langflow/api/utils/core.py b/src/backend/base/langflow/api/utils/core.py index 8e0c67c0ba41..73412cde607e 100644 --- a/src/backend/base/langflow/api/utils/core.py +++ b/src/backend/base/langflow/api/utils/core.py @@ -69,6 +69,17 @@ def has_api_terms(word: str): return "api" in word and ("key" in word or ("token" in word and "tokens" not in word)) +def _get_provider_from_template(template: dict) -> str | None: + """Return provider name from template's model field, if any.""" + model_field = template.get("model") + if not isinstance(model_field, dict): + return None + raw = model_field.get("value") + if isinstance(raw, list) and len(raw) > 0 and isinstance(raw[0], dict): + return raw[0].get("provider") + return None + + def remove_api_keys(flow: dict): """Remove api keys from flow data.""" for node in flow.get("data", {}).get("nodes", []): diff --git a/src/backend/base/langflow/api/utils/kb_helpers.py b/src/backend/base/langflow/api/utils/kb_helpers.py index 466125bed34a..3d5481255b9b 100644 --- a/src/backend/base/langflow/api/utils/kb_helpers.py +++ b/src/backend/base/langflow/api/utils/kb_helpers.py @@ -2,6 +2,8 @@ import contextlib import gc import json +import shutil +import time import uuid from datetime import datetime, timezone from functools import lru_cache @@ -25,8 +27,10 @@ from langflow.services.deps import get_settings_service from langflow.services.jobs.service import JobService from langflow.utils.kb_constants import ( + DELETE_BACKOFF_SECONDS, EXPONENTIAL_BACKOFF_MULTIPLIER, INGESTION_BATCH_SIZE, + MAX_DELETE_RETRIES, MAX_RETRY_ATTEMPTS, ) @@ -81,8 +85,31 @@ def get_fresh_chroma_client(kb_path: Path) -> chromadb.PersistentClient: ) @staticmethod - def teardown_storage(kb_path: Path, kb_name: str) -> None: - """Explicitly flush and invalidate Chroma clients before directory deletion.""" + def release_chroma_resources(kb_path: Path) -> None: + """Release ChromaDB resources by clearing the registry entry and forcing GC.""" + path_key = str(kb_path) + try: + if path_key in SharedSystemClient._identifier_to_system: # noqa: SLF001 + del SharedSystemClient._identifier_to_system[path_key] # noqa: SLF001 + except KeyError: + pass + gc.collect() + + @staticmethod + def delete_storage(kb_path: Path, kb_name: str) -> bool: + """Teardown ChromaDB connections and delete KB directory with retry logic. + + Handles ChromaDB SQLite file locks that can prevent deletion, particularly + on Windows where mandatory file locks block deletion of open files. + Uses retry with exponential backoff and rename-as-fallback strategy. + + Returns: + True if deletion succeeded (or path already gone), False otherwise. + """ + if not kb_path.exists(): + return True + + # Teardown ChromaDB collection to release handles try: has_data = any((kb_path / m).exists() for m in ["chroma", "chroma.sqlite3", "index"]) if has_data: @@ -91,9 +118,70 @@ def teardown_storage(kb_path: Path, kb_name: str) -> None: with contextlib.suppress(Exception): chroma.delete_collection() chroma = None - gc.collect() + client = None except (OSError, ValueError, TypeError, chromadb.errors.ChromaError) as e: - logger.debug(f"Storage teardown failed for {kb_path.name} (ignoring): {e}") + logger.debug("Collection teardown failed for %s: %s", kb_path.name, e) + + gc.collect() + + for attempt in range(MAX_DELETE_RETRIES): + try: + if attempt > 0: + time.sleep(DELETE_BACKOFF_SECONDS * (2**attempt)) + + _remove_sqlite_lock_files(kb_path) + _truncate_sqlite_files(kb_path) + gc.collect() + + shutil.rmtree(kb_path, ignore_errors=False) + + if not kb_path.exists(): + logger.info("Deleted knowledge base %s on attempt %d", kb_name, attempt + 1) + return True + + except OSError as e: + if attempt < MAX_DELETE_RETRIES - 1: + logger.debug("KB deletion attempt %d failed for %s: %s", attempt + 1, kb_name, e) + else: + logger.warning( + "KB deletion failed for %s after %d attempts: %s", + kb_name, + MAX_DELETE_RETRIES, + e, + ) + + # Last resort: rename for deferred cleanup + if kb_path.exists(): + try: + deferred = kb_path.with_name(f".deleted_{kb_name}_{int(time.time())}") + kb_path.rename(deferred) + except OSError as e: + logger.warning("Deferred rename failed for %s: %s", kb_name, e) + else: + logger.info("Renamed %s for deferred cleanup", kb_name) + return True + + return False + + +def _remove_sqlite_lock_files(kb_path: Path) -> None: + """Remove SQLite auxiliary files (WAL, SHM, journal) that hold locks.""" + for pattern in ["*.sqlite3-wal", "*.sqlite3-shm", "*.sqlite3-journal"]: + for lock_file in kb_path.glob(pattern): + try: + lock_file.unlink() + except OSError as e: + logger.debug("Could not remove lock file %s: %s", lock_file.name, e) + + +def _truncate_sqlite_files(kb_path: Path) -> None: + """Truncate SQLite database files to release locks.""" + for sqlite_file in kb_path.glob("*.sqlite3"): + try: + with sqlite_file.open("r+b") as f: + f.truncate(0) + except OSError as e: + logger.debug("Could not truncate %s: %s", sqlite_file.name, e) class KBAnalysisHelper: @@ -154,11 +242,15 @@ def get_metadata(kb_path: Path, *, fast: bool = False) -> dict: @staticmethod def update_text_metrics(kb_path: Path, metadata: dict, chroma: Chroma | None = None) -> None: """Update text metrics (chunks, words, characters) for a knowledge base.""" + created_locally = chroma is None + client = None try: - if chroma is None: + if created_locally: client = KBStorageHelper.get_fresh_chroma_client(kb_path) chroma = Chroma(client=client, collection_name=kb_path.name) + if chroma is None: + return collection = chroma._collection # noqa: SLF001 metadata["chunks"] = collection.count() @@ -190,6 +282,11 @@ def update_text_metrics(kb_path: Path, metadata: dict, chroma: Chroma | None = N ) except (OSError, ValueError, TypeError, json.JSONDecodeError, chromadb.errors.ChromaError) as e: logger.debug(f"Metrics update failed for {kb_path.name}: {e}") + finally: + if created_locally: + client = None + chroma = None + KBStorageHelper.release_chroma_resources(kb_path) @staticmethod def _detect_embedding_provider(kb_path: Path) -> str: @@ -417,8 +514,9 @@ async def perform_ingestion( await KBIngestionHelper.cleanup_chroma_chunks_by_job(task_job_id, kb_path, kb_name) raise finally: + client = None chroma = None - gc.collect() + KBStorageHelper.release_chroma_resources(kb_path) @staticmethod async def cleanup_chroma_chunks_by_job( @@ -438,8 +536,9 @@ async def cleanup_chroma_chunks_by_job( except (OSError, ValueError, TypeError, chromadb.errors.ChromaError) as cleanup_error: await logger.aerror(f"Failed to clean up chunks for job {job_id}: {cleanup_error}") finally: + client = None chroma = None - gc.collect() + KBStorageHelper.release_chroma_resources(kb_path) @staticmethod async def _is_job_cancelled(job_service: JobService, job_id: uuid.UUID) -> bool: diff --git a/src/backend/base/langflow/api/v1/knowledge_bases.py b/src/backend/base/langflow/api/v1/knowledge_bases.py index ec07311ee937..6f11838159a7 100644 --- a/src/backend/base/langflow/api/v1/knowledge_bases.py +++ b/src/backend/base/langflow/api/v1/knowledge_bases.py @@ -1,7 +1,5 @@ import asyncio -import gc import json -import shutil import uuid from datetime import datetime, timezone from http import HTTPStatus @@ -78,11 +76,11 @@ async def create_knowledge_base( try: client = KBStorageHelper.get_fresh_chroma_client(kb_path) client.create_collection(name=kb_name) - # Explicitly delete reference to help release handle - client = None - gc.collect() except (OSError, ValueError, chromadb.errors.ChromaError) as e: logger.warning("Initial Chroma setup for %s failed: %s", kb_name, e) + finally: + client = None + KBStorageHelper.release_chroma_resources(kb_path) # Serialize column_config for persistence column_config_dicts = None @@ -130,7 +128,7 @@ async def create_knowledge_base( except Exception as e: # Clean up if something went wrong if kb_path.exists(): - shutil.rmtree(kb_path) + KBStorageHelper.delete_storage(kb_path, kb_name) await logger.aerror("Error creating knowledge base: %s", e) raise HTTPException(status_code=500, detail="Internal error creating knowledge base") from e @@ -593,8 +591,9 @@ async def get_knowledge_base_chunks( await logger.aerror("Error getting chunks for '%s': %s", kb_name, e) raise HTTPException(status_code=500, detail="Error getting chunks.") from e finally: + client = None chroma = None - gc.collect() + KBStorageHelper.release_chroma_resources(kb_path) @router.delete("/{kb_name}", status_code=HTTPStatus.OK) @@ -603,11 +602,11 @@ async def delete_knowledge_base(kb_name: str, current_user: CurrentActiveUser) - try: kb_path = _resolve_kb_path(kb_name, current_user) - # Explicitly teardown KB storage to flush Chroma handles before directory deletion - KBStorageHelper.teardown_storage(kb_path, kb_name) - - # Delete the entire knowledge base directory - shutil.rmtree(kb_path) + if not KBStorageHelper.delete_storage(kb_path, kb_name): + raise HTTPException( + status_code=500, + detail=f"Failed to delete knowledge base '{kb_name}'. The database may be in use.", + ) except HTTPException: raise @@ -636,12 +635,8 @@ async def delete_knowledge_bases_bulk(request: BulkDeleteRequest, current_user: continue try: - # Explicitly teardown KB storage to flush Chroma handles before directory deletion - KBStorageHelper.teardown_storage(kb_path, kb_name) - - # Delete the entire knowledge base directory - shutil.rmtree(kb_path) - deleted_count += 1 + if KBStorageHelper.delete_storage(kb_path, kb_name): + deleted_count += 1 except (OSError, PermissionError) as e: await logger.aexception("Error deleting knowledge base '%s': %s", kb_name, e) # Continue with other deletions even if one fails diff --git a/src/backend/base/langflow/api/v1/mcp_projects.py b/src/backend/base/langflow/api/v1/mcp_projects.py index 8b95d6a2e3df..01092fe6d7fe 100644 --- a/src/backend/base/langflow/api/v1/mcp_projects.py +++ b/src/backend/base/langflow/api/v1/mcp_projects.py @@ -1013,6 +1013,13 @@ async def check_installed_mcp_servers( project_sse_url, list(config_data.get("mcpServers", {}).keys()), ) + except FileNotFoundError: + await logger.adebug( + "%s config file not found at %s (directory exists, app installed but not configured)", + client_name, + config_path, + ) + # available stays True, installed stays False — app is installed but not yet configured except json.JSONDecodeError: await logger.awarning("Failed to parse %s config JSON at: %s", client_name, config_path) # available is True but installed remains False due to parse error diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json index 01dfe6ef8f7a..58721fffff32 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json @@ -1422,7 +1422,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -2140,7 +2140,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -2858,7 +2858,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json index 610934193002..18956775fc6a 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Basic Prompting.json @@ -1037,7 +1037,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json index c623059f6206..a2b0e04d8685 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Blog Writer.json @@ -1563,7 +1563,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json index 98f4ae9a1df8..6b7d19194284 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Custom Component Generator.json @@ -2689,7 +2689,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json b/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json index 110ef0e7b241..f3e40adb72ef 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Document Q&A.json @@ -1053,7 +1053,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json b/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json index bf29e0c3ad8f..064c53e8d442 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Hybrid Search RAG.json @@ -1750,7 +1750,7 @@ "dependencies": [ { "name": "astrapy", - "version": "2.1.0" + "version": "2.2.1" }, { "name": "langchain_core", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json index 8f39ad1d94c0..4a16957fa0dc 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Image Sentiment Analysis.json @@ -1715,7 +1715,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json b/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json index daf937d64fb3..d612b25a7365 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Instagram Copywriter.json @@ -2071,7 +2071,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -2229,7 +2229,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", @@ -2783,7 +2783,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -3161,7 +3161,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json b/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json index 1a5747b35668..a874637f4eec 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Invoice Summarizer.json @@ -1179,7 +1179,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1337,7 +1337,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json b/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json index 20fd7287b0c7..501ce6b553e0 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Knowledge Retrieval.json @@ -541,7 +541,7 @@ "dependencies": [ { "name": "chromadb", - "version": "1.5.4" + "version": "1.5.5" }, { "name": "cryptography", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json b/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json index b435878fa574..171ea0a916ac 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Market Research.json @@ -1191,7 +1191,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1349,7 +1349,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json b/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json index dfcb0455dfb3..fc922e3296bf 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Meeting Summary.json @@ -3187,7 +3187,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -3564,7 +3564,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json index 31f088f46be3..bee96fd61f54 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Memory Chatbot.json @@ -1432,7 +1432,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json b/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json index b3d1a068bd08..b810bad75fec 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/News Aggregator.json @@ -1173,7 +1173,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1331,7 +1331,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json b/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json index eb87e6a5d447..a8b09759bcb5 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Nvidia Remix.json @@ -801,7 +801,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -960,7 +960,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", @@ -1746,7 +1746,7 @@ "legacy": false, "lf_version": "1.4.2", "metadata": { - "code_hash": "c5ce0982da48", + "code_hash": "b5cf1a06bba8", "dependencies": { "dependencies": [ { @@ -1890,7 +1890,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom lfx.base.embeddings.embeddings_class import EmbeddingsWithModels\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.unified_models import (\n get_api_key_for_provider,\n get_embedding_class,\n get_embedding_model_options,\n get_unified_models_detailed,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n ModelInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"embedding_model_options\",\n get_options_func=get_embedding_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Show/hide provider-specific fields based on selected model\n if field_name == \"model\" and isinstance(field_value, list) and len(field_value) > 0:\n selected_model = field_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Show/hide watsonx fields\n is_watsonx = provider == \"IBM WatsonX\"\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = is_watsonx\n build_config[\"project_id\"][\"show\"] = is_watsonx\n build_config[\"truncate_input_tokens\"][\"show\"] = is_watsonx\n build_config[\"input_text\"][\"show\"] = is_watsonx\n if is_watsonx:\n build_config[\"base_url_ibm_watsonx\"][\"required\"] = True\n build_config[\"project_id\"][\"required\"] = True\n\n return build_config\n\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Embedding Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n model_type=\"embedding\",\n input_types=[\"Embeddings\"], # Override default to accept Embeddings instead of LanguageModel\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n # Watson-specific inputs\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 show=False,\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n advanced=True,\n value=1000,\n ),\n FloatInput(\n name=\"request_timeout\",\n display_name=\"Request Timeout\",\n advanced=True,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n advanced=True,\n value=3,\n ),\n BoolInput(\n name=\"show_progress_bar\",\n display_name=\"Show Progress Bar\",\n advanced=True,\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n IntInput(\n name=\"truncate_input_tokens\",\n display_name=\"Truncate Input Tokens\",\n advanced=True,\n value=200,\n show=False,\n ),\n BoolInput(\n name=\"input_text\",\n display_name=\"Include the original text in the output\",\n value=True,\n advanced=True,\n show=False,\n ),\n ]\n\n def build_embeddings(self) -> Embeddings:\n \"\"\"Build and return an embeddings instance based on the selected model.\n\n Returns an EmbeddingsWithModels wrapper that contains:\n - The primary embedding instance (for the selected model)\n - available_models dict mapping all available model names to their instances\n \"\"\"\n # If an Embeddings object is directly connected, return it\n try:\n from langchain_core.embeddings import Embeddings as BaseEmbeddings\n\n if isinstance(self.model, BaseEmbeddings):\n return self.model\n except ImportError:\n pass\n\n # Safely extract model configuration\n if not self.model or not isinstance(self.model, list):\n msg = \"Model must be a non-empty list\"\n raise ValueError(msg)\n\n model = self.model[0]\n model_name = model.get(\"name\")\n provider = model.get(\"provider\")\n metadata = model.get(\"metadata\", {})\n\n # Get API key from user input or global variables\n api_key = get_api_key_for_provider(self.user_id, provider, self.api_key)\n\n # Validate required fields (Ollama doesn't require API key)\n if not api_key and provider != \"Ollama\":\n msg = (\n f\"{provider} API key is required. \"\n f\"Please provide it in the component or configure it globally as \"\n f\"{provider.upper().replace(' ', '_')}_API_KEY.\"\n )\n raise ValueError(msg)\n\n if not model_name:\n msg = \"Model name is required\"\n raise ValueError(msg)\n\n # Get embedding class\n embedding_class_name = metadata.get(\"embedding_class\")\n if not embedding_class_name:\n msg = f\"No embedding class defined in metadata for {model_name}\"\n raise ValueError(msg)\n\n embedding_class = get_embedding_class(embedding_class_name)\n\n # Build kwargs using parameter mapping for primary instance\n kwargs = self._build_kwargs(model, metadata)\n primary_instance = embedding_class(**kwargs)\n\n # Get all available embedding models for this provider\n available_models_dict = self._build_available_models(\n provider=provider,\n embedding_class=embedding_class,\n metadata=metadata,\n api_key=api_key,\n )\n\n # Wrap with EmbeddingsWithModels to provide available_models metadata\n return EmbeddingsWithModels(\n embeddings=primary_instance,\n available_models=available_models_dict,\n )\n\n def _build_available_models(\n self,\n provider: str,\n embedding_class: type,\n metadata: dict[str, Any],\n api_key: str | None,\n ) -> dict[str, Embeddings]:\n \"\"\"Build a dictionary of all available embedding model instances for the provider.\n\n Args:\n provider: The provider name (e.g., \"OpenAI\", \"Ollama\")\n embedding_class: The embedding class to instantiate\n metadata: Metadata containing param_mapping\n api_key: The API key for the provider\n\n Returns:\n Dict mapping model names to their embedding instances\n \"\"\"\n available_models_dict: dict[str, Embeddings] = {}\n\n # Get all embedding models for this provider from unified models\n all_embedding_models = get_unified_models_detailed(\n providers=[provider],\n model_type=\"embeddings\",\n include_deprecated=False,\n include_unsupported=False,\n )\n\n if not all_embedding_models:\n return available_models_dict\n\n # Extract models from the provider data\n for provider_data in all_embedding_models:\n if provider_data.get(\"provider\") != provider:\n continue\n\n for model_data in provider_data.get(\"models\", []):\n model_name = model_data.get(\"model_name\")\n if not model_name:\n continue\n\n # Create a model dict compatible with _build_kwargs\n model_dict = {\n \"name\": model_name,\n \"provider\": provider,\n \"metadata\": metadata, # Reuse the same metadata/param_mapping\n }\n\n try:\n # Build kwargs for this model\n model_kwargs = self._build_kwargs_for_model(model_dict, metadata, api_key)\n # Create the embedding instance\n available_models_dict[model_name] = embedding_class(**model_kwargs)\n except Exception: # noqa: BLE001\n # Skip models that fail to instantiate\n # This handles cases where specific models have incompatible parameters\n logger.debug(\"Failed to instantiate embedding model %s: skipping\", model_name, exc_info=True)\n continue\n\n return available_models_dict\n\n def _build_kwargs_for_model(\n self,\n model: dict[str, Any],\n metadata: dict[str, Any],\n api_key: str | None,\n ) -> dict[str, Any]:\n \"\"\"Build kwargs dictionary for a specific model using parameter mapping.\n\n This is similar to _build_kwargs but uses the provided api_key directly\n instead of looking it up again.\n\n Args:\n model: Model dict with name and provider\n metadata: Metadata containing param_mapping\n api_key: The API key to use\n\n Returns:\n kwargs dict for embedding class instantiation\n \"\"\"\n param_mapping = metadata.get(\"param_mapping\", {})\n if not param_mapping:\n msg = \"Parameter mapping not found in metadata\"\n raise ValueError(msg)\n\n kwargs = {}\n provider = model.get(\"provider\")\n\n # Required parameters - handle both \"model\" and \"model_id\" (for watsonx)\n if \"model\" in param_mapping:\n kwargs[param_mapping[\"model\"]] = model.get(\"name\")\n elif \"model_id\" in param_mapping:\n kwargs[param_mapping[\"model_id\"]] = model.get(\"name\")\n\n # Add API key if mapped\n if \"api_key\" in param_mapping and api_key:\n kwargs[param_mapping[\"api_key\"]] = api_key\n\n # Optional parameters with their values\n optional_params = {\n \"api_base\": self.api_base if self.api_base else None,\n \"dimensions\": int(self.dimensions) if self.dimensions else None,\n \"chunk_size\": int(self.chunk_size) if self.chunk_size else None,\n \"request_timeout\": float(self.request_timeout) if self.request_timeout else None,\n \"max_retries\": int(self.max_retries) if self.max_retries else None,\n \"show_progress_bar\": self.show_progress_bar if hasattr(self, \"show_progress_bar\") else None,\n \"model_kwargs\": self.model_kwargs if self.model_kwargs else None,\n }\n\n # Watson-specific parameters\n if provider in {\"IBM WatsonX\", \"IBM watsonx.ai\"}:\n # Map base_url_ibm_watsonx to \"url\" parameter for watsonx\n if \"url\" in param_mapping:\n url_value = (\n self.base_url_ibm_watsonx\n if hasattr(self, \"base_url_ibm_watsonx\") and self.base_url_ibm_watsonx\n else \"https://us-south.ml.cloud.ibm.com\"\n )\n kwargs[param_mapping[\"url\"]] = url_value\n # Map project_id for watsonx\n if hasattr(self, \"project_id\") and self.project_id and \"project_id\" in param_mapping:\n kwargs[param_mapping[\"project_id\"]] = self.project_id\n\n # Ollama-specific parameters\n if provider == \"Ollama\" and \"base_url\" in param_mapping:\n # Map api_base to \"base_url\" parameter for Ollama\n base_url_value = self.api_base if hasattr(self, \"api_base\") and self.api_base else \"http://localhost:11434\"\n kwargs[param_mapping[\"base_url\"]] = base_url_value\n\n # Add optional parameters if they have values and are mapped\n for param_name, param_value in optional_params.items():\n if param_value is not None and param_name in param_mapping:\n # Special handling for request_timeout with Google provider\n if param_name == \"request_timeout\":\n if provider == \"Google Generative AI\" and isinstance(param_value, (int, float)):\n kwargs[param_mapping[param_name]] = {\"timeout\": param_value}\n else:\n kwargs[param_mapping[param_name]] = param_value\n else:\n kwargs[param_mapping[param_name]] = param_value\n\n return kwargs\n\n def _build_kwargs(self, model: dict[str, Any], metadata: dict[str, Any]) -> dict[str, Any]:\n \"\"\"Build kwargs dictionary using parameter mapping.\"\"\"\n param_mapping = metadata.get(\"param_mapping\", {})\n if not param_mapping:\n msg = \"Parameter mapping not found in metadata\"\n raise ValueError(msg)\n\n kwargs = {}\n\n # Required parameters - handle both \"model\" and \"model_id\" (for watsonx)\n if \"model\" in param_mapping:\n kwargs[param_mapping[\"model\"]] = model.get(\"name\")\n elif \"model_id\" in param_mapping:\n kwargs[param_mapping[\"model_id\"]] = model.get(\"name\")\n if \"api_key\" in param_mapping:\n kwargs[param_mapping[\"api_key\"]] = get_api_key_for_provider(\n self.user_id,\n model.get(\"provider\"),\n self.api_key,\n )\n\n # Optional parameters with their values\n provider = model.get(\"provider\")\n optional_params = {\n \"api_base\": self.api_base if self.api_base else None,\n \"dimensions\": int(self.dimensions) if self.dimensions else None,\n \"chunk_size\": int(self.chunk_size) if self.chunk_size else None,\n \"request_timeout\": float(self.request_timeout) if self.request_timeout else None,\n \"max_retries\": int(self.max_retries) if self.max_retries else None,\n \"show_progress_bar\": self.show_progress_bar if hasattr(self, \"show_progress_bar\") else None,\n \"model_kwargs\": self.model_kwargs if self.model_kwargs else None,\n }\n\n # Watson-specific parameters\n if provider in {\"IBM WatsonX\", \"IBM watsonx.ai\"}:\n # Map base_url_ibm_watsonx to \"url\" parameter for watsonx\n if \"url\" in param_mapping:\n url_value = (\n self.base_url_ibm_watsonx\n if hasattr(self, \"base_url_ibm_watsonx\") and self.base_url_ibm_watsonx\n else \"https://us-south.ml.cloud.ibm.com\"\n )\n kwargs[param_mapping[\"url\"]] = url_value\n # Map project_id for watsonx\n if hasattr(self, \"project_id\") and self.project_id and \"project_id\" in param_mapping:\n kwargs[param_mapping[\"project_id\"]] = self.project_id\n\n # Ollama-specific parameters\n if provider == \"Ollama\" and \"base_url\" in param_mapping:\n # Map api_base to \"base_url\" parameter for Ollama\n base_url_value = self.api_base if hasattr(self, \"api_base\") and self.api_base else \"http://localhost:11434\"\n kwargs[param_mapping[\"base_url\"]] = base_url_value\n\n # Add optional parameters if they have values and are mapped\n for param_name, param_value in optional_params.items():\n if param_value is not None and param_name in param_mapping:\n # Special handling for request_timeout with Google provider\n if param_name == \"request_timeout\":\n if provider == \"Google Generative AI\" and isinstance(param_value, (int, float)):\n kwargs[param_mapping[param_name]] = {\"timeout\": param_value}\n else:\n kwargs[param_mapping[param_name]] = param_value\n else:\n kwargs[param_mapping[param_name]] = param_value\n\n return kwargs\n" + "value": "from typing import Any\n\nfrom lfx.base.embeddings.embeddings_class import EmbeddingsWithModels\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_api_key_for_provider,\n get_embedding_class,\n get_embedding_model_options,\n get_provider_for_model_name,\n get_unified_models_detailed,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n ModelInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"embedding_model_options\",\n get_options_func=get_embedding_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Embedding-specific WatsonX toggles not covered by provider metadata\n is_watsonx = provider == \"IBM WatsonX\"\n if \"truncate_input_tokens\" in build_config:\n build_config[\"truncate_input_tokens\"][\"show\"] = is_watsonx\n if \"input_text\" in build_config:\n build_config[\"input_text\"][\"show\"] = is_watsonx\n\n return build_config\n\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Embedding Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n model_type=\"embedding\",\n input_types=[\"Embeddings\"], # Override default to accept Embeddings instead of LanguageModel\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n # Watson-specific inputs\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 show=False,\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n advanced=True,\n value=1000,\n ),\n FloatInput(\n name=\"request_timeout\",\n display_name=\"Request Timeout\",\n advanced=True,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n advanced=True,\n value=3,\n ),\n BoolInput(\n name=\"show_progress_bar\",\n display_name=\"Show Progress Bar\",\n advanced=True,\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n IntInput(\n name=\"truncate_input_tokens\",\n display_name=\"Truncate Input Tokens\",\n advanced=True,\n value=200,\n show=False,\n ),\n BoolInput(\n name=\"input_text\",\n display_name=\"Include the original text in the output\",\n value=True,\n advanced=True,\n show=False,\n ),\n ]\n\n def build_embeddings(self) -> Embeddings:\n \"\"\"Build and return an embeddings instance based on the selected model.\n\n Returns an EmbeddingsWithModels wrapper that contains:\n - The primary embedding instance (for the selected model)\n - available_models dict mapping all available model names to their instances\n \"\"\"\n # If an Embeddings object is directly connected, return it\n try:\n from langchain_core.embeddings import Embeddings as BaseEmbeddings\n\n if isinstance(self.model, BaseEmbeddings):\n return self.model\n except ImportError:\n pass\n\n # Safely extract model configuration\n if not self.model or not isinstance(self.model, list):\n msg = \"Model must be a non-empty list\"\n raise ValueError(msg)\n\n model = self.model[0]\n model_name = model.get(\"name\")\n provider = model.get(\"provider\")\n metadata = model.get(\"metadata\", {})\n\n # Get API key from user input or global variables\n api_key = get_api_key_for_provider(self.user_id, provider, self.api_key)\n\n # Validate required fields (Ollama doesn't require API key)\n if not api_key and provider != \"Ollama\":\n msg = (\n f\"{provider} API key is required. \"\n f\"Please provide it in the component or configure it globally as \"\n f\"{provider.upper().replace(' ', '_')}_API_KEY.\"\n )\n raise ValueError(msg)\n\n if not model_name:\n msg = \"Model name is required\"\n raise ValueError(msg)\n\n # Get embedding class\n embedding_class_name = metadata.get(\"embedding_class\")\n if not embedding_class_name:\n msg = f\"No embedding class defined in metadata for {model_name}\"\n raise ValueError(msg)\n\n embedding_class = get_embedding_class(embedding_class_name)\n\n # Build kwargs using parameter mapping for primary instance\n kwargs = self._build_kwargs(model, metadata)\n primary_instance = embedding_class(**kwargs)\n\n # Get all available embedding models for this provider\n available_models_dict = self._build_available_models(\n provider=provider,\n embedding_class=embedding_class,\n metadata=metadata,\n api_key=api_key,\n )\n\n # Wrap with EmbeddingsWithModels to provide available_models metadata\n return EmbeddingsWithModels(\n embeddings=primary_instance,\n available_models=available_models_dict,\n )\n\n def _build_available_models(\n self,\n provider: str,\n embedding_class: type,\n metadata: dict[str, Any],\n api_key: str | None,\n ) -> dict[str, Embeddings]:\n \"\"\"Build a dictionary of all available embedding model instances for the provider.\n\n Args:\n provider: The provider name (e.g., \"OpenAI\", \"Ollama\")\n embedding_class: The embedding class to instantiate\n metadata: Metadata containing param_mapping\n api_key: The API key for the provider\n\n Returns:\n Dict mapping model names to their embedding instances\n \"\"\"\n available_models_dict: dict[str, Embeddings] = {}\n\n # Get all embedding models for this provider from unified models\n all_embedding_models = get_unified_models_detailed(\n providers=[provider],\n model_type=\"embeddings\",\n include_deprecated=False,\n include_unsupported=False,\n )\n\n if not all_embedding_models:\n return available_models_dict\n\n # Extract models from the provider data\n for provider_data in all_embedding_models:\n if provider_data.get(\"provider\") != provider:\n continue\n\n for model_data in provider_data.get(\"models\", []):\n model_name = model_data.get(\"model_name\")\n if not model_name:\n continue\n\n # Create a model dict compatible with _build_kwargs\n model_dict = {\n \"name\": model_name,\n \"provider\": provider,\n \"metadata\": metadata, # Reuse the same metadata/param_mapping\n }\n\n try:\n # Build kwargs for this model\n model_kwargs = self._build_kwargs_for_model(model_dict, metadata, api_key)\n # Create the embedding instance\n available_models_dict[model_name] = embedding_class(**model_kwargs)\n except Exception: # noqa: BLE001\n # Skip models that fail to instantiate\n # This handles cases where specific models have incompatible parameters\n logger.debug(\"Failed to instantiate embedding model %s: skipping\", model_name, exc_info=True)\n continue\n\n return available_models_dict\n\n def _build_kwargs_for_model(\n self,\n model: dict[str, Any],\n metadata: dict[str, Any],\n api_key: str | None,\n ) -> dict[str, Any]:\n \"\"\"Build kwargs dictionary for a specific model using parameter mapping.\n\n This is similar to _build_kwargs but uses the provided api_key directly\n instead of looking it up again.\n\n Args:\n model: Model dict with name and provider\n metadata: Metadata containing param_mapping\n api_key: The API key to use\n\n Returns:\n kwargs dict for embedding class instantiation\n \"\"\"\n param_mapping = metadata.get(\"param_mapping\", {})\n if not param_mapping:\n msg = \"Parameter mapping not found in metadata\"\n raise ValueError(msg)\n\n kwargs = {}\n provider = model.get(\"provider\")\n\n # Required parameters - handle both \"model\" and \"model_id\" (for watsonx)\n if \"model\" in param_mapping:\n kwargs[param_mapping[\"model\"]] = model.get(\"name\")\n elif \"model_id\" in param_mapping:\n kwargs[param_mapping[\"model_id\"]] = model.get(\"name\")\n\n # Add API key if mapped\n if \"api_key\" in param_mapping and api_key:\n kwargs[param_mapping[\"api_key\"]] = api_key\n\n # Optional parameters with their values\n optional_params = {\n \"api_base\": self.api_base if self.api_base else None,\n \"dimensions\": int(self.dimensions) if self.dimensions else None,\n \"chunk_size\": int(self.chunk_size) if self.chunk_size else None,\n \"request_timeout\": float(self.request_timeout) if self.request_timeout else None,\n \"max_retries\": int(self.max_retries) if self.max_retries else None,\n \"show_progress_bar\": self.show_progress_bar if hasattr(self, \"show_progress_bar\") else None,\n \"model_kwargs\": self.model_kwargs if self.model_kwargs else None,\n }\n\n # Watson-specific parameters\n if provider in {\"IBM WatsonX\", \"IBM watsonx.ai\"}:\n # Map base_url_ibm_watsonx to \"url\" parameter for watsonx\n if \"url\" in param_mapping:\n url_value = (\n self.base_url_ibm_watsonx\n if hasattr(self, \"base_url_ibm_watsonx\") and self.base_url_ibm_watsonx\n else \"https://us-south.ml.cloud.ibm.com\"\n )\n kwargs[param_mapping[\"url\"]] = url_value\n # Map project_id for watsonx\n if hasattr(self, \"project_id\") and self.project_id and \"project_id\" in param_mapping:\n kwargs[param_mapping[\"project_id\"]] = self.project_id\n\n # Ollama-specific parameters\n if provider == \"Ollama\" and \"base_url\" in param_mapping:\n # Map api_base to \"base_url\" parameter for Ollama\n base_url_value = self.api_base if hasattr(self, \"api_base\") and self.api_base else \"http://localhost:11434\"\n kwargs[param_mapping[\"base_url\"]] = base_url_value\n\n # Add optional parameters if they have values and are mapped\n for param_name, param_value in optional_params.items():\n if param_value is not None and param_name in param_mapping:\n # Special handling for request_timeout with Google provider\n if param_name == \"request_timeout\":\n if provider == \"Google Generative AI\" and isinstance(param_value, (int, float)):\n kwargs[param_mapping[param_name]] = {\"timeout\": param_value}\n else:\n kwargs[param_mapping[param_name]] = param_value\n else:\n kwargs[param_mapping[param_name]] = param_value\n\n return kwargs\n\n def _build_kwargs(self, model: dict[str, Any], metadata: dict[str, Any]) -> dict[str, Any]:\n \"\"\"Build kwargs dictionary using parameter mapping.\"\"\"\n param_mapping = metadata.get(\"param_mapping\", {})\n if not param_mapping:\n msg = \"Parameter mapping not found in metadata\"\n raise ValueError(msg)\n\n kwargs = {}\n\n # Required parameters - handle both \"model\" and \"model_id\" (for watsonx)\n if \"model\" in param_mapping:\n kwargs[param_mapping[\"model\"]] = model.get(\"name\")\n elif \"model_id\" in param_mapping:\n kwargs[param_mapping[\"model_id\"]] = model.get(\"name\")\n if \"api_key\" in param_mapping:\n kwargs[param_mapping[\"api_key\"]] = get_api_key_for_provider(\n self.user_id,\n model.get(\"provider\"),\n self.api_key,\n )\n\n # Optional parameters with their values\n provider = model.get(\"provider\")\n optional_params = {\n \"api_base\": self.api_base if self.api_base else None,\n \"dimensions\": int(self.dimensions) if self.dimensions else None,\n \"chunk_size\": int(self.chunk_size) if self.chunk_size else None,\n \"request_timeout\": float(self.request_timeout) if self.request_timeout else None,\n \"max_retries\": int(self.max_retries) if self.max_retries else None,\n \"show_progress_bar\": self.show_progress_bar if hasattr(self, \"show_progress_bar\") else None,\n \"model_kwargs\": self.model_kwargs if self.model_kwargs else None,\n }\n\n # Watson-specific parameters\n if provider in {\"IBM WatsonX\", \"IBM watsonx.ai\"}:\n # Map base_url_ibm_watsonx to \"url\" parameter for watsonx\n if \"url\" in param_mapping:\n url_value = (\n self.base_url_ibm_watsonx\n if hasattr(self, \"base_url_ibm_watsonx\") and self.base_url_ibm_watsonx\n else \"https://us-south.ml.cloud.ibm.com\"\n )\n kwargs[param_mapping[\"url\"]] = url_value\n # Map project_id for watsonx\n if hasattr(self, \"project_id\") and self.project_id and \"project_id\" in param_mapping:\n kwargs[param_mapping[\"project_id\"]] = self.project_id\n\n # Ollama-specific parameters\n if provider == \"Ollama\" and \"base_url\" in param_mapping:\n # Map api_base to \"base_url\" parameter for Ollama\n base_url_value = self.api_base if hasattr(self, \"api_base\") and self.api_base else \"http://localhost:11434\"\n kwargs[param_mapping[\"base_url\"]] = base_url_value\n\n # Add optional parameters if they have values and are mapped\n for param_name, param_value in optional_params.items():\n if param_value is not None and param_name in param_mapping:\n # Special handling for request_timeout with Google provider\n if param_name == \"request_timeout\":\n if provider == \"Google Generative AI\" and isinstance(param_value, (int, float)):\n kwargs[param_mapping[param_name]] = {\"timeout\": param_value}\n else:\n kwargs[param_mapping[param_name]] = param_value\n else:\n kwargs[param_mapping[param_name]] = param_value\n\n return kwargs\n" }, "dimensions": { "_input_type": "IntInput", diff --git "a/src/backend/base/langflow/initial_setup/starter_projects/Pok\303\251dex Agent.json" "b/src/backend/base/langflow/initial_setup/starter_projects/Pok\303\251dex Agent.json" index 8f4d224776d2..52e65b8eb923 100644 --- "a/src/backend/base/langflow/initial_setup/starter_projects/Pok\303\251dex Agent.json" +++ "b/src/backend/base/langflow/initial_setup/starter_projects/Pok\303\251dex Agent.json" @@ -1238,7 +1238,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1396,7 +1396,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json index 4baa54810808..342bf6023c76 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Portfolio Website Code Generator.json @@ -1730,7 +1730,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json b/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json index abdefcd50fab..f4cfda7a189a 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Price Deal Finder.json @@ -1607,7 +1607,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1765,7 +1765,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json index b2ebd55eba92..ce6c0d54658c 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Research Agent.json @@ -2183,7 +2183,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -2561,7 +2561,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -2810,7 +2810,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -2968,7 +2968,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json b/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json index a0b9a76c01a9..cb91e8b5e197 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Research Translation Loop.json @@ -9,16 +9,12 @@ "dataType": "ChatInput", "id": "ChatInput-nr0Vp", "name": "message", - "output_types": [ - "Message" - ] + "output_types": ["Message"] }, "targetHandle": { "fieldName": "search_query", "id": "ArXivComponent-tAHR5", - "inputTypes": [ - "Message" - ], + "inputTypes": ["Message"], "type": "str" } }, @@ -36,19 +32,12 @@ "dataType": "LoopComponent", "id": "LoopComponent-GtPZT", "name": "item", - "output_types": [ - "JSON" - ] + "output_types": ["JSON"] }, "targetHandle": { "fieldName": "input_data", "id": "ParserComponent-pXAMb", - "inputTypes": [ - "DataFrame", - "Table", - "Data", - "JSON" - ], + "inputTypes": ["DataFrame", "Table", "Data", "JSON"], "type": "other" } }, @@ -65,16 +54,12 @@ "dataType": "ParserComponent", "id": "ParserComponent-pXAMb", "name": "parsed_text", - "output_types": [ - "Message" - ] + "output_types": ["Message"] }, "targetHandle": { "fieldName": "input_value", "id": "LanguageModelComponent-XKvly", - "inputTypes": [ - "Message" - ], + "inputTypes": ["Message"], "type": "str" } }, @@ -91,18 +76,13 @@ "dataType": "LanguageModelComponent", "id": "LanguageModelComponent-XKvly", "name": "text_output", - "output_types": [ - "Message" - ] + "output_types": ["Message"] }, "targetHandle": { "dataType": "LoopComponent", "id": "LoopComponent-GtPZT", "name": "item", - "output_types": [ - "Data", - "Message" - ] + "output_types": ["Data", "Message"] } }, "id": "xy-edge__LanguageModelComponent-XKvly{œdataTypeœ:œLanguageModelComponentœ,œidœ:œLanguageModelComponent-XKvlyœ,œnameœ:œtext_outputœ,œoutput_typesœ:[œMessageœ]}-LoopComponent-GtPZT{œdataTypeœ:œLoopComponentœ,œidœ:œLoopComponent-GtPZTœ,œnameœ:œitemœ,œoutput_typesœ:[œDataœ,œMessageœ]}", @@ -118,17 +98,12 @@ "dataType": "ArXivComponent", "id": "ArXivComponent-tAHR5", "name": "dataframe", - "output_types": [ - "Table" - ] + "output_types": ["Table"] }, "targetHandle": { "fieldName": "data", "id": "LoopComponent-GtPZT", - "inputTypes": [ - "DataFrame", - "Table" - ], + "inputTypes": ["DataFrame", "Table"], "type": "other" } }, @@ -145,20 +120,12 @@ "dataType": "LoopComponent", "id": "LoopComponent-GtPZT", "name": "done", - "output_types": [ - "Table" - ] + "output_types": ["Table"] }, "targetHandle": { "fieldName": "input_value", "id": "ChatOutput-YAED9", - "inputTypes": [ - "Data", - "JSON", - "DataFrame", - "Table", - "Message" - ], + "inputTypes": ["Data", "JSON", "DataFrame", "Table", "Message"], "type": "other" } }, @@ -174,10 +141,7 @@ "data": { "id": "ArXivComponent-tAHR5", "node": { - "base_classes": [ - "DataFrame", - "Table" - ], + "base_classes": ["DataFrame", "Table"], "beta": false, "conditional_paths": [], "custom_fields": {}, @@ -185,11 +149,7 @@ "display_name": "arXiv", "documentation": "", "edited": false, - "field_order": [ - "search_query", - "search_type", - "max_results" - ], + "field_order": ["search_query", "search_type", "max_results"], "frozen": false, "icon": "arXiv", "legacy": false, @@ -223,9 +183,7 @@ "name": "dataframe", "selected": "Table", "tool_mode": true, - "types": [ - "Table" - ], + "types": ["Table"], "value": "__UNDEFINED__" } ], @@ -275,9 +233,7 @@ "display_name": "Search Query", "dynamic": false, "info": "The search query for arXiv papers (e.g., 'quantum computing')", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -301,13 +257,7 @@ "dynamic": false, "info": "The field to search in", "name": "search_type", - "options": [ - "all", - "title", - "abstract", - "author", - "cat" - ], + "options": ["all", "title", "abstract", "author", "cat"], "options_metadata": [], "placeholder": "", "required": false, @@ -342,9 +292,7 @@ "data": { "id": "ChatOutput-YAED9", "node": { - "base_classes": [ - "Message" - ], + "base_classes": ["Message"], "beta": false, "conditional_paths": [], "custom_fields": {}, @@ -399,9 +347,7 @@ "name": "message", "selected": "Message", "tool_mode": true, - "types": [ - "Message" - ], + "types": ["Message"], "value": "__UNDEFINED__" } ], @@ -450,9 +396,7 @@ "display_name": "Context ID", "dynamic": false, "info": "The context ID of the chat. Adds an extra layer to the local memory.", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -473,9 +417,7 @@ "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" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -523,10 +465,7 @@ "dynamic": false, "info": "Type of sender.", "name": "sender", - "options": [ - "Machine", - "User" - ], + "options": ["Machine", "User"], "options_metadata": [], "placeholder": "", "required": false, @@ -544,9 +483,7 @@ "display_name": "Sender Name", "dynamic": false, "info": "Name of the sender.", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -567,9 +504,7 @@ "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" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -625,9 +560,7 @@ "data": { "id": "ChatInput-nr0Vp", "node": { - "base_classes": [ - "Message" - ], + "base_classes": ["Message"], "beta": false, "conditional_paths": [], "custom_fields": {}, @@ -673,9 +606,7 @@ "name": "message", "selected": "Message", "tool_mode": true, - "types": [ - "Message" - ], + "types": ["Message"], "value": "__UNDEFINED__" } ], @@ -706,9 +637,7 @@ "display_name": "Context ID", "dynamic": false, "info": "The context ID of the chat. Adds an extra layer to the local memory.", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -799,10 +728,7 @@ "dynamic": false, "info": "Type of sender.", "name": "sender", - "options": [ - "Machine", - "User" - ], + "options": ["Machine", "User"], "options_metadata": [], "placeholder": "", "required": false, @@ -820,9 +746,7 @@ "display_name": "Sender Name", "dynamic": false, "info": "Name of the sender.", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -843,9 +767,7 @@ "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" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -928,10 +850,7 @@ "data": { "id": "LanguageModelComponent-XKvly", "node": { - "base_classes": [ - "LanguageModel", - "Message" - ], + "base_classes": ["LanguageModel", "Message"], "beta": false, "conditional_paths": [], "custom_fields": {}, @@ -991,9 +910,7 @@ "required_inputs": null, "selected": "Message", "tool_mode": true, - "types": [ - "Message" - ], + "types": ["Message"], "value": "__UNDEFINED__" }, { @@ -1009,9 +926,7 @@ "required_inputs": null, "selected": "LanguageModel", "tool_mode": true, - "types": [ - "LanguageModel" - ], + "types": ["LanguageModel"], "value": "__UNDEFINED__" } ], @@ -1093,7 +1008,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -1101,9 +1016,7 @@ "display_name": "Input", "dynamic": false, "info": "The input text to send to the model", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -1188,9 +1101,7 @@ "context_length": 128000, "model_class": "ChatOpenAI", "model_name_param": "model", - "reasoning_models": [ - "gpt-5.1" - ] + "reasoning_models": ["gpt-5.1"] }, "name": "gpt-5.1", "provider": "OpenAI" @@ -1215,9 +1126,7 @@ "context_length": 128000, "model_class": "ChatOpenAI", "model_name_param": "model", - "reasoning_models": [ - "o1" - ] + "reasoning_models": ["o1"] }, "name": "o1", "provider": "OpenAI" @@ -1289,9 +1198,7 @@ "context_length": 128000, "model_class": "ChatOpenAI", "model_name_param": "model", - "reasoning_models": [ - "gpt-5.1" - ] + "reasoning_models": ["gpt-5.1"] }, "name": "gpt-5.1", "provider": "OpenAI" @@ -1304,9 +1211,7 @@ "display_name": "Ollama API URL", "dynamic": false, "info": "Endpoint of the Ollama API (Ollama only). Defaults to http://localhost:11434", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -1373,9 +1278,7 @@ "display_name": "System Message", "dynamic": false, "info": "A system message that helps set the behavior of the assistant", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -1447,12 +1350,7 @@ "data": { "id": "LoopComponent-GtPZT", "node": { - "base_classes": [ - "Data", - "JSON", - "DataFrame", - "Table" - ], + "base_classes": ["Data", "JSON", "DataFrame", "Table"], "beta": false, "conditional_paths": [], "custom_fields": {}, @@ -1460,9 +1358,7 @@ "display_name": "Loop", "documentation": "https://docs.langflow.org/loop", "edited": false, - "field_order": [ - "data" - ], + "field_order": ["data"], "frozen": false, "icon": "infinity", "legacy": false, @@ -1487,16 +1383,12 @@ "cache": true, "display_name": "Item", "group_outputs": true, - "loop_types": [ - "Message" - ], + "loop_types": ["Message"], "method": "item_output", "name": "item", "selected": "JSON", "tool_mode": true, - "types": [ - "JSON" - ], + "types": ["JSON"], "value": "__UNDEFINED__" }, { @@ -1508,9 +1400,7 @@ "name": "done", "selected": "Table", "tool_mode": true, - "types": [ - "Table" - ], + "types": ["Table"], "value": "__UNDEFINED__" } ], @@ -1541,10 +1431,7 @@ "display_name": "Inputs", "dynamic": false, "info": "The initial DataFrame to iterate over.", - "input_types": [ - "DataFrame", - "Table" - ], + "input_types": ["DataFrame", "Table"], "list": false, "list_add_label": "Add More", "name": "data", @@ -1581,9 +1468,7 @@ "data": { "id": "ParserComponent-pXAMb", "node": { - "base_classes": [ - "Message" - ], + "base_classes": ["Message"], "beta": false, "category": "models_and_agents", "conditional_paths": [], @@ -1592,12 +1477,7 @@ "display_name": "Parser", "documentation": "https://docs.langflow.org/parser", "edited": false, - "field_order": [ - "input_data", - "mode", - "pattern", - "sep" - ], + "field_order": ["input_data", "mode", "pattern", "sep"], "frozen": false, "icon": "braces", "legacy": false, @@ -1626,9 +1506,7 @@ "name": "parsed_text", "selected": "Message", "tool_mode": true, - "types": [ - "Message" - ], + "types": ["Message"], "value": "__UNDEFINED__" } ], @@ -1659,12 +1537,7 @@ "display_name": "JSON or Table", "dynamic": false, "info": "Accepts either a DataFrame or a Data object.", - "input_types": [ - "DataFrame", - "Table", - "Data", - "JSON" - ], + "input_types": ["DataFrame", "Table", "Data", "JSON"], "list": false, "list_add_label": "Add More", "name": "input_data", @@ -1685,10 +1558,7 @@ "dynamic": false, "info": "Convert into raw string instead of using a template.", "name": "mode", - "options": [ - "Parser", - "Stringify" - ], + "options": ["Parser", "Stringify"], "override_skip": false, "placeholder": "", "real_time_refresh": true, @@ -1709,9 +1579,7 @@ "display_name": "Template", "dynamic": true, "info": "Use variables within curly brackets to extract column values for DataFrames or key values for Data.For example: `Name: {Name}, Age: {Age}, Country: {Country}`", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -1735,9 +1603,7 @@ "display_name": "Separator", "dynamic": false, "info": "String used to separate rows/items.", - "input_types": [ - "Message" - ], + "input_types": ["Message"], "list": false, "list_add_label": "Add More", "load_from_db": false, @@ -1786,8 +1652,5 @@ "is_component": false, "last_tested_version": "1.7.0", "name": "Research Translation Loop", - "tags": [ - "chatbots", - "content-generation" - ] -} \ No newline at end of file + "tags": ["chatbots", "content-generation"] +} diff --git a/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json index 062abdd0db43..7170453c8996 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/SEO Keyword Generator.json @@ -1065,7 +1065,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json b/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json index 1bbc69283b18..2abe3ff40e8d 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/SaaS Pricing.json @@ -892,7 +892,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1050,7 +1050,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json index 392b5b531dad..ba774d9ae589 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Search agent.json @@ -939,7 +939,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1097,7 +1097,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json index 77e1b7da8d70..19e1cc445629 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Sequential Tasks Agents.json @@ -357,7 +357,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -515,7 +515,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", @@ -945,7 +945,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1103,7 +1103,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", @@ -2390,7 +2390,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -2548,7 +2548,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json index 802c923c4222..726e9cf13ddd 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Simple Agent.json @@ -938,7 +938,7 @@ "last_updated": "2026-02-12T20:48:13.965Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1097,7 +1097,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json index fe8ba2cb9873..1b53a2f63c2c 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Social Media Agent.json @@ -1288,7 +1288,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1446,7 +1446,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json index 6bef5d96338f..0749ccf455d7 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Text Sentiment Analysis.json @@ -1611,7 +1611,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -2305,7 +2305,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -2999,7 +2999,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json b/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json index 0a52789952f6..d3e30dd103e2 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Travel Planning Agents.json @@ -1666,7 +1666,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -1824,7 +1824,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", @@ -2249,7 +2249,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -2407,7 +2407,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", @@ -2832,7 +2832,7 @@ "last_updated": "2025-12-11T21:41:48.407Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -2990,7 +2990,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json b/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json index 76905e095025..ae29293333ac 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Twitter Thread Generator.json @@ -2150,7 +2150,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json b/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json index 49889a5a2fd9..200d27d17cfc 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Vector Store RAG.json @@ -2356,7 +2356,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -2851,7 +2851,7 @@ "dependencies": [ { "name": "astrapy", - "version": "2.1.0" + "version": "2.2.1" }, { "name": "langchain_core", @@ -3932,7 +3932,7 @@ "dependencies": [ { "name": "astrapy", - "version": "2.1.0" + "version": "2.2.1" }, { "name": "langchain_core", diff --git a/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json b/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json index dab28c76485a..da775361255c 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Youtube Analysis.json @@ -500,7 +500,7 @@ "last_updated": "2025-12-22T21:08:01.050Z", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -658,7 +658,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", diff --git a/src/backend/base/langflow/processing/process.py b/src/backend/base/langflow/processing/process.py index 74d32fcdb94a..2372cd38ba7b 100644 --- a/src/backend/base/langflow/processing/process.py +++ b/src/backend/base/langflow/processing/process.py @@ -171,9 +171,15 @@ def apply_tweaks(node: dict[str, Any], node_tweaks: dict[str, Any]) -> None: for k, v in tweak_value.items(): k_ = "file_path" if field_type == "file" else k template_data[tweak_name][k_] = v + # If the user didn't explicitly set load_from_db in the dict, + # we default to False for the override. + if "load_from_db" not in tweak_value and "load_from_db" in template_data[tweak_name]: + template_data[tweak_name]["load_from_db"] = False else: key = "file_path" if field_type == "file" else "value" template_data[tweak_name][key] = tweak_value + if "load_from_db" in template_data[tweak_name]: + template_data[tweak_name]["load_from_db"] = False def apply_tweaks_on_vertex(vertex: Vertex, node_tweaks: dict[str, Any]) -> None: @@ -181,6 +187,17 @@ def apply_tweaks_on_vertex(vertex: Vertex, node_tweaks: dict[str, Any]) -> None: if tweak_name and tweak_value and tweak_name in vertex.params: vertex.params[tweak_name] = tweak_value + # Determine if we should load from DB + tweak_load_from_db = False + if isinstance(tweak_value, dict): + tweak_load_from_db = tweak_value.get("load_from_db", False) + + if tweak_load_from_db: + if tweak_name not in vertex.load_from_db_fields: + vertex.load_from_db_fields.append(tweak_name) + elif tweak_name in vertex.load_from_db_fields: + vertex.load_from_db_fields.remove(tweak_name) + def process_tweaks( graph_data: dict[str, Any], tweaks: Tweaks | dict[str, dict[str, Any]], *, stream: bool = False diff --git a/src/backend/base/langflow/utils/kb_constants.py b/src/backend/base/langflow/utils/kb_constants.py index 1eaee8cb892a..b05b876b21bd 100644 --- a/src/backend/base/langflow/utils/kb_constants.py +++ b/src/backend/base/langflow/utils/kb_constants.py @@ -3,3 +3,7 @@ EXPONENTIAL_BACKOFF_MULTIPLIER = 2 MIN_KB_NAME_LENGTH = 3 CHUNK_PREVIEW_MULTIPLIER = 3 + +# KB deletion retry constants +MAX_DELETE_RETRIES = 5 +DELETE_BACKOFF_SECONDS = 0.5 diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index b5943b6a6bcb..fac8fc792a4d 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langflow-base" -version = "0.8.0" +version = "0.8.1" description = "A Python package with a built-in web application" requires-python = ">=3.10,<3.14" license = "MIT" @@ -17,8 +17,8 @@ maintainers = [ ] dependencies = [ - "lfx~=0.3.0", - "fastapi>=0.115.2,<1.0.0", + "lfx~=0.3.1", + "fastapi>=0.135.0,<1.0.0", "httpx[http2]>=0.27,<1.0.0", "aiofile>=3.9.0,<4.0.0", "uvicorn>=0.30.0,<1.0.0", diff --git a/src/backend/tests/unit/api/v1/test_mcp_projects.py b/src/backend/tests/unit/api/v1/test_mcp_projects.py index 9f2b42d8da01..76d78fec0cd9 100644 --- a/src/backend/tests/unit/api/v1/test_mcp_projects.py +++ b/src/backend/tests/unit/api/v1/test_mcp_projects.py @@ -977,3 +977,215 @@ async def test_mcp_longterm_token_fails_without_superuser(): async with session_scope() as session: with pytest.raises(HTTPException, match="Auto login required to create a long-term token"): await create_user_longterm_token(session) + + +def _prepare_installed_check_env(monkeypatch, tmp_path): + """Set up environment for check_installed_mcp_servers tests. + + Creates per-client config directories under tmp_path so that + ``get_config_path`` returns paths whose *parent* directories exist + but whose config *files* may or may not exist. + """ + client_paths = { + "cursor": tmp_path / "cursor" / "mcp.json", + "windsurf": tmp_path / "windsurf" / "mcp_config.json", + "claude": tmp_path / "claude" / "claude_desktop_config.json", + } + # Create parent directories (simulating installed applications) + for path in client_paths.values(): + path.parent.mkdir(parents=True, exist_ok=True) + + async def fake_get_config_path(client_name): + return client_paths[client_name] + + monkeypatch.setattr("langflow.api.v1.mcp_projects.get_config_path", fake_get_config_path) + monkeypatch.setattr("langflow.api.v1.mcp_projects.should_use_mcp_composer", lambda project: False) # noqa: ARG005 + + async def fake_streamable(project_id): + return f"https://langflow.local/api/v1/mcp/project/{project_id}/streamable" + + async def fake_sse(project_id): + return f"https://langflow.local/api/v1/mcp/project/{project_id}/sse" + + monkeypatch.setattr("langflow.api.v1.mcp_projects.get_project_streamable_http_url", fake_streamable) + monkeypatch.setattr("langflow.api.v1.mcp_projects.get_project_sse_url", fake_sse) + + return client_paths + + +async def test_should_report_available_true_when_app_directory_exists_but_config_file_missing( + client: AsyncClient, + user_test_project, + logged_in_headers, + tmp_path, + monkeypatch, +): + """Bug: FileNotFoundError when config file is missing marks client as unavailable. + + GIVEN: App directories exist (e.g. ~/.cursor/) but config files don't exist yet + WHEN: GET /mcp/project/{id}/installed is called + THEN: Each client should have available=True (app is installed) and installed=False (not configured) + """ + _prepare_installed_check_env(monkeypatch, tmp_path) + + response = await client.get( + f"/api/v1/mcp/project/{user_test_project.id}/installed", + headers=logged_in_headers, + ) + + assert response.status_code == 200 + results = response.json() + + # All three clients should be reported + assert len(results) == 3 + + for entry in results: + assert entry["available"] is True, ( + f"{entry['name']} should be available (directory exists) even when config file is missing" + ) + assert entry["installed"] is False, f"{entry['name']} should not be installed (config file doesn't exist)" + + +async def test_should_report_installed_true_when_config_file_contains_matching_url( + client: AsyncClient, + user_test_project, + logged_in_headers, + tmp_path, + monkeypatch, +): + """Config with matching URL marks client as installed. + + GIVEN: Config files exist with a matching project URL in mcpServers args + WHEN: GET /mcp/project/{id}/installed is called + THEN: Each client should have available=True AND installed=True + """ + client_paths = _prepare_installed_check_env(monkeypatch, tmp_path) + + # Write config files with matching URLs for all clients + project_id = user_test_project.id + for path in client_paths.values(): + config = {"mcpServers": {"lf-test": {"args": [f"https://langflow.local/api/v1/mcp/project/{project_id}/sse"]}}} + path.write_text(json.dumps(config)) + + response = await client.get( + f"/api/v1/mcp/project/{project_id}/installed", + headers=logged_in_headers, + ) + + assert response.status_code == 200 + results = response.json() + + for entry in results: + assert entry["available"] is True, f"{entry['name']} should be available" + assert entry["installed"] is True, f"{entry['name']} should be installed (config has matching URL)" + + +async def test_should_report_installed_false_when_config_file_has_no_matching_url( + client: AsyncClient, + user_test_project, + logged_in_headers, + tmp_path, + monkeypatch, +): + """Config with non-matching URL reports installed=False. + + GIVEN: Config files exist but with a DIFFERENT project URL + WHEN: GET /mcp/project/{id}/installed is called + THEN: available=True (file exists) but installed=False (URL doesn't match) + """ + client_paths = _prepare_installed_check_env(monkeypatch, tmp_path) + + for path in client_paths.values(): + config = {"mcpServers": {"other-server": {"args": ["https://other-server.example.com/sse"]}}} + path.write_text(json.dumps(config)) + + response = await client.get( + f"/api/v1/mcp/project/{user_test_project.id}/installed", + headers=logged_in_headers, + ) + + assert response.status_code == 200 + results = response.json() + + for entry in results: + assert entry["available"] is True, f"{entry['name']} should be available" + assert entry["installed"] is False, f"{entry['name']} should not be installed (URL doesn't match)" + + +async def test_should_report_available_false_when_app_directory_does_not_exist( + client: AsyncClient, + user_test_project, + logged_in_headers, + tmp_path, + monkeypatch, +): + """Missing app directory reports available=False. + + GIVEN: App directories do NOT exist (applications not installed) + WHEN: GET /mcp/project/{id}/installed is called + THEN: available=False and installed=False for all clients + """ + # Point to paths whose parent directories do NOT exist + nonexistent_paths = { + "cursor": tmp_path / "nonexistent_cursor" / "mcp.json", + "windsurf": tmp_path / "nonexistent_windsurf" / "mcp_config.json", + "claude": tmp_path / "nonexistent_claude" / "claude_desktop_config.json", + } + + async def fake_get_config_path(client_name): + return nonexistent_paths[client_name] + + monkeypatch.setattr("langflow.api.v1.mcp_projects.get_config_path", fake_get_config_path) + monkeypatch.setattr("langflow.api.v1.mcp_projects.should_use_mcp_composer", lambda project: False) # noqa: ARG005 + + async def fake_streamable(project_id): + return f"https://langflow.local/api/v1/mcp/project/{project_id}/streamable" + + async def fake_sse(project_id): + return f"https://langflow.local/api/v1/mcp/project/{project_id}/sse" + + monkeypatch.setattr("langflow.api.v1.mcp_projects.get_project_streamable_http_url", fake_streamable) + monkeypatch.setattr("langflow.api.v1.mcp_projects.get_project_sse_url", fake_sse) + + response = await client.get( + f"/api/v1/mcp/project/{user_test_project.id}/installed", + headers=logged_in_headers, + ) + + assert response.status_code == 200 + results = response.json() + + for entry in results: + assert entry["available"] is False, f"{entry['name']} should not be available (directory doesn't exist)" + assert entry["installed"] is False + + +async def test_should_report_available_true_when_config_file_has_corrupt_json( + client: AsyncClient, + user_test_project, + logged_in_headers, + tmp_path, + monkeypatch, +): + """Corrupt JSON config reports available=True but installed=False. + + GIVEN: Config files exist but contain invalid/corrupt JSON + WHEN: GET /mcp/project/{id}/installed is called + THEN: available=True (directory exists) but installed=False (can't parse config) + """ + client_paths = _prepare_installed_check_env(monkeypatch, tmp_path) + + for path in client_paths.values(): + path.write_text("{corrupt json content!!! not valid") + + response = await client.get( + f"/api/v1/mcp/project/{user_test_project.id}/installed", + headers=logged_in_headers, + ) + + assert response.status_code == 200 + results = response.json() + + for entry in results: + assert entry["available"] is True, f"{entry['name']} should be available (directory exists)" + assert entry["installed"] is False, f"{entry['name']} should not be installed (JSON is corrupt)" diff --git a/src/backend/tests/unit/test_kb_storage_deletion.py b/src/backend/tests/unit/test_kb_storage_deletion.py new file mode 100644 index 000000000000..ba6345e4eaf9 --- /dev/null +++ b/src/backend/tests/unit/test_kb_storage_deletion.py @@ -0,0 +1,392 @@ +"""Tests for unified Knowledge Base deletion and resource cleanup. + +Covers the delete_storage method, release_chroma_resources, private helpers +(_remove_sqlite_lock_files, _truncate_sqlite_files), and the delete endpoints. +""" + +import json +import shutil +import uuid +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def kb_dir(tmp_path): + """Create a fake KB directory with SQLite files simulating ChromaDB.""" + kb = tmp_path / "test_kb" + kb.mkdir() + (kb / "chroma.sqlite3").write_bytes(b"fake-sqlite-data") + (kb / "chroma.sqlite3-wal").write_bytes(b"wal-data") + (kb / "chroma.sqlite3-shm").write_bytes(b"shm-data") + (kb / "embedding_metadata.json").write_text(json.dumps({"id": str(uuid.uuid4())})) + return kb + + +@pytest.fixture +def empty_kb_dir(tmp_path): + """Create a KB directory with no ChromaDB data files.""" + kb = tmp_path / "empty_kb" + kb.mkdir() + return kb + + +# =========================================================================== +# Unit tests: _remove_sqlite_lock_files +# =========================================================================== + + +class TestRemoveSqliteLockFiles: + """Tests for _remove_sqlite_lock_files — removes WAL, SHM, journal files.""" + + def test_should_remove_all_lock_files(self, kb_dir): + from langflow.api.utils.kb_helpers import _remove_sqlite_lock_files + + (kb_dir / "chroma.sqlite3-journal").write_bytes(b"journal") + assert (kb_dir / "chroma.sqlite3-wal").exists() + assert (kb_dir / "chroma.sqlite3-shm").exists() + + _remove_sqlite_lock_files(kb_dir) + + assert not (kb_dir / "chroma.sqlite3-wal").exists() + assert not (kb_dir / "chroma.sqlite3-shm").exists() + assert not (kb_dir / "chroma.sqlite3-journal").exists() + assert (kb_dir / "chroma.sqlite3").exists() + + def test_should_not_raise_when_no_lock_files(self, empty_kb_dir): + from langflow.api.utils.kb_helpers import _remove_sqlite_lock_files + + _remove_sqlite_lock_files(empty_kb_dir) + + def test_should_handle_permission_error_gracefully(self, kb_dir): + from langflow.api.utils.kb_helpers import _remove_sqlite_lock_files + + with patch.object(Path, "unlink", side_effect=OSError("Permission denied")): + _remove_sqlite_lock_files(kb_dir) + + +# =========================================================================== +# Unit tests: _truncate_sqlite_files +# =========================================================================== + + +class TestTruncateSqliteFiles: + """Tests for _truncate_sqlite_files — truncates .sqlite3 files.""" + + def test_should_truncate_sqlite_files_to_zero(self, kb_dir): + from langflow.api.utils.kb_helpers import _truncate_sqlite_files + + assert (kb_dir / "chroma.sqlite3").stat().st_size > 0 + + _truncate_sqlite_files(kb_dir) + + assert (kb_dir / "chroma.sqlite3").stat().st_size == 0 + + def test_should_not_raise_when_no_sqlite_files(self, empty_kb_dir): + from langflow.api.utils.kb_helpers import _truncate_sqlite_files + + _truncate_sqlite_files(empty_kb_dir) + + def test_should_handle_locked_file_gracefully(self, kb_dir): + from langflow.api.utils.kb_helpers import _truncate_sqlite_files + + with patch("builtins.open", side_effect=OSError("File is locked")): + _truncate_sqlite_files(kb_dir) + + +# =========================================================================== +# Unit tests: KBStorageHelper.release_chroma_resources +# =========================================================================== + + +class TestReleaseChromaResources: + """Tests for release_chroma_resources — clears registry and forces GC.""" + + def test_should_clear_registry_entry_for_path(self, kb_dir): + from chromadb.api.shared_system_client import SharedSystemClient + from langflow.api.utils.kb_helpers import KBStorageHelper + + path_key = str(kb_dir) + SharedSystemClient._identifier_to_system[path_key] = MagicMock() + + KBStorageHelper.release_chroma_resources(kb_dir) + + assert path_key not in SharedSystemClient._identifier_to_system + + def test_should_not_raise_when_path_not_in_registry(self, tmp_path): + from langflow.api.utils.kb_helpers import KBStorageHelper + + KBStorageHelper.release_chroma_resources(tmp_path / "nonexistent") + + @patch("langflow.api.utils.kb_helpers.gc.collect") + def test_should_call_gc_collect(self, mock_gc, tmp_path): + from langflow.api.utils.kb_helpers import KBStorageHelper + + KBStorageHelper.release_chroma_resources(tmp_path) + + mock_gc.assert_called_once() + + +# =========================================================================== +# Unit tests: KBStorageHelper.delete_storage +# =========================================================================== + + +class TestDeleteStorage: + """Tests for KBStorageHelper.delete_storage — unified deletion with retry.""" + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + def test_should_return_true_when_path_does_not_exist(self, tmp_path): + from langflow.api.utils.kb_helpers import KBStorageHelper + + non_existent = tmp_path / "does_not_exist" + result = KBStorageHelper.delete_storage(non_existent, "ghost_kb") + + assert result is True + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + def test_should_delete_directory_on_first_attempt(self, kb_dir): + from langflow.api.utils.kb_helpers import KBStorageHelper + + result = KBStorageHelper.delete_storage(kb_dir, "test_kb") + + assert result is True + assert not kb_dir.exists() + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + def test_should_retry_and_succeed_on_second_attempt(self, kb_dir): + from langflow.api.utils.kb_helpers import KBStorageHelper + + original_rmtree = shutil.rmtree + call_count = 0 + + def rmtree_fails_once(path, *, ignore_errors=False): + nonlocal call_count + call_count += 1 + if call_count == 1: + msg = "[WinError 32] File in use" + raise OSError(msg) + original_rmtree(path, ignore_errors=ignore_errors) + + with patch("langflow.api.utils.kb_helpers.shutil.rmtree", side_effect=rmtree_fails_once): + result = KBStorageHelper.delete_storage(kb_dir, "test_kb") + + assert result is True + assert call_count == 2 + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + def test_should_rename_as_fallback_when_all_retries_fail(self, kb_dir): + from langflow.api.utils.kb_helpers import KBStorageHelper + + with patch( + "langflow.api.utils.kb_helpers.shutil.rmtree", + side_effect=OSError("[WinError 32] File in use"), + ): + result = KBStorageHelper.delete_storage(kb_dir, "test_kb") + + assert result is True + assert not kb_dir.exists() + renamed_dirs = [p for p in kb_dir.parent.iterdir() if p.name.startswith(".deleted_test_kb_")] + assert len(renamed_dirs) == 1 + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + def test_should_return_false_when_all_strategies_fail(self, kb_dir): + from langflow.api.utils.kb_helpers import KBStorageHelper + + with ( + patch( + "langflow.api.utils.kb_helpers.shutil.rmtree", + side_effect=OSError("[WinError 32] File in use"), + ), + patch.object(Path, "rename", side_effect=OSError("Cannot rename")), + ): + result = KBStorageHelper.delete_storage(kb_dir, "test_kb") + + assert result is False + assert kb_dir.exists() + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep") + def test_should_use_exponential_backoff_on_retries(self, mock_sleep, kb_dir): + from langflow.api.utils.kb_helpers import KBStorageHelper + + real_rmtree = shutil.rmtree + call_count = 0 + + def rmtree_fails_three_times(path, *, ignore_errors=False): + nonlocal call_count + call_count += 1 + if call_count <= 3: + msg = "[WinError 32] File in use" + raise OSError(msg) + real_rmtree(path, ignore_errors=ignore_errors) + + with patch("langflow.api.utils.kb_helpers.shutil.rmtree", side_effect=rmtree_fails_three_times): + result = KBStorageHelper.delete_storage(kb_dir, "test_kb") + + assert result is True + sleep_values = [call.args[0] for call in mock_sleep.call_args_list] + assert sleep_values == [1.0, 2.0, 4.0] + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client") + @patch("langflow.api.utils.kb_helpers.Chroma") + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + def test_should_teardown_collection_before_deletion(self, mock_chroma_cls, mock_client_cls, kb_dir): + from langflow.api.utils.kb_helpers import KBStorageHelper + + mock_chroma = MagicMock() + mock_chroma_cls.return_value = mock_chroma + mock_client_cls.return_value = MagicMock() + + KBStorageHelper.delete_storage(kb_dir, "test_kb") + + mock_chroma.delete_collection.assert_called_once() + assert not kb_dir.exists() + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client") + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + def test_should_not_fail_when_teardown_raises(self, mock_client_cls, kb_dir): + from langflow.api.utils.kb_helpers import KBStorageHelper + + mock_client_cls.side_effect = OSError("Cannot open database") + + result = KBStorageHelper.delete_storage(kb_dir, "test_kb") + + assert result is True + assert not kb_dir.exists() + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + def test_should_skip_teardown_when_no_chroma_data(self, empty_kb_dir): + from langflow.api.utils.kb_helpers import KBStorageHelper + + result = KBStorageHelper.delete_storage(empty_kb_dir, "empty_kb") + + assert result is True + assert not empty_kb_dir.exists() + + +# =========================================================================== +# Integration tests: delete endpoints using unified delete_storage +# =========================================================================== + + +class TestDeleteEndpoint: + """Tests that delete endpoint uses KBStorageHelper.delete_storage.""" + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.get_root_path") + async def test_should_delete_kb_successfully(self, mock_root, client, logged_in_headers, tmp_path): + mock_root.return_value = tmp_path + (tmp_path / "activeuser" / "My_KB").mkdir(parents=True) + + response = await client.delete("api/v1/knowledge_bases/My_KB", headers=logged_in_headers) + + assert response.status_code == 200 + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.delete_storage", return_value=False) + @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.get_root_path") + async def test_should_return_500_when_deletion_fails( + self, mock_root, mock_delete, client, logged_in_headers, tmp_path + ): + mock_root.return_value = tmp_path + (tmp_path / "activeuser" / "My_KB").mkdir(parents=True) + + response = await client.delete("api/v1/knowledge_bases/My_KB", headers=logged_in_headers) + + assert response.status_code == 500 + assert "may be in use" in response.json()["detail"] + mock_delete.assert_called_once() + + async def test_should_return_404_when_kb_not_found(self, client, logged_in_headers): + response = await client.delete("api/v1/knowledge_bases/NonExistent_KB", headers=logged_in_headers) + + assert response.status_code == 404 + + +class TestBulkDeleteEndpoint: + """Tests that bulk delete endpoint uses KBStorageHelper.delete_storage.""" + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.get_fresh_chroma_client", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.Chroma", new=MagicMock()) + @patch("langflow.api.utils.kb_helpers.time.sleep", new=MagicMock()) + @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.get_root_path") + async def test_should_delete_multiple_kbs(self, mock_root, client, logged_in_headers, tmp_path): + mock_root.return_value = tmp_path + kb_user_path = tmp_path / "activeuser" + kb_user_path.mkdir(parents=True) + (kb_user_path / "KB1").mkdir() + (kb_user_path / "KB2").mkdir() + + response = await client.request( + "DELETE", + "api/v1/knowledge_bases", + headers=logged_in_headers, + json={"kb_names": ["KB1", "KB2"]}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["deleted_count"] == 2 + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.delete_storage") + @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.get_root_path") + async def test_should_handle_partial_failure(self, mock_root, mock_delete, client, logged_in_headers, tmp_path): + mock_root.return_value = tmp_path + kb_user_path = tmp_path / "activeuser" + kb_user_path.mkdir(parents=True) + (kb_user_path / "KB1").mkdir() + (kb_user_path / "KB2").mkdir() + + mock_delete.side_effect = [True, False] + + response = await client.request( + "DELETE", + "api/v1/knowledge_bases", + headers=logged_in_headers, + json={"kb_names": ["KB1", "KB2"]}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["deleted_count"] == 1 + + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.delete_storage", new=MagicMock(return_value=True)) + @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.get_root_path") + async def test_should_report_not_found_kbs(self, mock_root, client, logged_in_headers, tmp_path): + mock_root.return_value = tmp_path + kb_user_path = tmp_path / "activeuser" + kb_user_path.mkdir(parents=True) + (kb_user_path / "KB1").mkdir() + + response = await client.request( + "DELETE", + "api/v1/knowledge_bases", + headers=logged_in_headers, + json={"kb_names": ["KB1", "Ghost"]}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["deleted_count"] == 1 + assert "Ghost" in data["not_found"] diff --git a/src/backend/tests/unit/test_knowledge_bases_api.py b/src/backend/tests/unit/test_knowledge_bases_api.py index 07065d4a41ae..34f02e1c70be 100644 --- a/src/backend/tests/unit/test_knowledge_bases_api.py +++ b/src/backend/tests/unit/test_knowledge_bases_api.py @@ -259,22 +259,22 @@ async def test_get_knowledge_base_detail(self, mock_root, client: AsyncClient, l assert data["chunks"] == 5 assert data["name"] == "Detail KB" - @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.teardown_storage") + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.delete_storage", return_value=True) @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.get_root_path") async def test_delete_knowledge_base( - self, mock_root, mock_teardown, client: AsyncClient, logged_in_headers, tmp_path + self, mock_root, mock_delete, client: AsyncClient, logged_in_headers, tmp_path ): mock_root.return_value = tmp_path (tmp_path / "activeuser" / "To_Delete").mkdir(parents=True, exist_ok=True) response = await client.delete("api/v1/knowledge_bases/To_Delete", headers=logged_in_headers) assert response.status_code == 200 - mock_teardown.assert_called_once() + mock_delete.assert_called_once() - @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.teardown_storage") + @patch("langflow.api.utils.kb_helpers.KBStorageHelper.delete_storage", return_value=True) @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.get_root_path") async def test_bulk_delete_knowledge_bases( - self, mock_root, mock_teardown, client: AsyncClient, logged_in_headers, tmp_path + self, mock_root, mock_delete, client: AsyncClient, logged_in_headers, tmp_path ): mock_root.return_value = tmp_path kb_user_path = tmp_path / "activeuser" @@ -292,7 +292,7 @@ async def test_bulk_delete_knowledge_bases( data = response.json() assert data["deleted_count"] == 2 assert "NonExistent" in data["not_found"] - assert mock_teardown.called + assert mock_delete.called @patch("langflow.api.v1.knowledge_bases.KBStorageHelper.get_root_path") @patch("langflow.api.v1.knowledge_bases.KBAnalysisHelper.get_metadata") diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index f59667117bfa..04d90cdae059 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "langflow", - "version": "1.8.0", + "version": "1.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "langflow", - "version": "1.8.0", + "version": "1.8.1", "dependencies": { "@chakra-ui/number-input": "^2.1.2", "@chakra-ui/system": "^2.6.2", diff --git a/src/frontend/package.json b/src/frontend/package.json index e15ffc52ba41..6185adb5b00d 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,6 +1,6 @@ { "name": "langflow", - "version": "1.8.0", + "version": "1.8.1", "private": true, "engines": { "node": ">=20.19.0" diff --git a/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.ts b/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.ts index 73dcb3b599b3..d6a6e69288a7 100644 --- a/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.ts +++ b/src/frontend/src/CustomNodes/hooks/use-fetch-data-on-mount.ts @@ -20,15 +20,29 @@ const useFetchDataOnMount = ( useEffect(() => { async function fetchData() { const template = node.template[name]; - if ( - (template?.real_time_refresh || - template?.refresh_button || - (node.tool_mode && name === "tools_metadata")) && - // options can be undefined but not an empty array - (template?.options?.length ?? 0) === 0 - ) { + if (!template) return; + + const isRealtimeOrRefresh = + template.real_time_refresh || + template.refresh_button || + (node.tool_mode && name === "tools_metadata"); + + const hasOptions = (template.options?.length ?? 0) > 0; + + const needApiKeyPrefill = + name === "model" && + node.template?.api_key != null && + !node.template?.api_key?.value; + + const shouldFetchOnMount = + isRealtimeOrRefresh && + (!hasOptions || + (name === "api_key" && !template.value) || + needApiKeyPrefill); + + if (shouldFetchOnMount) { mutateTemplate( - template?.value, + template.value, nodeId, node, setNodeClass, @@ -41,7 +55,7 @@ const useFetchDataOnMount = ( } } fetchData(); - }, []); // Empty dependency array ensures that this effect runs only once, on mount + }, []); }; export default useFetchDataOnMount; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/inputGlobalComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/inputGlobalComponent/index.tsx index 8cc8f9067fb1..db7da237ae94 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/inputGlobalComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/inputGlobalComponent/index.tsx @@ -7,6 +7,7 @@ import { CommandItem } from "../../../../ui/command"; import GlobalVariableModal from "../../../GlobalVariableModal/GlobalVariableModal"; import { getPlaceholder } from "../../helpers/get-placeholder-disabled"; import type { InputGlobalComponentType, InputProps } from "../../types"; +import { looksLikeVariableName } from "../../../../../utils/reactflowUtils"; import InputComponent from "../inputComponent"; import { useGlobalVariableValue, @@ -114,9 +115,21 @@ export default function InputGlobalComponent({ /> ); - // // Extract options list for better readability - const variableOptions = typedGlobalVariables.map((variable) => variable.name); - const selectedOption = loadFromDb && valueExists ? currentValue : ""; + let variableOptions = typedGlobalVariables.map((variable) => variable.name); + + const isEnvVarName = + password && currentValue && looksLikeVariableName(currentValue); + if ( + (loadFromDb && + currentValue && + !valueExists && + !variableOptions.includes(currentValue)) || + (isEnvVarName && !variableOptions.includes(currentValue)) + ) { + variableOptions = [...variableOptions, currentValue]; + } + + const selectedOption = loadFromDb || isEnvVarName ? currentValue : ""; if (!showParameter) { return null; diff --git a/src/frontend/src/components/ui/__tests__/dialog.test.tsx b/src/frontend/src/components/ui/__tests__/dialog.test.tsx index 551d52d9c486..387a7b07c996 100644 --- a/src/frontend/src/components/ui/__tests__/dialog.test.tsx +++ b/src/frontend/src/components/ui/__tests__/dialog.test.tsx @@ -14,7 +14,7 @@ jest.mock("@/components/common/genericIconComponent", () => ({ default: () => null, })); -const renderWithProviders = (ui: ReactElement) => { +const renderWithProviders = (ui: React.ReactElement) => { return render({ui}); }; diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 0fcb2874d49a..e5bb2fa6ff3e 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -741,7 +741,7 @@ export const ZERO_NOTIFICATIONS = "No new notifications"; export const SUCCESS_BUILD = "Built successfully ✨"; export const ALERT_SAVE_WITH_API = - "Caution: Unchecking this box only removes API keys from fields specifically designated for API keys."; + "⚠️ Caution: Exporting this flow may expose sensitive credentials."; export const SAVE_WITH_API_CHECKBOX = "Save with my API keys"; export const EDIT_TEXT_MODAL_TITLE = "Edit Text"; diff --git a/src/frontend/src/icons/AstraDB/AstraDB.jsx b/src/frontend/src/icons/AstraDB/AstraDB.jsx index b1962327ae9f..6aa319d6bdbe 100644 --- a/src/frontend/src/icons/AstraDB/AstraDB.jsx +++ b/src/frontend/src/icons/AstraDB/AstraDB.jsx @@ -1,8 +1,9 @@ const AstraSVG = (props) => ( { try { - // TODO: Full-version export (embedding all versions) is planned as a follow-up feature. - // For now, export only the current working version of the flow. - const flowToExport: FlowType = { + let flowToExport: FlowType = { id: currentFlow!.id, data: currentFlow!.data!, description, diff --git a/src/frontend/src/utils/__tests__/removeApiKeys.test.ts b/src/frontend/src/utils/__tests__/removeApiKeys.test.ts new file mode 100644 index 000000000000..767fc103146b --- /dev/null +++ b/src/frontend/src/utils/__tests__/removeApiKeys.test.ts @@ -0,0 +1,79 @@ +import { removeApiKeys } from "../reactflowUtils"; + +describe("removeApiKeys", () => { + function makeFlow(template: Record) { + return { + data: { + nodes: [ + { + type: "genericNode", + data: { + node: { + template, + }, + }, + }, + ], + edges: [], + }, + } as any; + } + + it("preserves api_key when it is an env/global variable name", () => { + const flow = makeFlow({ + api_key: { + name: "api_key", + value: "OPENAI_API_KEY", + password: true, + load_from_db: true, + }, + openai_api_key: { + name: "openai_api_key", + value: "sk-test-123", + password: true, + load_from_db: false, + }, + }); + + const result = removeApiKeys(flow); + const template = result.data!.nodes[0].data.node!.template; + + expect(template.api_key.value).toBe("OPENAI_API_KEY"); + expect(template.api_key.load_from_db).toBe(true); + + expect(template.openai_api_key.value).toBe(""); + expect(template.openai_api_key.load_from_db).toBe(false); + }); + + it("clears api_key when it contains a raw secret", () => { + const flow = makeFlow({ + api_key: { + name: "api_key", + value: "sk-secret-123", + password: true, + load_from_db: false, + }, + }); + + const result = removeApiKeys(flow); + const template = result.data!.nodes[0].data.node!.template; + + expect(template.api_key.value).toBe(""); + expect(template.api_key.load_from_db).toBe(false); + }); + + it("preserves non-password fields", () => { + const flow = makeFlow({ + regular_field: { + name: "regular_field", + value: "keep-me", + password: false, + }, + }); + + const result = removeApiKeys(flow); + const template = result.data!.nodes[0].data.node!.template; + + expect(template.regular_field.value).toBe("keep-me"); + }); +}); diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 3dbbd96c9c5e..6db3861499e5 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -457,16 +457,32 @@ export function isValidConnection( return false; } +export function looksLikeVariableName(value: unknown): boolean { + if (typeof value !== "string" || !value.trim()) return false; + return /^[A-Z][A-Z0-9_]*$/i.test(value.trim()); +} + export function removeApiKeys(flow: FlowType): FlowType { - const cleanFLow = cloneDeep(flow); - cleanFLow.data!.nodes.forEach((node) => { + const cleanFlow = cloneDeep(flow); + cleanFlow.data!.nodes.forEach((node) => { if (node.type !== "genericNode") return; - for (const key in node.data.node!.template) { - const field = node.data.node!.template[key]; + const template = node.data.node!.template; + for (const key in template) { + const field = template[key]; - // Remove password fields if (field.password) { + // Preserve env/global variable names for api_key so imported flows + // can still resolve credentials, but strip any raw secrets. + if ( + key === "api_key" && + ((typeof field.value === "string" && + looksLikeVariableName(field.value)) || + field.load_from_db === true) + ) { + continue; + } field.value = ""; + field.load_from_db = false; } // Handle MCP server configurations @@ -475,12 +491,11 @@ export function removeApiKeys(flow: FlowType): FlowType { field.value && typeof field.value === "object" ) { - // Type assertion is safe here as we've verified it's an object with runtime checks cleanMcpConfig(field.value as MCPServerValue); } } }); - return cleanFLow; + return cleanFlow; } export function updateTemplate( diff --git a/src/frontend/tests/assets/outdated_flow.json b/src/frontend/tests/assets/outdated_flow.json index b9897e3e7412..6a729385aff9 100644 --- a/src/frontend/tests/assets/outdated_flow.json +++ b/src/frontend/tests/assets/outdated_flow.json @@ -523,7 +523,7 @@ "show": true, "title_case": false, "type": "str", - "value": "dummy_api_key" + "value": "" }, "code": { "advanced": true, diff --git a/src/frontend/tests/core/regression/session-deletion-data-leakage.spec.ts b/src/frontend/tests/core/regression/session-deletion-data-leakage.spec.ts index b84b500a6f70..8320f79d067f 100644 --- a/src/frontend/tests/core/regression/session-deletion-data-leakage.spec.ts +++ b/src/frontend/tests/core/regression/session-deletion-data-leakage.spec.ts @@ -36,19 +36,26 @@ test.describe("Session Deletion Data Leakage Fix", () => { // Helper to delete a session via the more menu async function deleteSession(page: Page, sessionName: string) { + // Find all session selectors const sessionSelectors = await page.getByTestId("session-selector").all(); + // Find the one with exact matching text for (const selector of sessionSelectors) { const text = await selector.textContent(); + // Use exact match to avoid matching "Default Session" when looking for "New Session 0" if (text?.trim() === sessionName) { + // Hover to make the more button visible await selector.hover(); - await page.waitForTimeout(500); + await page.waitForTimeout(500); // Wait for hover effects + // Click the more options button const moreButton = selector.locator('[aria-label="More options"]'); await moreButton.click({ timeout: 5000 }); + // Wait for the menu to open await page.waitForTimeout(500); + // Wait for delete option to be visible and click it await page .getByTestId("delete-session-option") .waitFor({ state: "visible", timeout: 5000 }); @@ -183,6 +190,7 @@ test.describe("Session Deletion Data Leakage Fix", () => { const responseText = await lastMessage.textContent(); // Verify the AI does NOT remember "Victor" from the deleted session + // The response should indicate it doesn't know the name expect(responseText?.toLowerCase()).not.toContain("victor"); }, ); diff --git a/src/lfx/pyproject.toml b/src/lfx/pyproject.toml index ca5a474d9910..864a4edc3dbc 100644 --- a/src/lfx/pyproject.toml +++ b/src/lfx/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lfx" -version = "0.3.0" +version = "0.3.1" description = "Langflow Executor - A lightweight CLI tool for executing and serving Langflow AI flows" readme = "README.md" authors = [ @@ -12,7 +12,7 @@ dependencies = [ "pandas>=2.0.0,<3.0.0", "pydantic>=2.0.0,<3.0.0", "pillow>=10.0.0,<13.0.0", - "fastapi>=0.115.13,<1.0.0", + "fastapi>=0.135.0,<1.0.0", "uvicorn>=0.34.3,<1.0.0", "typer>=0.16.0,<1.0.0", "platformdirs>=4.3.8,<5.0.0", diff --git a/src/lfx/src/lfx/_assets/component_index.json b/src/lfx/src/lfx/_assets/component_index.json index ab3ea80f9336..f7b6e163cade 100644 --- a/src/lfx/src/lfx/_assets/component_index.json +++ b/src/lfx/src/lfx/_assets/component_index.json @@ -10026,7 +10026,7 @@ "dependencies": [ { "name": "chromadb", - "version": "1.5.4" + "version": "1.5.5" }, { "name": "langchain_chroma", @@ -60031,7 +60031,7 @@ "dependencies": [ { "name": "astrapy", - "version": "2.1.0" + "version": "2.2.1" }, { "name": "langchain_core", @@ -62752,7 +62752,7 @@ "dependencies": [ { "name": "astrapy", - "version": "2.1.0" + "version": "2.2.1" }, { "name": "langchain_core", @@ -64212,7 +64212,7 @@ }, { "name": "astrapy", - "version": "2.1.0" + "version": "2.2.1" } ], "total_dependencies": 3 @@ -68502,7 +68502,7 @@ "dependencies": [ { "name": "numpy", - "version": "2.4.2" + "version": "2.4.3" }, { "name": "lfx", @@ -69910,7 +69910,7 @@ "dependencies": [ { "name": "chromadb", - "version": "1.5.4" + "version": "1.5.5" }, { "name": "cryptography", @@ -88098,7 +88098,7 @@ "icon": "square-function", "legacy": false, "metadata": { - "code_hash": "4fb127dc371c", + "code_hash": "e9c638ca3f34", "dependencies": { "dependencies": [ { @@ -88195,7 +88195,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom collections.abc import Callable # noqa: TC003 - required at runtime for dynamic exec()\nfrom typing import Any\n\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, IntInput, ModelInput, MultilineInput, Output, SecretStrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.constants import MESSAGE_SENDER_AI\n\nTEXT_TRANSFORM_PROMPT = (\n \"Given this text, create a Python lambda function that transforms it \"\n \"according to the instruction.\\n\"\n \"The lambda should take a string parameter and return the transformed string.\\n\\n\"\n \"Text Preview:\\n{text_preview}\\n\\n\"\n \"Instruction: {instruction}\\n\\n\"\n \"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\\n\"\n \"Just a string starting with lambda.\\n\"\n \"Example: lambda text: text.upper()\"\n)\n\nDATA_TRANSFORM_PROMPT = (\n \"Given this data structure and examples, create a Python lambda function \"\n \"that implements the following instruction:\\n\\n\"\n \"Data Structure:\\n{dump_structure}\\n\\n\"\n \"Example Items:\\n{data_sample}\\n\\n\"\n \"Instruction: {instruction}\\n\\n\"\n \"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\\n\"\n \"Just a string starting with lambda.\"\n)\n\n\nclass LambdaFilterComponent(Component):\n display_name = \"Smart Transform\"\n description = \"Uses an LLM to generate a function for filtering or transforming structured data and messages.\"\n documentation: str = \"https://docs.langflow.org/smart-transform\"\n icon = \"square-function\"\n name = \"Smart Transform\"\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"JSON\",\n info=\"The structured data or text messages to filter or transform using a lambda function.\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\", \"Message\"],\n is_list=True,\n required=True,\n ),\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MultilineInput(\n name=\"filter_instruction\",\n display_name=\"Instructions\",\n info=(\n \"Natural language instructions for how to filter or transform the data using a lambda function. \"\n \"Examples: 'Filter the data to only include items where status is active', \"\n \"'Convert the text to uppercase', 'Keep only first 100 characters'\"\n ),\n value=\"Transform the data to...\",\n required=True,\n ),\n IntInput(\n name=\"sample_size\",\n display_name=\"Sample Size\",\n info=\"For large datasets, number of items to sample from head/tail.\",\n value=1000,\n advanced=True,\n ),\n IntInput(\n name=\"max_size\",\n display_name=\"Max Size\",\n info=\"Number of characters for the data to be considered large.\",\n value=30000,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Output\",\n name=\"data_output\",\n method=\"process_as_data\",\n ),\n Output(\n display_name=\"Output\",\n name=\"dataframe_output\",\n method=\"process_as_dataframe\",\n ),\n Output(\n display_name=\"Output\",\n name=\"message_output\",\n method=\"process_as_message\",\n ),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n def get_data_structure(self, data):\n \"\"\"Extract the structure of data, replacing values with their types.\"\"\"\n if isinstance(data, list):\n # For lists, get structure of first item if available\n if data:\n return [self.get_data_structure(data[0])]\n return []\n if isinstance(data, dict):\n return {k: self.get_data_structure(v) for k, v in data.items()}\n # For primitive types, return the type name\n return type(data).__name__\n\n def _validate_lambda(self, lambda_text: str) -> bool:\n \"\"\"Validate the provided lambda function text.\"\"\"\n # Return False if the lambda function does not start with 'lambda' or does not contain a colon\n return lambda_text.strip().startswith(\"lambda\") and \":\" in lambda_text\n\n def _get_input_type_name(self) -> str:\n \"\"\"Detect and return the input type name for error messages.\"\"\"\n if isinstance(self.data, Message):\n return \"Message\"\n if isinstance(self.data, DataFrame):\n return \"DataFrame\"\n if isinstance(self.data, Data):\n return \"Data\"\n if isinstance(self.data, list) and len(self.data) > 0:\n first = self.data[0]\n if isinstance(first, Message):\n return \"Message\"\n if isinstance(first, DataFrame):\n return \"DataFrame\"\n if isinstance(first, Data):\n return \"Data\"\n return \"unknown\"\n\n def _extract_message_text(self) -> str:\n \"\"\"Extract text content from Message input(s).\"\"\"\n if isinstance(self.data, Message):\n return self.data.text or \"\"\n\n texts = [msg.text or \"\" for msg in self.data if isinstance(msg, Message)]\n return \"\\n\\n\".join(texts) if len(texts) > 1 else (texts[0] if texts else \"\")\n\n def _extract_structured_data(self) -> dict | list:\n \"\"\"Extract structured data from Data or DataFrame input(s).\"\"\"\n if isinstance(self.data, DataFrame):\n return self.data.to_dict(orient=\"records\")\n\n if hasattr(self.data, \"data\"):\n return self.data.data\n\n if not isinstance(self.data, list):\n return self.data\n\n combined_data: list[dict] = []\n for item in self.data:\n if isinstance(item, DataFrame):\n combined_data.extend(item.to_dict(orient=\"records\"))\n elif hasattr(item, \"data\"):\n if isinstance(item.data, dict):\n combined_data.append(item.data)\n elif isinstance(item.data, list):\n combined_data.extend(item.data)\n\n if len(combined_data) == 1 and isinstance(combined_data[0], dict):\n return combined_data[0]\n if len(combined_data) == 0:\n return {}\n return combined_data\n\n def _is_message_input(self) -> bool:\n \"\"\"Check if input is Message type.\"\"\"\n if isinstance(self.data, Message):\n return True\n return isinstance(self.data, list) and len(self.data) > 0 and isinstance(self.data[0], Message)\n\n def _build_text_prompt(self, text: str) -> str:\n \"\"\"Build prompt for text/Message transformation.\"\"\"\n text_length = len(text)\n if text_length > self.max_size:\n text_preview = (\n f\"Text length: {text_length} characters\\n\\n\"\n f\"First {self.sample_size} characters:\\n{text[: self.sample_size]}\\n\\n\"\n f\"Last {self.sample_size} characters:\\n{text[-self.sample_size :]}\"\n )\n else:\n text_preview = text\n\n return TEXT_TRANSFORM_PROMPT.format(text_preview=text_preview, instruction=self.filter_instruction)\n\n def _build_data_prompt(self, data: dict | list) -> str:\n \"\"\"Build prompt for structured data transformation.\"\"\"\n dump = json.dumps(data)\n dump_structure = json.dumps(self.get_data_structure(data))\n\n if len(dump) > self.max_size:\n data_sample = (\n f\"Data is too long to display...\\n\\nFirst lines (head): {dump[: self.sample_size]}\\n\\n\"\n f\"Last lines (tail): {dump[-self.sample_size :]}\"\n )\n else:\n data_sample = dump\n\n return DATA_TRANSFORM_PROMPT.format(\n dump_structure=dump_structure, data_sample=data_sample, instruction=self.filter_instruction\n )\n\n def _parse_lambda_from_response(self, response_text: str) -> Callable[[Any], Any]:\n \"\"\"Extract and validate lambda function from LLM response.\"\"\"\n lambda_match = re.search(r\"lambda\\s+\\w+\\s*:.*?(?=\\n|$)\", response_text)\n if not lambda_match:\n msg = f\"Could not find lambda in response: {response_text}\"\n raise ValueError(msg)\n\n lambda_text = lambda_match.group().strip()\n self.log(f\"Generated lambda: {lambda_text}\")\n\n if not self._validate_lambda(lambda_text):\n msg = f\"Invalid lambda format: {lambda_text}\"\n raise ValueError(msg)\n\n return eval(lambda_text) # noqa: S307\n\n async def _execute_lambda(self) -> Any:\n \"\"\"Generate and execute a lambda function based on input type.\"\"\"\n if self._is_message_input():\n data: Any = self._extract_message_text()\n prompt = self._build_text_prompt(data)\n else:\n data = self._extract_structured_data()\n prompt = self._build_data_prompt(data)\n\n llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)\n response = await llm.ainvoke(prompt)\n response_text = response.content if hasattr(response, \"content\") else str(response)\n\n fn = self._parse_lambda_from_response(response_text)\n return fn(data)\n\n def _handle_process_error(self, error: Exception, output_type: str) -> None:\n \"\"\"Handle errors from process methods with context-aware messages.\"\"\"\n input_type = self._get_input_type_name()\n error_msg = (\n f\"Failed to convert result to {output_type} output. \"\n f\"Error: {error}. \"\n f\"Input type was {input_type}. \"\n f\"Try using the same output type as the input.\"\n )\n raise ValueError(error_msg) from error\n\n def _convert_result_to_data(self, result: Any) -> Data:\n \"\"\"Convert lambda result to Data object.\"\"\"\n if isinstance(result, dict):\n return Data(data=result)\n if isinstance(result, list):\n return Data(data={\"_results\": result})\n return Data(data={\"text\": str(result)})\n\n def _convert_result_to_dataframe(self, result: Any) -> DataFrame:\n \"\"\"Convert lambda result to DataFrame object.\"\"\"\n if isinstance(result, list):\n if all(isinstance(item, dict) for item in result):\n return DataFrame(result)\n return DataFrame([{\"value\": item} for item in result])\n if isinstance(result, dict):\n return DataFrame([result])\n return DataFrame([{\"value\": str(result)}])\n\n def _convert_result_to_message(self, result: Any) -> Message:\n \"\"\"Convert lambda result to Message object.\"\"\"\n if isinstance(result, str):\n return Message(text=result, sender=MESSAGE_SENDER_AI)\n if isinstance(result, list):\n text = \"\\n\".join(str(item) for item in result)\n return Message(text=text, sender=MESSAGE_SENDER_AI)\n if isinstance(result, dict):\n text = json.dumps(result, indent=2)\n return Message(text=text, sender=MESSAGE_SENDER_AI)\n return Message(text=str(result), sender=MESSAGE_SENDER_AI)\n\n async def process_as_data(self) -> Data:\n \"\"\"Process the data and return as a Data object.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_data(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"Data\")\n\n async def process_as_dataframe(self) -> DataFrame:\n \"\"\"Process the data and return as a DataFrame.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_dataframe(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"DataFrame\")\n\n async def process_as_message(self) -> Message:\n \"\"\"Process the data and return as a Message.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_message(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"Message\")\n" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom collections.abc import Callable # noqa: TC003 - required at runtime for dynamic exec()\nfrom typing import Any\n\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import DataInput, IntInput, ModelInput, MultilineInput, Output, SecretStrInput\nfrom lfx.schema.data import Data\nfrom lfx.schema.dataframe import DataFrame\nfrom lfx.schema.message import Message\nfrom lfx.utils.constants import MESSAGE_SENDER_AI\n\nTEXT_TRANSFORM_PROMPT = (\n \"Given this text, create a Python lambda function that transforms it \"\n \"according to the instruction.\\n\"\n \"The lambda should take a string parameter and return the transformed string.\\n\\n\"\n \"Text Preview:\\n{text_preview}\\n\\n\"\n \"Instruction: {instruction}\\n\\n\"\n \"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\\n\"\n \"Just a string starting with lambda.\\n\"\n \"Example: lambda text: text.upper()\"\n)\n\nDATA_TRANSFORM_PROMPT = (\n \"Given this data structure and examples, create a Python lambda function \"\n \"that implements the following instruction:\\n\\n\"\n \"Data Structure:\\n{dump_structure}\\n\\n\"\n \"Example Items:\\n{data_sample}\\n\\n\"\n \"Instruction: {instruction}\\n\\n\"\n \"Return ONLY the lambda function and nothing else. No need for ```python or whatever.\\n\"\n \"Just a string starting with lambda.\"\n)\n\n\nclass LambdaFilterComponent(Component):\n display_name = \"Smart Transform\"\n description = \"Uses an LLM to generate a function for filtering or transforming structured data and messages.\"\n documentation: str = \"https://docs.langflow.org/smart-transform\"\n icon = \"square-function\"\n name = \"Smart Transform\"\n\n inputs = [\n DataInput(\n name=\"data\",\n display_name=\"JSON\",\n info=\"The structured data or text messages to filter or transform using a lambda function.\",\n input_types=[\"Data\", \"JSON\", \"DataFrame\", \"Table\", \"Message\"],\n is_list=True,\n required=True,\n ),\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MultilineInput(\n name=\"filter_instruction\",\n display_name=\"Instructions\",\n info=(\n \"Natural language instructions for how to filter or transform the data using a lambda function. \"\n \"Examples: 'Filter the data to only include items where status is active', \"\n \"'Convert the text to uppercase', 'Keep only first 100 characters'\"\n ),\n value=\"Transform the data to...\",\n required=True,\n ),\n IntInput(\n name=\"sample_size\",\n display_name=\"Sample Size\",\n info=\"For large datasets, number of items to sample from head/tail.\",\n value=1000,\n advanced=True,\n ),\n IntInput(\n name=\"max_size\",\n display_name=\"Max Size\",\n info=\"Number of characters for the data to be considered large.\",\n value=30000,\n advanced=True,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Output\",\n name=\"data_output\",\n method=\"process_as_data\",\n ),\n Output(\n display_name=\"Output\",\n name=\"dataframe_output\",\n method=\"process_as_dataframe\",\n ),\n Output(\n display_name=\"Output\",\n name=\"message_output\",\n method=\"process_as_message\",\n ),\n ]\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n\n def get_data_structure(self, data):\n \"\"\"Extract the structure of data, replacing values with their types.\"\"\"\n if isinstance(data, list):\n # For lists, get structure of first item if available\n if data:\n return [self.get_data_structure(data[0])]\n return []\n if isinstance(data, dict):\n return {k: self.get_data_structure(v) for k, v in data.items()}\n # For primitive types, return the type name\n return type(data).__name__\n\n def _validate_lambda(self, lambda_text: str) -> bool:\n \"\"\"Validate the provided lambda function text.\"\"\"\n # Return False if the lambda function does not start with 'lambda' or does not contain a colon\n return lambda_text.strip().startswith(\"lambda\") and \":\" in lambda_text\n\n def _get_input_type_name(self) -> str:\n \"\"\"Detect and return the input type name for error messages.\"\"\"\n if isinstance(self.data, Message):\n return \"Message\"\n if isinstance(self.data, DataFrame):\n return \"DataFrame\"\n if isinstance(self.data, Data):\n return \"Data\"\n if isinstance(self.data, list) and len(self.data) > 0:\n first = self.data[0]\n if isinstance(first, Message):\n return \"Message\"\n if isinstance(first, DataFrame):\n return \"DataFrame\"\n if isinstance(first, Data):\n return \"Data\"\n return \"unknown\"\n\n def _extract_message_text(self) -> str:\n \"\"\"Extract text content from Message input(s).\"\"\"\n if isinstance(self.data, Message):\n return self.data.text or \"\"\n\n texts = [msg.text or \"\" for msg in self.data if isinstance(msg, Message)]\n return \"\\n\\n\".join(texts) if len(texts) > 1 else (texts[0] if texts else \"\")\n\n def _extract_structured_data(self) -> dict | list:\n \"\"\"Extract structured data from Data or DataFrame input(s).\"\"\"\n if isinstance(self.data, DataFrame):\n return self.data.to_dict(orient=\"records\")\n\n if hasattr(self.data, \"data\"):\n return self.data.data\n\n if not isinstance(self.data, list):\n return self.data\n\n combined_data: list[dict] = []\n for item in self.data:\n if isinstance(item, DataFrame):\n combined_data.extend(item.to_dict(orient=\"records\"))\n elif hasattr(item, \"data\"):\n if isinstance(item.data, dict):\n combined_data.append(item.data)\n elif isinstance(item.data, list):\n combined_data.extend(item.data)\n\n if len(combined_data) == 1 and isinstance(combined_data[0], dict):\n return combined_data[0]\n if len(combined_data) == 0:\n return {}\n return combined_data\n\n def _is_message_input(self) -> bool:\n \"\"\"Check if input is Message type.\"\"\"\n if isinstance(self.data, Message):\n return True\n return isinstance(self.data, list) and len(self.data) > 0 and isinstance(self.data[0], Message)\n\n def _build_text_prompt(self, text: str) -> str:\n \"\"\"Build prompt for text/Message transformation.\"\"\"\n text_length = len(text)\n if text_length > self.max_size:\n text_preview = (\n f\"Text length: {text_length} characters\\n\\n\"\n f\"First {self.sample_size} characters:\\n{text[: self.sample_size]}\\n\\n\"\n f\"Last {self.sample_size} characters:\\n{text[-self.sample_size :]}\"\n )\n else:\n text_preview = text\n\n return TEXT_TRANSFORM_PROMPT.format(text_preview=text_preview, instruction=self.filter_instruction)\n\n def _build_data_prompt(self, data: dict | list) -> str:\n \"\"\"Build prompt for structured data transformation.\"\"\"\n dump = json.dumps(data)\n dump_structure = json.dumps(self.get_data_structure(data))\n\n if len(dump) > self.max_size:\n data_sample = (\n f\"Data is too long to display...\\n\\nFirst lines (head): {dump[: self.sample_size]}\\n\\n\"\n f\"Last lines (tail): {dump[-self.sample_size :]}\"\n )\n else:\n data_sample = dump\n\n return DATA_TRANSFORM_PROMPT.format(\n dump_structure=dump_structure, data_sample=data_sample, instruction=self.filter_instruction\n )\n\n def _parse_lambda_from_response(self, response_text: str) -> Callable[[Any], Any]:\n \"\"\"Extract and validate lambda function from LLM response.\"\"\"\n lambda_match = re.search(r\"lambda\\s+\\w+\\s*:.*?(?=\\n|$)\", response_text)\n if not lambda_match:\n msg = f\"Could not find lambda in response: {response_text}\"\n raise ValueError(msg)\n\n lambda_text = lambda_match.group().strip()\n self.log(f\"Generated lambda: {lambda_text}\")\n\n if not self._validate_lambda(lambda_text):\n msg = f\"Invalid lambda format: {lambda_text}\"\n raise ValueError(msg)\n\n return eval(lambda_text) # noqa: S307\n\n async def _execute_lambda(self) -> Any:\n \"\"\"Generate and execute a lambda function based on input type.\"\"\"\n if self._is_message_input():\n data: Any = self._extract_message_text()\n prompt = self._build_text_prompt(data)\n else:\n data = self._extract_structured_data()\n prompt = self._build_data_prompt(data)\n\n llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)\n response = await llm.ainvoke(prompt)\n response_text = response.content if hasattr(response, \"content\") else str(response)\n\n fn = self._parse_lambda_from_response(response_text)\n return fn(data)\n\n def _handle_process_error(self, error: Exception, output_type: str) -> None:\n \"\"\"Handle errors from process methods with context-aware messages.\"\"\"\n input_type = self._get_input_type_name()\n error_msg = (\n f\"Failed to convert result to {output_type} output. \"\n f\"Error: {error}. \"\n f\"Input type was {input_type}. \"\n f\"Try using the same output type as the input.\"\n )\n raise ValueError(error_msg) from error\n\n def _convert_result_to_data(self, result: Any) -> Data:\n \"\"\"Convert lambda result to Data object.\"\"\"\n if isinstance(result, dict):\n return Data(data=result)\n if isinstance(result, list):\n return Data(data={\"_results\": result})\n return Data(data={\"text\": str(result)})\n\n def _convert_result_to_dataframe(self, result: Any) -> DataFrame:\n \"\"\"Convert lambda result to DataFrame object.\"\"\"\n if isinstance(result, list):\n if all(isinstance(item, dict) for item in result):\n return DataFrame(result)\n return DataFrame([{\"value\": item} for item in result])\n if isinstance(result, dict):\n return DataFrame([result])\n return DataFrame([{\"value\": str(result)}])\n\n def _convert_result_to_message(self, result: Any) -> Message:\n \"\"\"Convert lambda result to Message object.\"\"\"\n if isinstance(result, str):\n return Message(text=result, sender=MESSAGE_SENDER_AI)\n if isinstance(result, list):\n text = \"\\n\".join(str(item) for item in result)\n return Message(text=text, sender=MESSAGE_SENDER_AI)\n if isinstance(result, dict):\n text = json.dumps(result, indent=2)\n return Message(text=text, sender=MESSAGE_SENDER_AI)\n return Message(text=str(result), sender=MESSAGE_SENDER_AI)\n\n async def process_as_data(self) -> Data:\n \"\"\"Process the data and return as a Data object.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_data(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"Data\")\n\n async def process_as_dataframe(self) -> DataFrame:\n \"\"\"Process the data and return as a DataFrame.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_dataframe(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"DataFrame\")\n\n async def process_as_message(self) -> Message:\n \"\"\"Process the data and return as a Message.\"\"\"\n try:\n result = await self._execute_lambda()\n return self._convert_result_to_message(result)\n except Exception as e: # noqa: BLE001 - dynamic lambda can raise any exception\n self._handle_process_error(e, \"Message\")\n" }, "data": { "_input_type": "JSONInput", @@ -88356,7 +88356,7 @@ "icon": "route", "legacy": false, "metadata": { - "code_hash": "7bdefeec6280", + "code_hash": "86404d4beb95", "dependencies": { "dependencies": [ { @@ -88410,7 +88410,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom lfx.base.models.unified_models import (\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.custom import Component\nfrom lfx.io import (\n BoolInput,\n MessageInput,\n MessageTextInput,\n ModelInput,\n MultilineInput,\n Output,\n SecretStrInput,\n TableInput,\n)\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\nclass SmartRouterComponent(Component):\n display_name = \"Smart Router\"\n description = \"Routes an input message using LLM-based categorization.\"\n icon = \"route\"\n name = \"SmartRouter\"\n\n def __init__(self, **kwargs):\n super().__init__(**kwargs)\n self._matched_category = None\n self._categorization_result: str | None = None\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"input_text\",\n display_name=\"Input\",\n info=\"The primary text input for the operation.\",\n required=True,\n ),\n TableInput(\n name=\"routes\",\n display_name=\"Routes\",\n info=(\n \"Define the categories for routing. Each row should have a route/category name \"\n \"and optionally a custom output value.\"\n ),\n table_schema=[\n {\n \"name\": \"route_category\",\n \"display_name\": \"Route Name\",\n \"type\": \"str\",\n \"description\": \"Name for the route (used for both output name and category matching)\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"route_description\",\n \"display_name\": \"Route Description\",\n \"type\": \"str\",\n \"description\": \"Description of when this route should be used (helps LLM understand the category)\",\n \"default\": \"\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"output_value\",\n \"display_name\": \"Route Message (Optional)\",\n \"type\": \"str\",\n \"description\": (\n \"Optional message to send when this route is matched.\"\n \"Leave empty to pass through the original input text.\"\n ),\n \"default\": \"\",\n \"edit_mode\": EditMode.POPOVER,\n },\n ],\n value=[\n {\n \"route_category\": \"Positive\",\n \"route_description\": \"Positive feedback, satisfaction, or compliments\",\n \"output_value\": \"\",\n },\n {\n \"route_category\": \"Negative\",\n \"route_description\": \"Complaints, issues, or dissatisfaction\",\n \"output_value\": \"\",\n },\n ],\n real_time_refresh=True,\n required=True,\n ),\n MessageInput(\n name=\"message\",\n display_name=\"Override Output\",\n info=(\n \"Optional override message that will replace both the Input and Output Value \"\n \"for all routes when filled.\"\n ),\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"enable_else_output\",\n display_name=\"Include Else Output\",\n info=\"Include an Else output for cases that don't match any route.\",\n value=False,\n advanced=True,\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"custom_prompt\",\n display_name=\"Additional Instructions\",\n info=(\n \"Additional instructions for LLM-based categorization. \"\n \"These will be added to the base prompt. \"\n \"Use {input_text} for the input text and {routes} for the available categories.\"\n ),\n advanced=True,\n ),\n ]\n\n outputs: list[Output] = []\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n return update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Create a dynamic output for each category in the categories table.\"\"\"\n if field_name in {\"routes\", \"enable_else_output\", \"model\"}:\n frontend_node[\"outputs\"] = []\n\n # Get the routes data - either from field_value (if routes field) or from component state\n routes_data = field_value if field_name == \"routes\" else getattr(self, \"routes\", [])\n\n # Add a dynamic output for each category - all using the same method\n for i, row in enumerate(routes_data):\n route_category = row.get(\"route_category\", f\"Category {i + 1}\")\n frontend_node[\"outputs\"].append(\n Output(\n display_name=route_category,\n name=f\"category_{i + 1}_result\",\n method=\"process_case\",\n group_outputs=True,\n )\n )\n # Add default output only if enabled\n if field_name == \"enable_else_output\":\n enable_else = field_value\n else:\n enable_else = getattr(self, \"enable_else_output\", False)\n\n if enable_else:\n frontend_node[\"outputs\"].append(\n Output(display_name=\"Else\", name=\"default_result\", method=\"default_response\", group_outputs=True)\n )\n return frontend_node\n\n def _get_categorization(self) -> str:\n \"\"\"Perform LLM categorization and cache the result.\n\n This ensures the LLM is called only once per component execution,\n regardless of how many outputs are connected.\n \"\"\"\n # Return cached result if available\n if self._categorization_result is not None:\n return self._categorization_result\n\n categories = getattr(self, \"routes\", [])\n input_text = getattr(self, \"input_text\", \"\")\n llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)\n\n if not llm or not categories:\n self.status = \"No LLM provided for categorization\"\n self._categorization_result = \"NONE\"\n return self._categorization_result\n\n # Create prompt for categorization\n category_info = []\n for i, category in enumerate(categories):\n cat_name = category.get(\"route_category\", f\"Category {i + 1}\")\n cat_desc = category.get(\"route_description\", \"\")\n if cat_desc and cat_desc.strip():\n category_info.append(f'\"{cat_name}\": {cat_desc}')\n else:\n category_info.append(f'\"{cat_name}\"')\n\n categories_text = \"\\n\".join([f\"- {info}\" for info in category_info if info])\n\n # Create base prompt\n base_prompt = (\n f\"You are a text classifier. Given the following text and categories, \"\n f\"determine which category best matches the text.\\n\\n\"\n f'Text to classify: \"{input_text}\"\\n\\n'\n f\"Available categories:\\n{categories_text}\\n\\n\"\n f\"Respond with ONLY the exact category name that best matches the text. \"\n f'If none match well, respond with \"NONE\".\\n\\n'\n f\"Category:\"\n )\n\n # Use custom prompt as additional instructions if provided\n custom_prompt = getattr(self, \"custom_prompt\", \"\")\n if custom_prompt and custom_prompt.strip():\n self.status = \"Using custom prompt as additional instructions\"\n simple_routes = \", \".join(\n [f'\"{cat.get(\"route_category\", f\"Category {i + 1}\")}\"' for i, cat in enumerate(categories)]\n )\n formatted_custom = custom_prompt.format(input_text=input_text, routes=simple_routes)\n prompt = f\"{base_prompt}\\n\\nAdditional Instructions:\\n{formatted_custom}\"\n else:\n self.status = \"Using default prompt for LLM categorization\"\n prompt = base_prompt\n\n self.status = f\"Prompt sent to LLM:\\n{prompt}\"\n\n try:\n if hasattr(llm, \"invoke\"):\n response = llm.invoke(prompt)\n if hasattr(response, \"content\"):\n categorization = response.content.strip().strip('\"')\n else:\n categorization = str(response).strip().strip('\"')\n else:\n categorization = str(llm(prompt)).strip().strip('\"')\n\n self.status = f\"LLM response: '{categorization}'\"\n self._categorization_result = categorization\n except RuntimeError as e:\n self.status = f\"Error in LLM categorization: {e!s}\"\n self._categorization_result = \"NONE\"\n\n return self._categorization_result\n\n def process_case(self) -> Message:\n \"\"\"Process all categories using LLM categorization and return message for matching category.\"\"\"\n # Clear any previous match state (only on first call)\n if self._categorization_result is None:\n self._matched_category = None\n\n # Get categories and input text\n categories = getattr(self, \"routes\", [])\n input_text = getattr(self, \"input_text\", \"\")\n\n # Get the cached categorization result (performs LLM call only once)\n categorization = self._get_categorization()\n\n # Find matching category based on LLM response\n matched_category = None\n for i, category in enumerate(categories):\n route_category = category.get(\"route_category\", \"\")\n if categorization.lower() == route_category.lower():\n matched_category = i\n self.status = f\"MATCH FOUND! Category {i + 1} matched with '{categorization}'\"\n break\n\n if matched_category is not None:\n # Store the matched category for other outputs to check\n self._matched_category = matched_category\n\n # Stop all category outputs except the matched one\n for i in range(len(categories)):\n if i != matched_category:\n self.stop(f\"category_{i + 1}_result\")\n\n # Also stop the default output (if it exists)\n enable_else = getattr(self, \"enable_else_output\", False)\n if enable_else:\n self.stop(\"default_result\")\n\n route_category = categories[matched_category].get(\"route_category\", f\"Category {matched_category + 1}\")\n self.status = f\"Categorized as {route_category}\"\n\n # Check if there's an override output (takes precedence over everything)\n override_output = getattr(self, \"message\", None)\n if (\n override_output\n and hasattr(override_output, \"text\")\n and override_output.text\n and str(override_output.text).strip()\n ):\n return Message(text=str(override_output.text))\n if override_output and isinstance(override_output, str) and override_output.strip():\n return Message(text=str(override_output))\n\n # Check if there's a custom output value for this category\n custom_output = categories[matched_category].get(\"output_value\", \"\")\n # Treat None, empty string, or whitespace as blank\n if custom_output and str(custom_output).strip() and str(custom_output).strip().lower() != \"none\":\n # Use custom output value\n return Message(text=str(custom_output))\n # Use input as default output\n return Message(text=input_text)\n # No match found, stop all category outputs\n for i in range(len(categories)):\n self.stop(f\"category_{i + 1}_result\")\n\n # Check if else output is enabled\n enable_else = getattr(self, \"enable_else_output\", False)\n if enable_else:\n # The default_response will handle the else case\n self.stop(\"process_case\")\n return Message(text=\"\")\n # No else output, so no output at all\n self.status = \"No match found and Else output is disabled\"\n return Message(text=\"\")\n\n def default_response(self) -> Message:\n \"\"\"Handle the else case when no conditions match.\"\"\"\n enable_else = getattr(self, \"enable_else_output\", False)\n if not enable_else:\n self.status = \"Else output is disabled\"\n return Message(text=\"\")\n\n categories = getattr(self, \"routes\", [])\n input_text = getattr(self, \"input_text\", \"\")\n\n # Get the cached categorization result (performs LLM call only if not already done)\n categorization = self._get_categorization()\n\n # Check if the categorization matches any category\n has_match = False\n for i, category in enumerate(categories):\n route_category = category.get(\"route_category\", \"\")\n if categorization.lower() == route_category.lower():\n has_match = True\n self.status = f\"Match found for '{categorization}' (Category {i + 1}), stopping default_response\"\n break\n\n if has_match:\n # A case matches, stop this output\n self.stop(\"default_result\")\n return Message(text=\"\")\n\n # No case matches, check for override output first, then use input as default\n override_output = getattr(self, \"message\", None)\n if (\n override_output\n and hasattr(override_output, \"text\")\n and override_output.text\n and str(override_output.text).strip()\n ):\n self.status = \"Routed to Else (no match) - using override output\"\n return Message(text=str(override_output.text))\n if override_output and isinstance(override_output, str) and override_output.strip():\n self.status = \"Routed to Else (no match) - using override output\"\n return Message(text=str(override_output))\n\n self.status = \"Routed to Else (no match) - using input as default\"\n return Message(text=input_text)\n" + "value": "from typing import Any\n\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.custom import Component\nfrom lfx.io import (\n BoolInput,\n MessageInput,\n MessageTextInput,\n ModelInput,\n MultilineInput,\n Output,\n SecretStrInput,\n TableInput,\n)\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\nclass SmartRouterComponent(Component):\n display_name = \"Smart Router\"\n description = \"Routes an input message using LLM-based categorization.\"\n icon = \"route\"\n name = \"SmartRouter\"\n\n def __init__(self, **kwargs):\n super().__init__(**kwargs)\n self._matched_category = None\n self._categorization_result: str | None = None\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"input_text\",\n display_name=\"Input\",\n info=\"The primary text input for the operation.\",\n required=True,\n ),\n TableInput(\n name=\"routes\",\n display_name=\"Routes\",\n info=(\n \"Define the categories for routing. Each row should have a route/category name \"\n \"and optionally a custom output value.\"\n ),\n table_schema=[\n {\n \"name\": \"route_category\",\n \"display_name\": \"Route Name\",\n \"type\": \"str\",\n \"description\": \"Name for the route (used for both output name and category matching)\",\n \"edit_mode\": EditMode.INLINE,\n },\n {\n \"name\": \"route_description\",\n \"display_name\": \"Route Description\",\n \"type\": \"str\",\n \"description\": \"Description of when this route should be used (helps LLM understand the category)\",\n \"default\": \"\",\n \"edit_mode\": EditMode.POPOVER,\n },\n {\n \"name\": \"output_value\",\n \"display_name\": \"Route Message (Optional)\",\n \"type\": \"str\",\n \"description\": (\n \"Optional message to send when this route is matched.\"\n \"Leave empty to pass through the original input text.\"\n ),\n \"default\": \"\",\n \"edit_mode\": EditMode.POPOVER,\n },\n ],\n value=[\n {\n \"route_category\": \"Positive\",\n \"route_description\": \"Positive feedback, satisfaction, or compliments\",\n \"output_value\": \"\",\n },\n {\n \"route_category\": \"Negative\",\n \"route_description\": \"Complaints, issues, or dissatisfaction\",\n \"output_value\": \"\",\n },\n ],\n real_time_refresh=True,\n required=True,\n ),\n MessageInput(\n name=\"message\",\n display_name=\"Override Output\",\n info=(\n \"Optional override message that will replace both the Input and Output Value \"\n \"for all routes when filled.\"\n ),\n required=False,\n advanced=True,\n ),\n BoolInput(\n name=\"enable_else_output\",\n display_name=\"Include Else Output\",\n info=\"Include an Else output for cases that don't match any route.\",\n value=False,\n advanced=True,\n real_time_refresh=True,\n ),\n MultilineInput(\n name=\"custom_prompt\",\n display_name=\"Additional Instructions\",\n info=(\n \"Additional instructions for LLM-based categorization. \"\n \"These will be added to the base prompt. \"\n \"Use {input_text} for the input text and {routes} for the available categories.\"\n ),\n advanced=True,\n ),\n ]\n\n outputs: list[Output] = []\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n\n def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict:\n \"\"\"Create a dynamic output for each category in the categories table.\"\"\"\n if field_name in {\"routes\", \"enable_else_output\", \"model\"}:\n frontend_node[\"outputs\"] = []\n\n # Get the routes data - either from field_value (if routes field) or from component state\n routes_data = field_value if field_name == \"routes\" else getattr(self, \"routes\", [])\n\n # Add a dynamic output for each category - all using the same method\n for i, row in enumerate(routes_data):\n route_category = row.get(\"route_category\", f\"Category {i + 1}\")\n frontend_node[\"outputs\"].append(\n Output(\n display_name=route_category,\n name=f\"category_{i + 1}_result\",\n method=\"process_case\",\n group_outputs=True,\n )\n )\n # Add default output only if enabled\n if field_name == \"enable_else_output\":\n enable_else = field_value\n else:\n enable_else = getattr(self, \"enable_else_output\", False)\n\n if enable_else:\n frontend_node[\"outputs\"].append(\n Output(display_name=\"Else\", name=\"default_result\", method=\"default_response\", group_outputs=True)\n )\n return frontend_node\n\n def _get_categorization(self) -> str:\n \"\"\"Perform LLM categorization and cache the result.\n\n This ensures the LLM is called only once per component execution,\n regardless of how many outputs are connected.\n \"\"\"\n # Return cached result if available\n if self._categorization_result is not None:\n return self._categorization_result\n\n categories = getattr(self, \"routes\", [])\n input_text = getattr(self, \"input_text\", \"\")\n llm = get_llm(model=self.model, user_id=self.user_id, api_key=self.api_key)\n\n if not llm or not categories:\n self.status = \"No LLM provided for categorization\"\n self._categorization_result = \"NONE\"\n return self._categorization_result\n\n # Create prompt for categorization\n category_info = []\n for i, category in enumerate(categories):\n cat_name = category.get(\"route_category\", f\"Category {i + 1}\")\n cat_desc = category.get(\"route_description\", \"\")\n if cat_desc and cat_desc.strip():\n category_info.append(f'\"{cat_name}\": {cat_desc}')\n else:\n category_info.append(f'\"{cat_name}\"')\n\n categories_text = \"\\n\".join([f\"- {info}\" for info in category_info if info])\n\n # Create base prompt\n base_prompt = (\n f\"You are a text classifier. Given the following text and categories, \"\n f\"determine which category best matches the text.\\n\\n\"\n f'Text to classify: \"{input_text}\"\\n\\n'\n f\"Available categories:\\n{categories_text}\\n\\n\"\n f\"Respond with ONLY the exact category name that best matches the text. \"\n f'If none match well, respond with \"NONE\".\\n\\n'\n f\"Category:\"\n )\n\n # Use custom prompt as additional instructions if provided\n custom_prompt = getattr(self, \"custom_prompt\", \"\")\n if custom_prompt and custom_prompt.strip():\n self.status = \"Using custom prompt as additional instructions\"\n simple_routes = \", \".join(\n [f'\"{cat.get(\"route_category\", f\"Category {i + 1}\")}\"' for i, cat in enumerate(categories)]\n )\n formatted_custom = custom_prompt.format(input_text=input_text, routes=simple_routes)\n prompt = f\"{base_prompt}\\n\\nAdditional Instructions:\\n{formatted_custom}\"\n else:\n self.status = \"Using default prompt for LLM categorization\"\n prompt = base_prompt\n\n self.status = f\"Prompt sent to LLM:\\n{prompt}\"\n\n try:\n if hasattr(llm, \"invoke\"):\n response = llm.invoke(prompt)\n if hasattr(response, \"content\"):\n categorization = response.content.strip().strip('\"')\n else:\n categorization = str(response).strip().strip('\"')\n else:\n categorization = str(llm(prompt)).strip().strip('\"')\n\n self.status = f\"LLM response: '{categorization}'\"\n self._categorization_result = categorization\n except RuntimeError as e:\n self.status = f\"Error in LLM categorization: {e!s}\"\n self._categorization_result = \"NONE\"\n\n return self._categorization_result\n\n def process_case(self) -> Message:\n \"\"\"Process all categories using LLM categorization and return message for matching category.\"\"\"\n # Clear any previous match state (only on first call)\n if self._categorization_result is None:\n self._matched_category = None\n\n # Get categories and input text\n categories = getattr(self, \"routes\", [])\n input_text = getattr(self, \"input_text\", \"\")\n\n # Get the cached categorization result (performs LLM call only once)\n categorization = self._get_categorization()\n\n # Find matching category based on LLM response\n matched_category = None\n for i, category in enumerate(categories):\n route_category = category.get(\"route_category\", \"\")\n if categorization.lower() == route_category.lower():\n matched_category = i\n self.status = f\"MATCH FOUND! Category {i + 1} matched with '{categorization}'\"\n break\n\n if matched_category is not None:\n # Store the matched category for other outputs to check\n self._matched_category = matched_category\n\n # Stop all category outputs except the matched one\n for i in range(len(categories)):\n if i != matched_category:\n self.stop(f\"category_{i + 1}_result\")\n\n # Also stop the default output (if it exists)\n enable_else = getattr(self, \"enable_else_output\", False)\n if enable_else:\n self.stop(\"default_result\")\n\n route_category = categories[matched_category].get(\"route_category\", f\"Category {matched_category + 1}\")\n self.status = f\"Categorized as {route_category}\"\n\n # Check if there's an override output (takes precedence over everything)\n override_output = getattr(self, \"message\", None)\n if (\n override_output\n and hasattr(override_output, \"text\")\n and override_output.text\n and str(override_output.text).strip()\n ):\n return Message(text=str(override_output.text))\n if override_output and isinstance(override_output, str) and override_output.strip():\n return Message(text=str(override_output))\n\n # Check if there's a custom output value for this category\n custom_output = categories[matched_category].get(\"output_value\", \"\")\n # Treat None, empty string, or whitespace as blank\n if custom_output and str(custom_output).strip() and str(custom_output).strip().lower() != \"none\":\n # Use custom output value\n return Message(text=str(custom_output))\n # Use input as default output\n return Message(text=input_text)\n # No match found, stop all category outputs\n for i in range(len(categories)):\n self.stop(f\"category_{i + 1}_result\")\n\n # Check if else output is enabled\n enable_else = getattr(self, \"enable_else_output\", False)\n if enable_else:\n # The default_response will handle the else case\n self.stop(\"process_case\")\n return Message(text=\"\")\n # No else output, so no output at all\n self.status = \"No match found and Else output is disabled\"\n return Message(text=\"\")\n\n def default_response(self) -> Message:\n \"\"\"Handle the else case when no conditions match.\"\"\"\n enable_else = getattr(self, \"enable_else_output\", False)\n if not enable_else:\n self.status = \"Else output is disabled\"\n return Message(text=\"\")\n\n categories = getattr(self, \"routes\", [])\n input_text = getattr(self, \"input_text\", \"\")\n\n # Get the cached categorization result (performs LLM call only if not already done)\n categorization = self._get_categorization()\n\n # Check if the categorization matches any category\n has_match = False\n for i, category in enumerate(categories):\n route_category = category.get(\"route_category\", \"\")\n if categorization.lower() == route_category.lower():\n has_match = True\n self.status = f\"Match found for '{categorization}' (Category {i + 1}), stopping default_response\"\n break\n\n if has_match:\n # A case matches, stop this output\n self.stop(\"default_result\")\n return Message(text=\"\")\n\n # No case matches, check for override output first, then use input as default\n override_output = getattr(self, \"message\", None)\n if (\n override_output\n and hasattr(override_output, \"text\")\n and override_output.text\n and str(override_output.text).strip()\n ):\n self.status = \"Routed to Else (no match) - using override output\"\n return Message(text=str(override_output.text))\n if override_output and isinstance(override_output, str) and override_output.strip():\n self.status = \"Routed to Else (no match) - using override output\"\n return Message(text=str(override_output))\n\n self.status = \"Routed to Else (no match) - using input as default\"\n return Message(text=input_text)\n" }, "custom_prompt": { "_input_type": "MultilineInput", @@ -91188,7 +91188,7 @@ "icon": "bot", "legacy": false, "metadata": { - "code_hash": "108da32d83f1", + "code_hash": "40d1976f4718", "dependencies": { "dependencies": [ { @@ -91346,7 +91346,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 max_tokens_val = getattr(self, \"max_tokens\", None)\n if max_tokens_val in {\"\", 0}:\n max_tokens_val = None\n llm_model = get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n max_tokens=max_tokens_val,\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n )\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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n # Iterate over all providers in the MODEL_PROVIDERS_DICT\n if field_name == \"model\":\n # Update input types for all fields\n build_config = self.update_input_types(build_config)\n\n # Show/hide provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Hide provider-specific fields by default before applying provider config\n for field in [\"base_url_ibm_watsonx\", \"project_id\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Apply provider variable configuration (advanced, required, info, env var fallback)\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Validate required keys\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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" + "value": "from __future__ import annotations\n\nimport json\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom pydantic import ValidationError\n\nfrom lfx.components.models_and_agents.memory import MemoryComponent\n\nif TYPE_CHECKING:\n from langchain_core.tools import Tool\n\nfrom lfx.base.agents.agent import LCToolsAgentComponent\nfrom lfx.base.agents.events import ExceptionWithMessageError\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.components.helpers import CurrentDateComponent\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.helpers.base_model import build_model_from_schema\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.schema.data import Data\nfrom lfx.schema.dotdict import dotdict\nfrom lfx.schema.message import Message\nfrom lfx.schema.table import EditMode\n\n\ndef set_advanced_true(component_input):\n component_input.advanced = True\n return component_input\n\n\nclass AgentComponent(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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider 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 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=\"System Prompt: Initial instructions and context provided to guide the agent's behavior.\",\n value=\"You are a helpful assistant that can use tools to answer questions and perform tasks.\",\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. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, 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 *LCToolsAgentComponent.get_base_inputs(),\n # removed memory inputs from agent component\n # *memory_inputs,\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 ]\n outputs = [\n Output(name=\"response\", display_name=\"Response\", method=\"message_response\"),\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 return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=getattr(self, \"api_key\", None),\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 )\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 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 self.tools.append(current_date_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 async def message_response(self) -> Message:\n try:\n llm_model, self.chat_history, self.tools = await self.get_agent_requirements()\n # Set up and run agent\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.system_prompt,\n )\n agent = self.create_agent_runnable()\n result = await self.run_agent(agent)\n\n # Store result for potential JSON output\n self._agent_result = result\n\n except (ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"{type(e).__name__}: {e!s}\")\n raise\n except ExceptionWithMessageError as e:\n await logger.aerror(f\"ExceptionWithMessageError occurred: {e}\")\n raise\n # Avoid catching blind Exception; let truly unexpected exceptions propagate\n except Exception as e:\n await logger.aerror(f\"Unexpected error: {e!s}\")\n raise\n else:\n return result\n\n def _preprocess_schema(self, schema):\n \"\"\"Preprocess schema to ensure correct data types for build_model_from_schema.\"\"\"\n processed_schema = []\n for field in schema:\n processed_field = {\n \"name\": str(field.get(\"name\", \"field\")),\n \"type\": str(field.get(\"type\", \"str\")),\n \"description\": str(field.get(\"description\", \"\")),\n \"multiple\": field.get(\"multiple\", False),\n }\n # Ensure multiple is handled correctly\n if isinstance(processed_field[\"multiple\"], str):\n processed_field[\"multiple\"] = processed_field[\"multiple\"].lower() in [\n \"true\",\n \"1\",\n \"t\",\n \"y\",\n \"yes\",\n ]\n processed_schema.append(processed_field)\n return processed_schema\n\n async def build_structured_output_base(self, content: str):\n \"\"\"Build structured output with optional BaseModel validation.\"\"\"\n json_pattern = r\"\\{.*\\}\"\n schema_error_msg = \"Try setting an output schema\"\n\n # Try to parse content as JSON first\n json_data = None\n try:\n json_data = json.loads(content)\n except json.JSONDecodeError:\n json_match = re.search(json_pattern, content, re.DOTALL)\n if json_match:\n try:\n json_data = json.loads(json_match.group())\n except json.JSONDecodeError:\n return {\"content\": content, \"error\": schema_error_msg}\n else:\n return {\"content\": content, \"error\": schema_error_msg}\n\n # If no output schema provided, return parsed JSON without validation\n if not hasattr(self, \"output_schema\") or not self.output_schema or len(self.output_schema) == 0:\n return json_data\n\n # Use BaseModel validation with schema\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n\n # Validate against the schema\n if isinstance(json_data, list):\n # Multiple objects\n validated_objects = []\n for item in json_data:\n try:\n validated_obj = output_model.model_validate(item)\n validated_objects.append(validated_obj.model_dump())\n except ValidationError as e:\n await logger.aerror(f\"Validation error for item: {e}\")\n # Include invalid items with error info\n validated_objects.append({\"data\": item, \"validation_error\": str(e)})\n return validated_objects\n\n # Single object\n try:\n validated_obj = output_model.model_validate(json_data)\n return [validated_obj.model_dump()] # Return as list for consistency\n except ValidationError as e:\n await logger.aerror(f\"Validation error: {e}\")\n return [{\"data\": json_data, \"validation_error\": str(e)}]\n\n except (TypeError, ValueError) as e:\n await logger.aerror(f\"Error building structured output: {e}\")\n # Fallback to parsed JSON without validation\n return json_data\n\n async def json_response(self) -> Data:\n \"\"\"Convert agent response to structured JSON Data output with schema validation.\"\"\"\n # Always use structured chat agent for JSON response mode for better JSON formatting\n try:\n system_components = []\n\n # 1. Agent Instructions (system_prompt)\n agent_instructions = getattr(self, \"system_prompt\", \"\") or \"\"\n if agent_instructions:\n system_components.append(f\"{agent_instructions}\")\n\n # 2. Format Instructions\n format_instructions = getattr(self, \"format_instructions\", \"\") or \"\"\n if format_instructions:\n system_components.append(f\"Format instructions: {format_instructions}\")\n\n # 3. Schema Information from BaseModel\n if hasattr(self, \"output_schema\") and self.output_schema and len(self.output_schema) > 0:\n try:\n processed_schema = self._preprocess_schema(self.output_schema)\n output_model = build_model_from_schema(processed_schema)\n schema_dict = output_model.model_json_schema()\n schema_info = (\n \"You are given some text that may include format instructions, \"\n \"explanations, or other content alongside a JSON schema.\\n\\n\"\n \"Your task:\\n\"\n \"- Extract only the JSON schema.\\n\"\n \"- Return it as valid JSON.\\n\"\n \"- Do not include format instructions, explanations, or extra text.\\n\\n\"\n \"Input:\\n\"\n f\"{json.dumps(schema_dict, indent=2)}\\n\\n\"\n \"Output (only JSON schema):\"\n )\n system_components.append(schema_info)\n except (ValidationError, ValueError, TypeError, KeyError) as e:\n await logger.aerror(f\"Could not build schema for prompt: {e}\", exc_info=True)\n\n # Combine all components\n combined_instructions = \"\\n\\n\".join(system_components) if system_components 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=combined_instructions,\n )\n\n # Create and run structured chat agent\n try:\n structured_agent = self.create_agent_runnable()\n except (NotImplementedError, ValueError, TypeError) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n raise\n try:\n result = await self.run_agent(structured_agent)\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n RuntimeError,\n ) as e:\n await logger.aerror(f\"Error with structured agent result: {e}\")\n raise\n # Extract content from structured agent result\n if hasattr(result, \"content\"):\n content = result.content\n elif hasattr(result, \"text\"):\n content = result.text\n else:\n content = str(result)\n\n except (\n ExceptionWithMessageError,\n ValueError,\n TypeError,\n NotImplementedError,\n AttributeError,\n ) as e:\n await logger.aerror(f\"Error with structured chat agent: {e}\")\n # Fallback to regular agent\n content_str = \"No content returned from agent\"\n return Data(data={\"content\": content_str, \"error\": str(e)})\n\n # Process with structured output validation\n try:\n structured_output = await self.build_structured_output_base(content)\n\n # Handle different output formats\n if isinstance(structured_output, list) and structured_output:\n if len(structured_output) == 1:\n return Data(data=structured_output[0])\n return Data(data={\"results\": structured_output})\n if isinstance(structured_output, dict):\n return Data(data=structured_output)\n return Data(data={\"content\": content})\n\n except (ValueError, TypeError) as e:\n await logger.aerror(f\"Error in structured output processing: {e}\")\n return Data(data={\"content\": content, \"error\": str(e)})\n\n async def get_memory_data(self):\n # TODO: This is a temporary fix to avoid message duplication. We should develop a function for this.\n messages = (\n await MemoryComponent(**self.get_base_args())\n .set(\n session_id=self.graph.session_id,\n context_id=self.context_id,\n order=\"Ascending\",\n n_messages=self.n_messages,\n )\n .retrieve_messages()\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 # Update model options with caching (for all field changes)\n # Agents require tool calling, so filter for only tool-calling capable models\n def get_tool_calling_model_options(user_id=None):\n return get_language_model_options(user_id=user_id, tool_calling=True)\n\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=dict(build_config),\n cache_key_prefix=\"language_model_options_tool_calling\",\n get_options_func=get_tool_calling_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n build_config = dotdict(build_config)\n\n if field_name == \"model\":\n build_config = self.update_input_types(build_config)\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n if field_name == \"model\":\n default_keys = [\n \"code\",\n \"_type\",\n \"model\",\n \"tools\",\n \"input_value\",\n \"add_current_date_tool\",\n \"system_prompt\",\n \"agent_description\",\n \"max_iterations\",\n \"handle_parsing_errors\",\n \"verbose\",\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 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 tools_names = self._build_tools_names()\n agent_description = self.get_tool_description()\n # TODO: Agent Description Depreciated Feature to be removed\n description = f\"{agent_description}{tools_names}\"\n\n tools = component_toolkit(component=self).get_tools(\n tool_name=\"Call_Agent\",\n tool_description=description,\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", @@ -91743,7 +91743,7 @@ "icon": "binary", "legacy": false, "metadata": { - "code_hash": "c5ce0982da48", + "code_hash": "b5cf1a06bba8", "dependencies": { "dependencies": [ { @@ -91893,7 +91893,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from typing import Any\n\nfrom lfx.base.embeddings.embeddings_class import EmbeddingsWithModels\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.unified_models import (\n get_api_key_for_provider,\n get_embedding_class,\n get_embedding_model_options,\n get_unified_models_detailed,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n ModelInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"embedding_model_options\",\n get_options_func=get_embedding_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Show/hide provider-specific fields based on selected model\n if field_name == \"model\" and isinstance(field_value, list) and len(field_value) > 0:\n selected_model = field_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n # Show/hide watsonx fields\n is_watsonx = provider == \"IBM WatsonX\"\n build_config[\"base_url_ibm_watsonx\"][\"show\"] = is_watsonx\n build_config[\"project_id\"][\"show\"] = is_watsonx\n build_config[\"truncate_input_tokens\"][\"show\"] = is_watsonx\n build_config[\"input_text\"][\"show\"] = is_watsonx\n if is_watsonx:\n build_config[\"base_url_ibm_watsonx\"][\"required\"] = True\n build_config[\"project_id\"][\"required\"] = True\n\n return build_config\n\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Embedding Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n model_type=\"embedding\",\n input_types=[\"Embeddings\"], # Override default to accept Embeddings instead of LanguageModel\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n # Watson-specific inputs\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 show=False,\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n advanced=True,\n value=1000,\n ),\n FloatInput(\n name=\"request_timeout\",\n display_name=\"Request Timeout\",\n advanced=True,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n advanced=True,\n value=3,\n ),\n BoolInput(\n name=\"show_progress_bar\",\n display_name=\"Show Progress Bar\",\n advanced=True,\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n IntInput(\n name=\"truncate_input_tokens\",\n display_name=\"Truncate Input Tokens\",\n advanced=True,\n value=200,\n show=False,\n ),\n BoolInput(\n name=\"input_text\",\n display_name=\"Include the original text in the output\",\n value=True,\n advanced=True,\n show=False,\n ),\n ]\n\n def build_embeddings(self) -> Embeddings:\n \"\"\"Build and return an embeddings instance based on the selected model.\n\n Returns an EmbeddingsWithModels wrapper that contains:\n - The primary embedding instance (for the selected model)\n - available_models dict mapping all available model names to their instances\n \"\"\"\n # If an Embeddings object is directly connected, return it\n try:\n from langchain_core.embeddings import Embeddings as BaseEmbeddings\n\n if isinstance(self.model, BaseEmbeddings):\n return self.model\n except ImportError:\n pass\n\n # Safely extract model configuration\n if not self.model or not isinstance(self.model, list):\n msg = \"Model must be a non-empty list\"\n raise ValueError(msg)\n\n model = self.model[0]\n model_name = model.get(\"name\")\n provider = model.get(\"provider\")\n metadata = model.get(\"metadata\", {})\n\n # Get API key from user input or global variables\n api_key = get_api_key_for_provider(self.user_id, provider, self.api_key)\n\n # Validate required fields (Ollama doesn't require API key)\n if not api_key and provider != \"Ollama\":\n msg = (\n f\"{provider} API key is required. \"\n f\"Please provide it in the component or configure it globally as \"\n f\"{provider.upper().replace(' ', '_')}_API_KEY.\"\n )\n raise ValueError(msg)\n\n if not model_name:\n msg = \"Model name is required\"\n raise ValueError(msg)\n\n # Get embedding class\n embedding_class_name = metadata.get(\"embedding_class\")\n if not embedding_class_name:\n msg = f\"No embedding class defined in metadata for {model_name}\"\n raise ValueError(msg)\n\n embedding_class = get_embedding_class(embedding_class_name)\n\n # Build kwargs using parameter mapping for primary instance\n kwargs = self._build_kwargs(model, metadata)\n primary_instance = embedding_class(**kwargs)\n\n # Get all available embedding models for this provider\n available_models_dict = self._build_available_models(\n provider=provider,\n embedding_class=embedding_class,\n metadata=metadata,\n api_key=api_key,\n )\n\n # Wrap with EmbeddingsWithModels to provide available_models metadata\n return EmbeddingsWithModels(\n embeddings=primary_instance,\n available_models=available_models_dict,\n )\n\n def _build_available_models(\n self,\n provider: str,\n embedding_class: type,\n metadata: dict[str, Any],\n api_key: str | None,\n ) -> dict[str, Embeddings]:\n \"\"\"Build a dictionary of all available embedding model instances for the provider.\n\n Args:\n provider: The provider name (e.g., \"OpenAI\", \"Ollama\")\n embedding_class: The embedding class to instantiate\n metadata: Metadata containing param_mapping\n api_key: The API key for the provider\n\n Returns:\n Dict mapping model names to their embedding instances\n \"\"\"\n available_models_dict: dict[str, Embeddings] = {}\n\n # Get all embedding models for this provider from unified models\n all_embedding_models = get_unified_models_detailed(\n providers=[provider],\n model_type=\"embeddings\",\n include_deprecated=False,\n include_unsupported=False,\n )\n\n if not all_embedding_models:\n return available_models_dict\n\n # Extract models from the provider data\n for provider_data in all_embedding_models:\n if provider_data.get(\"provider\") != provider:\n continue\n\n for model_data in provider_data.get(\"models\", []):\n model_name = model_data.get(\"model_name\")\n if not model_name:\n continue\n\n # Create a model dict compatible with _build_kwargs\n model_dict = {\n \"name\": model_name,\n \"provider\": provider,\n \"metadata\": metadata, # Reuse the same metadata/param_mapping\n }\n\n try:\n # Build kwargs for this model\n model_kwargs = self._build_kwargs_for_model(model_dict, metadata, api_key)\n # Create the embedding instance\n available_models_dict[model_name] = embedding_class(**model_kwargs)\n except Exception: # noqa: BLE001\n # Skip models that fail to instantiate\n # This handles cases where specific models have incompatible parameters\n logger.debug(\"Failed to instantiate embedding model %s: skipping\", model_name, exc_info=True)\n continue\n\n return available_models_dict\n\n def _build_kwargs_for_model(\n self,\n model: dict[str, Any],\n metadata: dict[str, Any],\n api_key: str | None,\n ) -> dict[str, Any]:\n \"\"\"Build kwargs dictionary for a specific model using parameter mapping.\n\n This is similar to _build_kwargs but uses the provided api_key directly\n instead of looking it up again.\n\n Args:\n model: Model dict with name and provider\n metadata: Metadata containing param_mapping\n api_key: The API key to use\n\n Returns:\n kwargs dict for embedding class instantiation\n \"\"\"\n param_mapping = metadata.get(\"param_mapping\", {})\n if not param_mapping:\n msg = \"Parameter mapping not found in metadata\"\n raise ValueError(msg)\n\n kwargs = {}\n provider = model.get(\"provider\")\n\n # Required parameters - handle both \"model\" and \"model_id\" (for watsonx)\n if \"model\" in param_mapping:\n kwargs[param_mapping[\"model\"]] = model.get(\"name\")\n elif \"model_id\" in param_mapping:\n kwargs[param_mapping[\"model_id\"]] = model.get(\"name\")\n\n # Add API key if mapped\n if \"api_key\" in param_mapping and api_key:\n kwargs[param_mapping[\"api_key\"]] = api_key\n\n # Optional parameters with their values\n optional_params = {\n \"api_base\": self.api_base if self.api_base else None,\n \"dimensions\": int(self.dimensions) if self.dimensions else None,\n \"chunk_size\": int(self.chunk_size) if self.chunk_size else None,\n \"request_timeout\": float(self.request_timeout) if self.request_timeout else None,\n \"max_retries\": int(self.max_retries) if self.max_retries else None,\n \"show_progress_bar\": self.show_progress_bar if hasattr(self, \"show_progress_bar\") else None,\n \"model_kwargs\": self.model_kwargs if self.model_kwargs else None,\n }\n\n # Watson-specific parameters\n if provider in {\"IBM WatsonX\", \"IBM watsonx.ai\"}:\n # Map base_url_ibm_watsonx to \"url\" parameter for watsonx\n if \"url\" in param_mapping:\n url_value = (\n self.base_url_ibm_watsonx\n if hasattr(self, \"base_url_ibm_watsonx\") and self.base_url_ibm_watsonx\n else \"https://us-south.ml.cloud.ibm.com\"\n )\n kwargs[param_mapping[\"url\"]] = url_value\n # Map project_id for watsonx\n if hasattr(self, \"project_id\") and self.project_id and \"project_id\" in param_mapping:\n kwargs[param_mapping[\"project_id\"]] = self.project_id\n\n # Ollama-specific parameters\n if provider == \"Ollama\" and \"base_url\" in param_mapping:\n # Map api_base to \"base_url\" parameter for Ollama\n base_url_value = self.api_base if hasattr(self, \"api_base\") and self.api_base else \"http://localhost:11434\"\n kwargs[param_mapping[\"base_url\"]] = base_url_value\n\n # Add optional parameters if they have values and are mapped\n for param_name, param_value in optional_params.items():\n if param_value is not None and param_name in param_mapping:\n # Special handling for request_timeout with Google provider\n if param_name == \"request_timeout\":\n if provider == \"Google Generative AI\" and isinstance(param_value, (int, float)):\n kwargs[param_mapping[param_name]] = {\"timeout\": param_value}\n else:\n kwargs[param_mapping[param_name]] = param_value\n else:\n kwargs[param_mapping[param_name]] = param_value\n\n return kwargs\n\n def _build_kwargs(self, model: dict[str, Any], metadata: dict[str, Any]) -> dict[str, Any]:\n \"\"\"Build kwargs dictionary using parameter mapping.\"\"\"\n param_mapping = metadata.get(\"param_mapping\", {})\n if not param_mapping:\n msg = \"Parameter mapping not found in metadata\"\n raise ValueError(msg)\n\n kwargs = {}\n\n # Required parameters - handle both \"model\" and \"model_id\" (for watsonx)\n if \"model\" in param_mapping:\n kwargs[param_mapping[\"model\"]] = model.get(\"name\")\n elif \"model_id\" in param_mapping:\n kwargs[param_mapping[\"model_id\"]] = model.get(\"name\")\n if \"api_key\" in param_mapping:\n kwargs[param_mapping[\"api_key\"]] = get_api_key_for_provider(\n self.user_id,\n model.get(\"provider\"),\n self.api_key,\n )\n\n # Optional parameters with their values\n provider = model.get(\"provider\")\n optional_params = {\n \"api_base\": self.api_base if self.api_base else None,\n \"dimensions\": int(self.dimensions) if self.dimensions else None,\n \"chunk_size\": int(self.chunk_size) if self.chunk_size else None,\n \"request_timeout\": float(self.request_timeout) if self.request_timeout else None,\n \"max_retries\": int(self.max_retries) if self.max_retries else None,\n \"show_progress_bar\": self.show_progress_bar if hasattr(self, \"show_progress_bar\") else None,\n \"model_kwargs\": self.model_kwargs if self.model_kwargs else None,\n }\n\n # Watson-specific parameters\n if provider in {\"IBM WatsonX\", \"IBM watsonx.ai\"}:\n # Map base_url_ibm_watsonx to \"url\" parameter for watsonx\n if \"url\" in param_mapping:\n url_value = (\n self.base_url_ibm_watsonx\n if hasattr(self, \"base_url_ibm_watsonx\") and self.base_url_ibm_watsonx\n else \"https://us-south.ml.cloud.ibm.com\"\n )\n kwargs[param_mapping[\"url\"]] = url_value\n # Map project_id for watsonx\n if hasattr(self, \"project_id\") and self.project_id and \"project_id\" in param_mapping:\n kwargs[param_mapping[\"project_id\"]] = self.project_id\n\n # Ollama-specific parameters\n if provider == \"Ollama\" and \"base_url\" in param_mapping:\n # Map api_base to \"base_url\" parameter for Ollama\n base_url_value = self.api_base if hasattr(self, \"api_base\") and self.api_base else \"http://localhost:11434\"\n kwargs[param_mapping[\"base_url\"]] = base_url_value\n\n # Add optional parameters if they have values and are mapped\n for param_name, param_value in optional_params.items():\n if param_value is not None and param_name in param_mapping:\n # Special handling for request_timeout with Google provider\n if param_name == \"request_timeout\":\n if provider == \"Google Generative AI\" and isinstance(param_value, (int, float)):\n kwargs[param_mapping[param_name]] = {\"timeout\": param_value}\n else:\n kwargs[param_mapping[param_name]] = param_value\n else:\n kwargs[param_mapping[param_name]] = param_value\n\n return kwargs\n" + "value": "from typing import Any\n\nfrom lfx.base.embeddings.embeddings_class import EmbeddingsWithModels\nfrom lfx.base.embeddings.model import LCEmbeddingsModel\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_api_key_for_provider,\n get_embedding_class,\n get_embedding_model_options,\n get_provider_for_model_name,\n get_unified_models_detailed,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import Embeddings\nfrom lfx.io import (\n BoolInput,\n DictInput,\n DropdownInput,\n FloatInput,\n IntInput,\n MessageTextInput,\n ModelInput,\n SecretStrInput,\n)\nfrom lfx.log.logger import logger\n\n\nclass EmbeddingModelComponent(LCEmbeddingsModel):\n display_name = \"Embedding Model\"\n description = \"Generate embeddings using a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-embedding-models\"\n icon = \"binary\"\n name = \"EmbeddingModel\"\n category = \"models\"\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"embedding_model_options\",\n get_options_func=get_embedding_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n # Embedding-specific WatsonX toggles not covered by provider metadata\n is_watsonx = provider == \"IBM WatsonX\"\n if \"truncate_input_tokens\" in build_config:\n build_config[\"truncate_input_tokens\"][\"show\"] = is_watsonx\n if \"input_text\" in build_config:\n build_config[\"input_text\"][\"show\"] = is_watsonx\n\n return build_config\n\n inputs = [\n ModelInput(\n name=\"model\",\n display_name=\"Embedding Model\",\n info=\"Select your model provider\",\n real_time_refresh=True,\n required=True,\n model_type=\"embedding\",\n input_types=[\"Embeddings\"], # Override default to accept Embeddings instead of LanguageModel\n ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n real_time_refresh=True,\n advanced=True,\n ),\n MessageTextInput(\n name=\"api_base\",\n display_name=\"API Base URL\",\n info=\"Base URL for the API. Leave empty for default.\",\n advanced=True,\n ),\n # Watson-specific inputs\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 show=False,\n real_time_refresh=True,\n ),\n MessageTextInput(\n name=\"project_id\",\n display_name=\"Project ID\",\n info=\"IBM watsonx.ai Project ID (required for IBM watsonx.ai)\",\n show=False,\n ),\n IntInput(\n name=\"dimensions\",\n display_name=\"Dimensions\",\n info=\"The number of dimensions the resulting output embeddings should have. \"\n \"Only supported by certain models.\",\n advanced=True,\n ),\n IntInput(\n name=\"chunk_size\",\n display_name=\"Chunk Size\",\n advanced=True,\n value=1000,\n ),\n FloatInput(\n name=\"request_timeout\",\n display_name=\"Request Timeout\",\n advanced=True,\n ),\n IntInput(\n name=\"max_retries\",\n display_name=\"Max Retries\",\n advanced=True,\n value=3,\n ),\n BoolInput(\n name=\"show_progress_bar\",\n display_name=\"Show Progress Bar\",\n advanced=True,\n ),\n DictInput(\n name=\"model_kwargs\",\n display_name=\"Model Kwargs\",\n advanced=True,\n info=\"Additional keyword arguments to pass to the model.\",\n ),\n IntInput(\n name=\"truncate_input_tokens\",\n display_name=\"Truncate Input Tokens\",\n advanced=True,\n value=200,\n show=False,\n ),\n BoolInput(\n name=\"input_text\",\n display_name=\"Include the original text in the output\",\n value=True,\n advanced=True,\n show=False,\n ),\n ]\n\n def build_embeddings(self) -> Embeddings:\n \"\"\"Build and return an embeddings instance based on the selected model.\n\n Returns an EmbeddingsWithModels wrapper that contains:\n - The primary embedding instance (for the selected model)\n - available_models dict mapping all available model names to their instances\n \"\"\"\n # If an Embeddings object is directly connected, return it\n try:\n from langchain_core.embeddings import Embeddings as BaseEmbeddings\n\n if isinstance(self.model, BaseEmbeddings):\n return self.model\n except ImportError:\n pass\n\n # Safely extract model configuration\n if not self.model or not isinstance(self.model, list):\n msg = \"Model must be a non-empty list\"\n raise ValueError(msg)\n\n model = self.model[0]\n model_name = model.get(\"name\")\n provider = model.get(\"provider\")\n metadata = model.get(\"metadata\", {})\n\n # Get API key from user input or global variables\n api_key = get_api_key_for_provider(self.user_id, provider, self.api_key)\n\n # Validate required fields (Ollama doesn't require API key)\n if not api_key and provider != \"Ollama\":\n msg = (\n f\"{provider} API key is required. \"\n f\"Please provide it in the component or configure it globally as \"\n f\"{provider.upper().replace(' ', '_')}_API_KEY.\"\n )\n raise ValueError(msg)\n\n if not model_name:\n msg = \"Model name is required\"\n raise ValueError(msg)\n\n # Get embedding class\n embedding_class_name = metadata.get(\"embedding_class\")\n if not embedding_class_name:\n msg = f\"No embedding class defined in metadata for {model_name}\"\n raise ValueError(msg)\n\n embedding_class = get_embedding_class(embedding_class_name)\n\n # Build kwargs using parameter mapping for primary instance\n kwargs = self._build_kwargs(model, metadata)\n primary_instance = embedding_class(**kwargs)\n\n # Get all available embedding models for this provider\n available_models_dict = self._build_available_models(\n provider=provider,\n embedding_class=embedding_class,\n metadata=metadata,\n api_key=api_key,\n )\n\n # Wrap with EmbeddingsWithModels to provide available_models metadata\n return EmbeddingsWithModels(\n embeddings=primary_instance,\n available_models=available_models_dict,\n )\n\n def _build_available_models(\n self,\n provider: str,\n embedding_class: type,\n metadata: dict[str, Any],\n api_key: str | None,\n ) -> dict[str, Embeddings]:\n \"\"\"Build a dictionary of all available embedding model instances for the provider.\n\n Args:\n provider: The provider name (e.g., \"OpenAI\", \"Ollama\")\n embedding_class: The embedding class to instantiate\n metadata: Metadata containing param_mapping\n api_key: The API key for the provider\n\n Returns:\n Dict mapping model names to their embedding instances\n \"\"\"\n available_models_dict: dict[str, Embeddings] = {}\n\n # Get all embedding models for this provider from unified models\n all_embedding_models = get_unified_models_detailed(\n providers=[provider],\n model_type=\"embeddings\",\n include_deprecated=False,\n include_unsupported=False,\n )\n\n if not all_embedding_models:\n return available_models_dict\n\n # Extract models from the provider data\n for provider_data in all_embedding_models:\n if provider_data.get(\"provider\") != provider:\n continue\n\n for model_data in provider_data.get(\"models\", []):\n model_name = model_data.get(\"model_name\")\n if not model_name:\n continue\n\n # Create a model dict compatible with _build_kwargs\n model_dict = {\n \"name\": model_name,\n \"provider\": provider,\n \"metadata\": metadata, # Reuse the same metadata/param_mapping\n }\n\n try:\n # Build kwargs for this model\n model_kwargs = self._build_kwargs_for_model(model_dict, metadata, api_key)\n # Create the embedding instance\n available_models_dict[model_name] = embedding_class(**model_kwargs)\n except Exception: # noqa: BLE001\n # Skip models that fail to instantiate\n # This handles cases where specific models have incompatible parameters\n logger.debug(\"Failed to instantiate embedding model %s: skipping\", model_name, exc_info=True)\n continue\n\n return available_models_dict\n\n def _build_kwargs_for_model(\n self,\n model: dict[str, Any],\n metadata: dict[str, Any],\n api_key: str | None,\n ) -> dict[str, Any]:\n \"\"\"Build kwargs dictionary for a specific model using parameter mapping.\n\n This is similar to _build_kwargs but uses the provided api_key directly\n instead of looking it up again.\n\n Args:\n model: Model dict with name and provider\n metadata: Metadata containing param_mapping\n api_key: The API key to use\n\n Returns:\n kwargs dict for embedding class instantiation\n \"\"\"\n param_mapping = metadata.get(\"param_mapping\", {})\n if not param_mapping:\n msg = \"Parameter mapping not found in metadata\"\n raise ValueError(msg)\n\n kwargs = {}\n provider = model.get(\"provider\")\n\n # Required parameters - handle both \"model\" and \"model_id\" (for watsonx)\n if \"model\" in param_mapping:\n kwargs[param_mapping[\"model\"]] = model.get(\"name\")\n elif \"model_id\" in param_mapping:\n kwargs[param_mapping[\"model_id\"]] = model.get(\"name\")\n\n # Add API key if mapped\n if \"api_key\" in param_mapping and api_key:\n kwargs[param_mapping[\"api_key\"]] = api_key\n\n # Optional parameters with their values\n optional_params = {\n \"api_base\": self.api_base if self.api_base else None,\n \"dimensions\": int(self.dimensions) if self.dimensions else None,\n \"chunk_size\": int(self.chunk_size) if self.chunk_size else None,\n \"request_timeout\": float(self.request_timeout) if self.request_timeout else None,\n \"max_retries\": int(self.max_retries) if self.max_retries else None,\n \"show_progress_bar\": self.show_progress_bar if hasattr(self, \"show_progress_bar\") else None,\n \"model_kwargs\": self.model_kwargs if self.model_kwargs else None,\n }\n\n # Watson-specific parameters\n if provider in {\"IBM WatsonX\", \"IBM watsonx.ai\"}:\n # Map base_url_ibm_watsonx to \"url\" parameter for watsonx\n if \"url\" in param_mapping:\n url_value = (\n self.base_url_ibm_watsonx\n if hasattr(self, \"base_url_ibm_watsonx\") and self.base_url_ibm_watsonx\n else \"https://us-south.ml.cloud.ibm.com\"\n )\n kwargs[param_mapping[\"url\"]] = url_value\n # Map project_id for watsonx\n if hasattr(self, \"project_id\") and self.project_id and \"project_id\" in param_mapping:\n kwargs[param_mapping[\"project_id\"]] = self.project_id\n\n # Ollama-specific parameters\n if provider == \"Ollama\" and \"base_url\" in param_mapping:\n # Map api_base to \"base_url\" parameter for Ollama\n base_url_value = self.api_base if hasattr(self, \"api_base\") and self.api_base else \"http://localhost:11434\"\n kwargs[param_mapping[\"base_url\"]] = base_url_value\n\n # Add optional parameters if they have values and are mapped\n for param_name, param_value in optional_params.items():\n if param_value is not None and param_name in param_mapping:\n # Special handling for request_timeout with Google provider\n if param_name == \"request_timeout\":\n if provider == \"Google Generative AI\" and isinstance(param_value, (int, float)):\n kwargs[param_mapping[param_name]] = {\"timeout\": param_value}\n else:\n kwargs[param_mapping[param_name]] = param_value\n else:\n kwargs[param_mapping[param_name]] = param_value\n\n return kwargs\n\n def _build_kwargs(self, model: dict[str, Any], metadata: dict[str, Any]) -> dict[str, Any]:\n \"\"\"Build kwargs dictionary using parameter mapping.\"\"\"\n param_mapping = metadata.get(\"param_mapping\", {})\n if not param_mapping:\n msg = \"Parameter mapping not found in metadata\"\n raise ValueError(msg)\n\n kwargs = {}\n\n # Required parameters - handle both \"model\" and \"model_id\" (for watsonx)\n if \"model\" in param_mapping:\n kwargs[param_mapping[\"model\"]] = model.get(\"name\")\n elif \"model_id\" in param_mapping:\n kwargs[param_mapping[\"model_id\"]] = model.get(\"name\")\n if \"api_key\" in param_mapping:\n kwargs[param_mapping[\"api_key\"]] = get_api_key_for_provider(\n self.user_id,\n model.get(\"provider\"),\n self.api_key,\n )\n\n # Optional parameters with their values\n provider = model.get(\"provider\")\n optional_params = {\n \"api_base\": self.api_base if self.api_base else None,\n \"dimensions\": int(self.dimensions) if self.dimensions else None,\n \"chunk_size\": int(self.chunk_size) if self.chunk_size else None,\n \"request_timeout\": float(self.request_timeout) if self.request_timeout else None,\n \"max_retries\": int(self.max_retries) if self.max_retries else None,\n \"show_progress_bar\": self.show_progress_bar if hasattr(self, \"show_progress_bar\") else None,\n \"model_kwargs\": self.model_kwargs if self.model_kwargs else None,\n }\n\n # Watson-specific parameters\n if provider in {\"IBM WatsonX\", \"IBM watsonx.ai\"}:\n # Map base_url_ibm_watsonx to \"url\" parameter for watsonx\n if \"url\" in param_mapping:\n url_value = (\n self.base_url_ibm_watsonx\n if hasattr(self, \"base_url_ibm_watsonx\") and self.base_url_ibm_watsonx\n else \"https://us-south.ml.cloud.ibm.com\"\n )\n kwargs[param_mapping[\"url\"]] = url_value\n # Map project_id for watsonx\n if hasattr(self, \"project_id\") and self.project_id and \"project_id\" in param_mapping:\n kwargs[param_mapping[\"project_id\"]] = self.project_id\n\n # Ollama-specific parameters\n if provider == \"Ollama\" and \"base_url\" in param_mapping:\n # Map api_base to \"base_url\" parameter for Ollama\n base_url_value = self.api_base if hasattr(self, \"api_base\") and self.api_base else \"http://localhost:11434\"\n kwargs[param_mapping[\"base_url\"]] = base_url_value\n\n # Add optional parameters if they have values and are mapped\n for param_name, param_value in optional_params.items():\n if param_value is not None and param_name in param_mapping:\n # Special handling for request_timeout with Google provider\n if param_name == \"request_timeout\":\n if provider == \"Google Generative AI\" and isinstance(param_value, (int, float)):\n kwargs[param_mapping[param_name]] = {\"timeout\": param_value}\n else:\n kwargs[param_mapping[param_name]] = param_value\n else:\n kwargs[param_mapping[param_name]] = param_value\n\n return kwargs\n" }, "dimensions": { "_input_type": "IntInput", @@ -92128,7 +92128,7 @@ "icon": "brain-circuit", "legacy": false, "metadata": { - "code_hash": "4af3c0cc0dcf", + "code_hash": "e9233312b063", "dependencies": { "dependencies": [ { @@ -92249,7 +92249,7 @@ "show": true, "title_case": false, "type": "code", - "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n # Hide all provider-specific fields by default\n for field in [\"api_key\", \"base_url_ibm_watsonx\", \"project_id\", \"ollama_base_url\"]:\n if field in build_config:\n build_config[field][\"show\"] = False\n build_config[field][\"required\"] = False\n\n # Show/configure provider-specific fields based on selected model\n # Get current model value - from field_value if model is being changed, otherwise from build_config\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n if isinstance(current_model_value, list) and len(current_model_value) > 0:\n selected_model = current_model_value[0]\n provider = selected_model.get(\"provider\", \"\")\n\n if provider:\n # Apply provider variable configuration (required_for_component, advanced, env var fallback)\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" + "value": "from lfx.base.models.model import LCModelComponent\nfrom lfx.base.models.unified_models import (\n apply_provider_variable_config_to_build_config,\n get_language_model_options,\n get_llm,\n get_provider_for_model_name,\n update_model_options_in_build_config,\n)\nfrom lfx.base.models.watsonx_constants import IBM_WATSONX_URLS\nfrom lfx.field_typing import LanguageModel\nfrom lfx.field_typing.range_spec import RangeSpec\nfrom lfx.inputs.inputs import BoolInput, DropdownInput, StrInput\nfrom lfx.io import IntInput, MessageInput, ModelInput, MultilineInput, SecretStrInput, SliderInput\n\nDEFAULT_OLLAMA_URL = \"http://localhost:11434\"\n\n\nclass LanguageModelComponent(LCModelComponent):\n display_name = \"Language Model\"\n description = \"Runs a language model given a specified provider.\"\n documentation: str = \"https://docs.langflow.org/components-models\"\n icon = \"brain-circuit\"\n category = \"models\"\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 ),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"API Key\",\n info=\"Model Provider API key\",\n required=False,\n show=True,\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 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 StrInput(\n name=\"ollama_base_url\",\n display_name=\"Ollama API URL\",\n info=f\"Endpoint of the Ollama API (Ollama only). Defaults to {DEFAULT_OLLAMA_URL}\",\n value=DEFAULT_OLLAMA_URL,\n show=False,\n real_time_refresh=True,\n ),\n MessageInput(\n name=\"input_value\",\n display_name=\"Input\",\n info=\"The input text to send to the model\",\n ),\n MultilineInput(\n name=\"system_message\",\n display_name=\"System Message\",\n info=\"A system message that helps set the behavior of the assistant\",\n advanced=False,\n ),\n BoolInput(\n name=\"stream\",\n display_name=\"Stream\",\n info=\"Whether to stream the response\",\n value=False,\n advanced=True,\n ),\n SliderInput(\n name=\"temperature\",\n display_name=\"Temperature\",\n value=0.1,\n info=\"Controls randomness in responses\",\n range_spec=RangeSpec(min=0, max=1, step=0.01),\n advanced=True,\n ),\n IntInput(\n name=\"max_tokens\",\n display_name=\"Max Tokens\",\n info=\"Maximum number of tokens to generate. Field name varies by provider.\",\n advanced=True,\n range_spec=RangeSpec(min=1, max=128000, step=1, step_type=\"int\"),\n ),\n ]\n\n def build_model(self) -> LanguageModel:\n return get_llm(\n model=self.model,\n user_id=self.user_id,\n api_key=self.api_key,\n temperature=self.temperature,\n stream=self.stream,\n max_tokens=getattr(self, \"max_tokens\", None),\n watsonx_url=getattr(self, \"base_url_ibm_watsonx\", None),\n watsonx_project_id=getattr(self, \"project_id\", None),\n ollama_base_url=getattr(self, \"ollama_base_url\", None),\n )\n\n def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None):\n \"\"\"Dynamically update build config with user-filtered model options.\"\"\"\n # Update model options\n build_config = update_model_options_in_build_config(\n component=self,\n build_config=build_config,\n cache_key_prefix=\"language_model_options\",\n get_options_func=get_language_model_options,\n field_name=field_name,\n field_value=field_value,\n )\n\n current_model_value = field_value if field_name == \"model\" else build_config.get(\"model\", {}).get(\"value\")\n provider = \"\"\n if isinstance(current_model_value, list) and current_model_value:\n selected_model = current_model_value[0]\n provider = (selected_model.get(\"provider\") or \"\").strip()\n if not provider and selected_model.get(\"name\"):\n provider = get_provider_for_model_name(str(selected_model[\"name\"]))\n\n if provider:\n build_config = apply_provider_variable_config_to_build_config(build_config, provider)\n\n return build_config\n" }, "input_value": { "_input_type": "MessageInput", @@ -99307,7 +99307,7 @@ "dependencies": [ { "name": "numpy", - "version": "2.4.2" + "version": "2.4.3" }, { "name": "langchain_core", @@ -115617,7 +115617,7 @@ "dependencies": [ { "name": "weaviate", - "version": "4.20.3" + "version": "4.20.4" }, { "name": "langchain_community", @@ -118487,6 +118487,6 @@ "num_components": 359, "num_modules": 97 }, - "sha256": "e08a909c9484aa9dec5cead73cc03ef97a5fc2e344399f2143de60e92cde3aef", - "version": "0.3.0" + "sha256": "dcb9a6e44e2405967f6fd4881e5bc5d3748bb507b3a0e57fc90c9b86c2536730", + "version": "0.3.1" } \ No newline at end of file diff --git a/src/lfx/src/lfx/_assets/stable_hash_history.json b/src/lfx/src/lfx/_assets/stable_hash_history.json index 866546f2d330..7a3f435d2ee3 100644 --- a/src/lfx/src/lfx/_assets/stable_hash_history.json +++ b/src/lfx/src/lfx/_assets/stable_hash_history.json @@ -1,772 +1,926 @@ { "FAISS": { "versions": { - "0.3.0": "2bd7a064d724" + "0.3.0": "2bd7a064d724", + "0.3.1": "2bd7a064d724" } }, "AddContentToPage": { "versions": { - "0.3.0": "ffcd44201c09" + "0.3.0": "ffcd44201c09", + "0.3.1": "ffcd44201c09" } }, "NotionPageCreator": { "versions": { - "0.3.0": "640438ed3d7b" + "0.3.0": "640438ed3d7b", + "0.3.1": "640438ed3d7b" } }, "NotionDatabaseProperties": { "versions": { - "0.3.0": "adce99660f9e" + "0.3.0": "adce99660f9e", + "0.3.1": "adce99660f9e" } }, "NotionListPages": { "versions": { - "0.3.0": "373f9ad32937" + "0.3.0": "373f9ad32937", + "0.3.1": "373f9ad32937" } }, "NotionUserList": { "versions": { - "0.3.0": "8966397da1d5" + "0.3.0": "8966397da1d5", + "0.3.1": "8966397da1d5" } }, "NotionPageContent": { "versions": { - "0.3.0": "ba15d6a01d04" + "0.3.0": "ba15d6a01d04", + "0.3.1": "ba15d6a01d04" } }, "NotionSearch": { "versions": { - "0.3.0": "793b8818a3b4" + "0.3.0": "793b8818a3b4", + "0.3.1": "793b8818a3b4" } }, "NotionPageUpdate": { "versions": { - "0.3.0": "32ccdf34df73" + "0.3.0": "32ccdf34df73", + "0.3.1": "32ccdf34df73" } }, "AgentQL": { "versions": { - "0.3.0": "3737ac221d7d" + "0.3.0": "3737ac221d7d", + "0.3.1": "3737ac221d7d" } }, "AIMLModel": { "versions": { - "0.3.0": "db72277a0d5a" + "0.3.0": "db72277a0d5a", + "0.3.1": "db72277a0d5a" } }, "AIMLEmbeddings": { "versions": { - "0.3.0": "dae370391ba3" + "0.3.0": "dae370391ba3", + "0.3.1": "dae370391ba3" } }, "ALTK Agent": { "versions": { - "0.3.0": "d1caf0d1db88" + "0.3.0": "d1caf0d1db88", + "0.3.1": "d1caf0d1db88" } }, "AmazonBedrockConverseModel": { "versions": { - "0.3.0": "54c335f8699f" + "0.3.0": "54c335f8699f", + "0.3.1": "54c335f8699f" } }, "AmazonBedrockEmbeddings": { "versions": { - "0.3.0": "70d039ff79f0" + "0.3.0": "70d039ff79f0", + "0.3.1": "70d039ff79f0" } }, "AmazonBedrockModel": { "versions": { - "0.3.0": "922093a831b6" + "0.3.0": "922093a831b6", + "0.3.1": "922093a831b6" } }, "s3bucketuploader": { "versions": { - "0.3.0": "119c89b6bd40" + "0.3.0": "119c89b6bd40", + "0.3.1": "119c89b6bd40" } }, "AnthropicModel": { "versions": { - "0.3.0": "7c894c5a66ba" + "0.3.0": "7c894c5a66ba", + "0.3.1": "7c894c5a66ba" } }, "ApifyActors": { "versions": { - "0.3.0": "e84290d462c2" + "0.3.0": "e84290d462c2", + "0.3.1": "e84290d462c2" } }, "ArXivComponent": { "versions": { - "0.3.0": "2d892beaf98b" + "0.3.0": "2d892beaf98b", + "0.3.1": "2d892beaf98b" } }, "AssemblyAIGetSubtitles": { "versions": { - "0.3.0": "533d1fcf7c7a" + "0.3.0": "533d1fcf7c7a", + "0.3.1": "533d1fcf7c7a" } }, "AssemblyAILeMUR": { "versions": { - "0.3.0": "8c96738ab967" + "0.3.0": "8c96738ab967", + "0.3.1": "8c96738ab967" } }, "AssemblyAIListTranscripts": { "versions": { - "0.3.0": "267dcda48ad4" + "0.3.0": "267dcda48ad4", + "0.3.1": "267dcda48ad4" } }, "AssemblyAITranscriptionJobPoller": { "versions": { - "0.3.0": "935c9296b149" + "0.3.0": "935c9296b149", + "0.3.1": "935c9296b149" } }, "AssemblyAITranscriptionJobCreator": { "versions": { - "0.3.0": "7ff7b3f90298" + "0.3.0": "7ff7b3f90298", + "0.3.1": "7ff7b3f90298" } }, "AzureOpenAIModel": { "versions": { - "0.3.0": "2ba26202203e" + "0.3.0": "2ba26202203e", + "0.3.1": "2ba26202203e" } }, "AzureOpenAIEmbeddings": { "versions": { - "0.3.0": "6b54f3243a6a" + "0.3.0": "6b54f3243a6a", + "0.3.1": "6b54f3243a6a" } }, "BaiduQianfanChatModel": { "versions": { - "0.3.0": "a5fdfdb5757f" + "0.3.0": "a5fdfdb5757f", + "0.3.1": "a5fdfdb5757f" } }, "BingSearchAPI": { "versions": { - "0.3.0": "21008f6682b9" + "0.3.0": "21008f6682b9", + "0.3.1": "21008f6682b9" } }, "Cassandra": { "versions": { - "0.3.0": "833f277daab7" + "0.3.0": "833f277daab7", + "0.3.1": "833f277daab7" } }, "CassandraChatMemory": { "versions": { - "0.3.0": "f6497182984e" + "0.3.0": "f6497182984e", + "0.3.1": "f6497182984e" } }, "CassandraGraph": { "versions": { - "0.3.0": "26c63f80745e" + "0.3.0": "26c63f80745e", + "0.3.1": "26c63f80745e" } }, "Chroma": { "versions": { - "0.3.0": "82d38624f19a" + "0.3.0": "82d38624f19a", + "0.3.1": "82d38624f19a" } }, "CleanlabEvaluator": { "versions": { - "0.3.0": "06963c804ffe" + "0.3.0": "06963c804ffe", + "0.3.1": "06963c804ffe" } }, "CleanlabRAGEvaluator": { "versions": { - "0.3.0": "f48b57ff7ca3" + "0.3.0": "f48b57ff7ca3", + "0.3.1": "f48b57ff7ca3" } }, "CleanlabRemediator": { "versions": { - "0.3.0": "a5b19d338991" + "0.3.0": "a5b19d338991", + "0.3.1": "a5b19d338991" } }, "Clickhouse": { "versions": { - "0.3.0": "ab991e83da44" + "0.3.0": "ab991e83da44", + "0.3.1": "ab991e83da44" } }, "CloudflareWorkersAIEmbeddings": { "versions": { - "0.3.0": "1ea6e4857c14" + "0.3.0": "1ea6e4857c14", + "0.3.1": "1ea6e4857c14" } }, "CohereEmbeddings": { "versions": { - "0.3.0": "9c0f413a2c64" + "0.3.0": "9c0f413a2c64", + "0.3.1": "9c0f413a2c64" } }, "CohereModel": { "versions": { - "0.3.0": "594852e1d706" + "0.3.0": "594852e1d706", + "0.3.1": "594852e1d706" } }, "CohereRerank": { "versions": { - "0.3.0": "a94a0d11eeac" + "0.3.0": "a94a0d11eeac", + "0.3.1": "a94a0d11eeac" } }, "CometAPIModel": { "versions": { - "0.3.0": "4ec4a8852e9c" + "0.3.0": "4ec4a8852e9c", + "0.3.1": "4ec4a8852e9c" } }, "ComposioAgentQLAPIComponent": { "versions": { - "0.3.0": "cca708a10ab6" + "0.3.0": "cca708a10ab6", + "0.3.1": "cca708a10ab6" } }, "ComposioAgiledAPIComponent": { "versions": { - "0.3.0": "3294a951a1a8" + "0.3.0": "3294a951a1a8", + "0.3.1": "3294a951a1a8" } }, "ComposioAirtableAPIComponent": { "versions": { - "0.3.0": "e47ad011c33c" + "0.3.0": "e47ad011c33c", + "0.3.1": "e47ad011c33c" } }, "ComposioApolloAPIComponent": { "versions": { - "0.3.0": "3af16f5d6ceb" + "0.3.0": "3af16f5d6ceb", + "0.3.1": "3af16f5d6ceb" } }, "ComposioAsanaAPIComponent": { "versions": { - "0.3.0": "290d6d61d049" + "0.3.0": "290d6d61d049", + "0.3.1": "290d6d61d049" } }, "ComposioAttioAPIComponent": { "versions": { - "0.3.0": "de43b3cf5671" + "0.3.0": "de43b3cf5671", + "0.3.1": "de43b3cf5671" } }, "ComposioBitbucketAPIComponent": { "versions": { - "0.3.0": "7528a8928646" + "0.3.0": "7528a8928646", + "0.3.1": "7528a8928646" } }, "ComposioBolnaAPIComponent": { "versions": { - "0.3.0": "dde7d2ee80a2" + "0.3.0": "dde7d2ee80a2", + "0.3.1": "dde7d2ee80a2" } }, "ComposioBrightdataAPIComponent": { "versions": { - "0.3.0": "49a04c5a23cb" + "0.3.0": "49a04c5a23cb", + "0.3.1": "49a04c5a23cb" } }, "ComposioCalendlyAPIComponent": { "versions": { - "0.3.0": "4a282e413d55" + "0.3.0": "4a282e413d55", + "0.3.1": "4a282e413d55" } }, "ComposioCanvaAPIComponent": { "versions": { - "0.3.0": "d149aa178e80" + "0.3.0": "d149aa178e80", + "0.3.1": "d149aa178e80" } }, "ComposioCanvasAPIComponent": { "versions": { - "0.3.0": "6510d212a720" + "0.3.0": "6510d212a720", + "0.3.1": "6510d212a720" } }, "ComposioCodaAPIComponent": { "versions": { - "0.3.0": "f7693920313f" + "0.3.0": "f7693920313f", + "0.3.1": "f7693920313f" } }, "ComposioAPI": { "versions": { - "0.3.0": "764255821307" + "0.3.0": "764255821307", + "0.3.1": "764255821307" } }, "ComposioContentfulAPIComponent": { "versions": { - "0.3.0": "36befb1ec8fc" + "0.3.0": "36befb1ec8fc", + "0.3.1": "36befb1ec8fc" } }, "ComposioDigicertAPIComponent": { "versions": { - "0.3.0": "0fcbc1b899f8" + "0.3.0": "0fcbc1b899f8", + "0.3.1": "0fcbc1b899f8" } }, "ComposioDiscordAPIComponent": { "versions": { - "0.3.0": "2ec988f25784" + "0.3.0": "2ec988f25784", + "0.3.1": "2ec988f25784" } }, "ComposioDropboxAPIComponent": { "versions": { - "0.3.0": "d05825599def" + "0.3.0": "d05825599def", + "0.3.1": "d05825599def" } }, "ComposioElevenLabsAPIComponent": { "versions": { - "0.3.0": "e0c91533558b" + "0.3.0": "e0c91533558b", + "0.3.1": "e0c91533558b" } }, "ComposioExaAPIComponent": { "versions": { - "0.3.0": "3b5cecdefab8" + "0.3.0": "3b5cecdefab8", + "0.3.1": "3b5cecdefab8" } }, "ComposioFigmaAPIComponent": { "versions": { - "0.3.0": "7443d213546b" + "0.3.0": "7443d213546b", + "0.3.1": "7443d213546b" } }, "ComposioFinageAPIComponent": { "versions": { - "0.3.0": "50a2bdee4cd1" + "0.3.0": "50a2bdee4cd1", + "0.3.1": "50a2bdee4cd1" } }, "ComposioFirecrawlAPIComponent": { "versions": { - "0.3.0": "2ab1c4b00071" + "0.3.0": "2ab1c4b00071", + "0.3.1": "2ab1c4b00071" } }, "ComposioFirefliesAPIComponent": { "versions": { - "0.3.0": "233cd91dbdad" + "0.3.0": "233cd91dbdad", + "0.3.1": "233cd91dbdad" } }, "ComposioFixerAPIComponent": { "versions": { - "0.3.0": "9e4c00f9dcd8" + "0.3.0": "9e4c00f9dcd8", + "0.3.1": "9e4c00f9dcd8" } }, "ComposioFlexisignAPIComponent": { "versions": { - "0.3.0": "c69bbee0005d" + "0.3.0": "c69bbee0005d", + "0.3.1": "c69bbee0005d" } }, "ComposioFreshdeskAPIComponent": { "versions": { - "0.3.0": "1dde03d615ca" + "0.3.0": "1dde03d615ca", + "0.3.1": "1dde03d615ca" } }, "ComposioGitHubAPIComponent": { "versions": { - "0.3.0": "ee201105d924" + "0.3.0": "ee201105d924", + "0.3.1": "ee201105d924" } }, "ComposioGmailAPIComponent": { "versions": { - "0.3.0": "d4b13ac8a3a1" + "0.3.0": "d4b13ac8a3a1", + "0.3.1": "d4b13ac8a3a1" } }, "ComposioGoogleBigQueryAPIComponent": { "versions": { - "0.3.0": "f7d84aaae78f" + "0.3.0": "f7d84aaae78f", + "0.3.1": "f7d84aaae78f" } }, "ComposioGoogleCalendarAPIComponent": { "versions": { - "0.3.0": "28adb6fff093" + "0.3.0": "28adb6fff093", + "0.3.1": "28adb6fff093" } }, "ComposioGoogleclassroomAPIComponent": { "versions": { - "0.3.0": "85a5c37c13f6" + "0.3.0": "85a5c37c13f6", + "0.3.1": "85a5c37c13f6" } }, "ComposioGoogleDocsAPIComponent": { "versions": { - "0.3.0": "ac2e88b6f706" + "0.3.0": "ac2e88b6f706", + "0.3.1": "ac2e88b6f706" } }, "ComposioGooglemeetAPIComponent": { "versions": { - "0.3.0": "cdbf16c4b42f" + "0.3.0": "cdbf16c4b42f", + "0.3.1": "cdbf16c4b42f" } }, "ComposioGoogleSheetsAPIComponent": { "versions": { - "0.3.0": "b0db7a3abe1f" + "0.3.0": "b0db7a3abe1f", + "0.3.1": "b0db7a3abe1f" } }, "ComposioGoogleTasksAPIComponent": { "versions": { - "0.3.0": "2ba9c1661f41" + "0.3.0": "2ba9c1661f41", + "0.3.1": "2ba9c1661f41" } }, "ComposioHeygenAPIComponent": { "versions": { - "0.3.0": "c72fd5d0350f" + "0.3.0": "c72fd5d0350f", + "0.3.1": "c72fd5d0350f" } }, "ComposioInstagramAPIComponent": { "versions": { - "0.3.0": "a6691c905833" + "0.3.0": "a6691c905833", + "0.3.1": "a6691c905833" } }, "ComposioJiraAPIComponent": { "versions": { - "0.3.0": "3e62396f3868" + "0.3.0": "3e62396f3868", + "0.3.1": "3e62396f3868" } }, "ComposioJotformAPIComponent": { "versions": { - "0.3.0": "7c1c6a676814" + "0.3.0": "7c1c6a676814", + "0.3.1": "7c1c6a676814" } }, "ComposioKlaviyoAPIComponent": { "versions": { - "0.3.0": "3be7e8a5e3fe" + "0.3.0": "3be7e8a5e3fe", + "0.3.1": "3be7e8a5e3fe" } }, "ComposioLinearAPIComponent": { "versions": { - "0.3.0": "be2b2ebbeea7" + "0.3.0": "be2b2ebbeea7", + "0.3.1": "be2b2ebbeea7" } }, "ComposioListennotesAPIComponent": { "versions": { - "0.3.0": "b85f2fe51906" + "0.3.0": "b85f2fe51906", + "0.3.1": "b85f2fe51906" } }, "ComposioMem0APIComponent": { "versions": { - "0.3.0": "68871a483786" + "0.3.0": "68871a483786", + "0.3.1": "68871a483786" } }, "ComposioMiroAPIComponent": { "versions": { - "0.3.0": "1e9c421e1ac4" + "0.3.0": "1e9c421e1ac4", + "0.3.1": "1e9c421e1ac4" } }, "ComposioMissiveAPIComponent": { "versions": { - "0.3.0": "6def944a7739" + "0.3.0": "6def944a7739", + "0.3.1": "6def944a7739" } }, "ComposioNotionAPIComponent": { "versions": { - "0.3.0": "590aa6ff30d1" + "0.3.0": "590aa6ff30d1", + "0.3.1": "590aa6ff30d1" } }, "ComposioOneDriveAPIComponent": { "versions": { - "0.3.0": "497cc4625121" + "0.3.0": "497cc4625121", + "0.3.1": "497cc4625121" } }, "ComposioOutlookAPIComponent": { "versions": { - "0.3.0": "bf6998d60b63" + "0.3.0": "bf6998d60b63", + "0.3.1": "bf6998d60b63" } }, "ComposioPandadocAPIComponent": { "versions": { - "0.3.0": "21d92aabc1bf" + "0.3.0": "21d92aabc1bf", + "0.3.1": "21d92aabc1bf" } }, "ComposioPeopleDataLabsAPIComponent": { "versions": { - "0.3.0": "bd05ce58f55c" + "0.3.0": "bd05ce58f55c", + "0.3.1": "bd05ce58f55c" } }, "ComposioPerplexityAIAPIComponent": { "versions": { - "0.3.0": "e40b0651344f" + "0.3.0": "e40b0651344f", + "0.3.1": "e40b0651344f" } }, "ComposioRedditAPIComponent": { "versions": { - "0.3.0": "a86794073c22" + "0.3.0": "a86794073c22", + "0.3.1": "a86794073c22" } }, "ComposioSerpAPIComponent": { "versions": { - "0.3.0": "74b0a07ee54b" + "0.3.0": "74b0a07ee54b", + "0.3.1": "74b0a07ee54b" } }, "ComposioSlackAPIComponent": { "versions": { - "0.3.0": "fa340cae1330" + "0.3.0": "fa340cae1330", + "0.3.1": "fa340cae1330" } }, "ComposioSlackbotAPIComponent": { "versions": { - "0.3.0": "ddeb26bc04e6" + "0.3.0": "ddeb26bc04e6", + "0.3.1": "ddeb26bc04e6" } }, "ComposioSnowflakeAPIComponent": { "versions": { - "0.3.0": "d0d1af5686d2" + "0.3.0": "d0d1af5686d2", + "0.3.1": "d0d1af5686d2" } }, "ComposioSupabaseAPIComponent": { "versions": { - "0.3.0": "7ad58ce34cc0" + "0.3.0": "7ad58ce34cc0", + "0.3.1": "7ad58ce34cc0" } }, "ComposioTavilyAPIComponent": { "versions": { - "0.3.0": "97af05e37911" + "0.3.0": "97af05e37911", + "0.3.1": "97af05e37911" } }, "ComposioTimelinesAIAPIComponent": { "versions": { - "0.3.0": "76e70e2de4d3" + "0.3.0": "76e70e2de4d3", + "0.3.1": "76e70e2de4d3" } }, "ComposioTodoistAPIComponent": { "versions": { - "0.3.0": "4dd9852f2058" + "0.3.0": "4dd9852f2058", + "0.3.1": "4dd9852f2058" } }, "ComposioWrikeAPIComponent": { "versions": { - "0.3.0": "a5f2cf00ca08" + "0.3.0": "a5f2cf00ca08", + "0.3.1": "a5f2cf00ca08" } }, "ComposioYoutubeAPIComponent": { "versions": { - "0.3.0": "d1af2ea00e8b" + "0.3.0": "d1af2ea00e8b", + "0.3.1": "d1af2ea00e8b" } }, "Confluence": { "versions": { - "0.3.0": "d669f422824e" + "0.3.0": "d669f422824e", + "0.3.1": "d669f422824e" } }, "Couchbase": { "versions": { - "0.3.0": "70ed475a6f48" + "0.3.0": "70ed475a6f48", + "0.3.1": "70ed475a6f48" } }, "CrewAIAgentComponent": { "versions": { - "0.3.0": "a23f0923049d" + "0.3.0": "a23f0923049d", + "0.3.1": "a23f0923049d" } }, "HierarchicalCrewComponent": { "versions": { - "0.3.0": "144be482cfb0" + "0.3.0": "144be482cfb0", + "0.3.1": "144be482cfb0" } }, "HierarchicalTaskComponent": { "versions": { - "0.3.0": "25071652dc20" + "0.3.0": "25071652dc20", + "0.3.1": "25071652dc20" } }, "SequentialCrewComponent": { "versions": { - "0.3.0": "42e59f6d6572" + "0.3.0": "42e59f6d6572", + "0.3.1": "42e59f6d6572" } }, "SequentialTaskComponent": { "versions": { - "0.3.0": "b1f17b8fcc5c" + "0.3.0": "b1f17b8fcc5c", + "0.3.1": "b1f17b8fcc5c" } }, "SequentialTaskAgentComponent": { "versions": { - "0.3.0": "0a5483ef82c3" + "0.3.0": "0a5483ef82c3", + "0.3.1": "0a5483ef82c3" } }, "Cuga": { "versions": { - "0.3.0": "35e838f89d13" + "0.3.0": "35e838f89d13", + "0.3.1": "35e838f89d13" } }, "CustomComponent": { "versions": { - "0.3.0": "d50a68a6fa57" + "0.3.0": "d50a68a6fa57", + "0.3.1": "d50a68a6fa57" } }, "APIRequest": { "versions": { - "0.3.0": "2af407885294" + "0.3.0": "2af407885294", + "0.3.1": "2af407885294" } }, "CSVtoData": { "versions": { - "0.3.0": "049e2eeb6901" + "0.3.0": "049e2eeb6901", + "0.3.1": "049e2eeb6901" } }, "JSONtoData": { "versions": { - "0.3.0": "e8d050bde0d0" + "0.3.0": "e8d050bde0d0", + "0.3.1": "e8d050bde0d0" } }, "MockDataGenerator": { "versions": { - "0.3.0": "d21dce7b329b" + "0.3.0": "d21dce7b329b", + "0.3.1": "d21dce7b329b" } }, "NewsSearch": { "versions": { - "0.3.0": "b8cb11f78518" + "0.3.0": "b8cb11f78518", + "0.3.1": "b8cb11f78518" } }, "RSSReaderSimple": { "versions": { - "0.3.0": "6eb8fb48c9b5" + "0.3.0": "6eb8fb48c9b5", + "0.3.1": "6eb8fb48c9b5" } }, "SQLComponent": { "versions": { - "0.3.0": "a8dd79af50b8" + "0.3.0": "a8dd79af50b8", + "0.3.1": "a8dd79af50b8" } }, "URLComponent": { "versions": { - "0.3.0": "7c2b0b18854e" + "0.3.0": "7c2b0b18854e", + "0.3.1": "7c2b0b18854e" } }, "UnifiedWebSearch": { "versions": { - "0.3.0": "cbeeaef8889a" + "0.3.0": "cbeeaef8889a", + "0.3.1": "cbeeaef8889a" } }, "Astra Assistant Agent": { "versions": { - "0.3.0": "4716d3b7c350" + "0.3.0": "4716d3b7c350", + "0.3.1": "4716d3b7c350" } }, "AstraDBChatMemory": { "versions": { - "0.3.0": "bafc81f78c76" + "0.3.0": "bafc81f78c76", + "0.3.1": "bafc81f78c76" } }, "AstraDBCQLToolComponent": { "versions": { - "0.3.0": "70c4523f841d" + "0.3.0": "70c4523f841d", + "0.3.1": "70c4523f841d" } }, "AstraDBGraph": { "versions": { - "0.3.0": "9f5d576b30ca" + "0.3.0": "9f5d576b30ca", + "0.3.1": "9f5d576b30ca" } }, "AstraDBTool": { "versions": { - "0.3.0": "44719b6ed1a3" + "0.3.0": "44719b6ed1a3", + "0.3.1": "44719b6ed1a3" } }, "AstraVectorize": { "versions": { - "0.3.0": "3d976690c262" + "0.3.0": "3d976690c262", + "0.3.1": "3d976690c262" } }, "AstraDB": { "versions": { - "0.3.0": "9ca3747e73d6" + "0.3.0": "9ca3747e73d6", + "0.3.1": "9ca3747e73d6" } }, "AssistantsCreateAssistant": { "versions": { - "0.3.0": "8d9869d9a89d" + "0.3.0": "8d9869d9a89d", + "0.3.1": "8d9869d9a89d" } }, "AssistantsCreateThread": { "versions": { - "0.3.0": "5d40a73accfd" + "0.3.0": "5d40a73accfd", + "0.3.1": "5d40a73accfd" } }, "Dotenv": { "versions": { - "0.3.0": "343ea9aaca1b" + "0.3.0": "343ea9aaca1b", + "0.3.1": "343ea9aaca1b" } }, "AssistantsGetAssistantName": { "versions": { - "0.3.0": "1f60da161fd3" + "0.3.0": "1f60da161fd3", + "0.3.1": "1f60da161fd3" } }, "GetEnvVar": { "versions": { - "0.3.0": "083f0a94f380" + "0.3.0": "083f0a94f380", + "0.3.1": "083f0a94f380" } }, "GraphRAG": { "versions": { - "0.3.0": "4d83709a5f5f" + "0.3.0": "4d83709a5f5f", + "0.3.1": "4d83709a5f5f" } }, "HCD": { "versions": { - "0.3.0": "25f009b9e171" + "0.3.0": "25f009b9e171", + "0.3.1": "25f009b9e171" } }, "AssistantsListAssistants": { "versions": { - "0.3.0": "17e9c5c78a6e" + "0.3.0": "17e9c5c78a6e", + "0.3.1": "17e9c5c78a6e" } }, "AssistantsRun": { "versions": { - "0.3.0": "5e219cd290d3" + "0.3.0": "5e219cd290d3", + "0.3.1": "5e219cd290d3" } }, "DeepSeekModelComponent": { "versions": { - "0.3.0": "c8dac7a258d7" + "0.3.0": "c8dac7a258d7", + "0.3.1": "c8dac7a258d7" } }, "ChunkDoclingDocument": { "versions": { - "0.3.0": "7775393185fe" + "0.3.0": "7775393185fe", + "0.3.1": "7775393185fe" } }, "DoclingInline": { "versions": { - "0.3.0": "519d12bd6451" + "0.3.0": "519d12bd6451", + "0.3.1": "519d12bd6451" } }, "DoclingRemote": { "versions": { - "0.3.0": "409d771a961e" + "0.3.0": "409d771a961e", + "0.3.1": "409d771a961e" } }, "ExportDoclingDocument": { "versions": { - "0.3.0": "24cc033dcec6" + "0.3.0": "24cc033dcec6", + "0.3.1": "24cc033dcec6" } }, "DuckDuckGoSearchComponent": { "versions": { - "0.3.0": "2b8d1e2e8317" + "0.3.0": "2b8d1e2e8317", + "0.3.1": "2b8d1e2e8317" } }, "Elasticsearch": { "versions": { - "0.3.0": "23ea4383039e" + "0.3.0": "23ea4383039e", + "0.3.1": "23ea4383039e" } }, "OpenSearchVectorStoreComponent": { "versions": { - "0.3.0": "f4dfc3668475" + "0.3.0": "f4dfc3668475", + "0.3.1": "f4dfc3668475" } }, "OpenSearchVectorStoreComponentMultimodalMultiEmbedding": { "versions": { - "0.3.0": "24abb9020048" + "0.3.0": "24abb9020048", + "0.3.1": "24abb9020048" } }, "EmbeddingSimilarityComponent": { "versions": { - "0.3.0": "d94c7d791f69" + "0.3.0": "d94c7d791f69", + "0.3.1": "d94c7d791f69" } }, "TextEmbedderComponent": { "versions": { - "0.3.0": "541a2fb78066" + "0.3.0": "541a2fb78066", + "0.3.1": "541a2fb78066" } }, "ExaSearch": { "versions": { - "0.3.0": "26039e2a8b78" + "0.3.0": "26039e2a8b78", + "0.3.1": "26039e2a8b78" } }, "Directory": { "versions": { - "0.3.0": "328e6f996926" + "0.3.0": "328e6f996926", + "0.3.1": "328e6f996926" } }, "File": { "versions": { - "0.3.0": "12a5841f1a03" + "0.3.0": "12a5841f1a03", + "0.3.1": "12a5841f1a03" } }, "KnowledgeIngestion": { @@ -781,1027 +935,1232 @@ }, "SaveToFile": { "versions": { - "0.3.0": "f8b6df3c93c0" + "0.3.0": "f8b6df3c93c0", + "0.3.1": "f8b6df3c93c0" } }, "FirecrawlCrawlApi": { "versions": { - "0.3.0": "21b4965f8b53" + "0.3.0": "21b4965f8b53", + "0.3.1": "21b4965f8b53" } }, "FirecrawlExtractApi": { "versions": { - "0.3.0": "1363b7da7bf7" + "0.3.0": "1363b7da7bf7", + "0.3.1": "1363b7da7bf7" } }, "FirecrawlMapApi": { "versions": { - "0.3.0": "31e75312e67e" + "0.3.0": "31e75312e67e", + "0.3.1": "31e75312e67e" } }, "FirecrawlScrapeApi": { "versions": { - "0.3.0": "a56c999d7a42" + "0.3.0": "a56c999d7a42", + "0.3.1": "a56c999d7a42" } }, "ConditionalRouter": { "versions": { - "0.3.0": "e92b1b243e5f" + "0.3.0": "e92b1b243e5f", + "0.3.1": "e92b1b243e5f" } }, "DataConditionalRouter": { "versions": { - "0.3.0": "6fa1bf4166a3" + "0.3.0": "6fa1bf4166a3", + "0.3.1": "6fa1bf4166a3" } }, "FlowTool": { "versions": { - "0.3.0": "9348d79d19f4" + "0.3.0": "9348d79d19f4", + "0.3.1": "9348d79d19f4" } }, "Listen": { "versions": { - "0.3.0": "7f4e3f36b7e2" + "0.3.0": "7f4e3f36b7e2", + "0.3.1": "7f4e3f36b7e2" } }, "LoopComponent": { "versions": { - "0.3.0": "f789817c7cd3" + "0.3.0": "f789817c7cd3", + "0.3.1": "f789817c7cd3" } }, "Notify": { "versions": { - "0.3.0": "a22284d4b01e" + "0.3.0": "a22284d4b01e", + "0.3.1": "a22284d4b01e" } }, "Pass": { "versions": { - "0.3.0": "04d3e1ed3390" + "0.3.0": "04d3e1ed3390", + "0.3.1": "04d3e1ed3390" } }, "RunFlow": { "versions": { - "0.3.0": "9e5eefa14766" + "0.3.0": "9e5eefa14766", + "0.3.1": "9e5eefa14766" } }, "SubFlow": { "versions": { - "0.3.0": "8ba6fbc5ca3a" + "0.3.0": "8ba6fbc5ca3a", + "0.3.1": "8ba6fbc5ca3a" } }, "GitLoaderComponent": { "versions": { - "0.3.0": "7797832cc23c" + "0.3.0": "7797832cc23c", + "0.3.1": "7797832cc23c" } }, "GitExtractorComponent": { "versions": { - "0.3.0": "79f338765b69" + "0.3.0": "79f338765b69", + "0.3.1": "79f338765b69" } }, "GleanSearchAPIComponent": { "versions": { - "0.3.0": "469618609b03" + "0.3.0": "469618609b03", + "0.3.1": "469618609b03" } }, "GmailLoaderComponent": { "versions": { - "0.3.0": "6ef945902cfd" + "0.3.0": "6ef945902cfd", + "0.3.1": "6ef945902cfd" } }, "BigQueryExecutor": { "versions": { - "0.3.0": "7f61159743ec" + "0.3.0": "7f61159743ec", + "0.3.1": "7f61159743ec" } }, "GoogleDriveComponent": { "versions": { - "0.3.0": "1727f517e814" + "0.3.0": "1727f517e814", + "0.3.1": "1727f517e814" } }, "GoogleDriveSearchComponent": { "versions": { - "0.3.0": "35528aa332d1" + "0.3.0": "35528aa332d1", + "0.3.1": "35528aa332d1" } }, "GoogleGenerativeAIModel": { "versions": { - "0.3.0": "049f38001189" + "0.3.0": "049f38001189", + "0.3.1": "049f38001189" } }, "Google Generative AI Embeddings": { "versions": { - "0.3.0": "7756ad70a221" + "0.3.0": "7756ad70a221", + "0.3.1": "7756ad70a221" } }, "GoogleOAuthToken": { "versions": { - "0.3.0": "151778572099" + "0.3.0": "151778572099", + "0.3.1": "151778572099" } }, "GoogleSearchAPICore": { "versions": { - "0.3.0": "853c193aa41c" + "0.3.0": "853c193aa41c", + "0.3.1": "853c193aa41c" } }, "GoogleSerperAPICore": { "versions": { - "0.3.0": "527183dcb201" + "0.3.0": "527183dcb201", + "0.3.1": "527183dcb201" } }, "GroqModel": { "versions": { - "0.3.0": "8a55d3fa8173" + "0.3.0": "8a55d3fa8173", + "0.3.1": "8a55d3fa8173" } }, "HomeAssistantControl": { "versions": { - "0.3.0": "32b34baf3c7d" + "0.3.0": "32b34baf3c7d", + "0.3.1": "32b34baf3c7d" } }, "ListHomeAssistantStates": { "versions": { - "0.3.0": "c7bb91632474" + "0.3.0": "c7bb91632474", + "0.3.1": "c7bb91632474" } }, "HuggingFaceModel": { "versions": { - "0.3.0": "ba9d7c28859d" + "0.3.0": "ba9d7c28859d", + "0.3.1": "ba9d7c28859d" } }, "HuggingFaceInferenceAPIEmbeddings": { "versions": { - "0.3.0": "af0546658974" + "0.3.0": "af0546658974", + "0.3.1": "af0546658974" } }, "IBMwatsonxModel": { "versions": { - "0.3.0": "e7d46a4de547" + "0.3.0": "e7d46a4de547", + "0.3.1": "e7d46a4de547" } }, "WatsonxEmbeddingsComponent": { "versions": { - "0.3.0": "6effadae5f84" + "0.3.0": "6effadae5f84", + "0.3.1": "6effadae5f84" } }, "Combinatorial Reasoner": { "versions": { - "0.3.0": "48557047ff23" + "0.3.0": "48557047ff23", + "0.3.1": "48557047ff23" } }, "ChatInput": { "versions": { - "0.3.0": "7a26c54d89ed" + "0.3.0": "7a26c54d89ed", + "0.3.1": "7a26c54d89ed" } }, "ChatOutput": { "versions": { - "0.3.0": "c312c84b1777" + "0.3.0": "c312c84b1777", + "0.3.1": "c312c84b1777" } }, "TextInput": { "versions": { - "0.3.0": "518f16485886" + "0.3.0": "518f16485886", + "0.3.1": "518f16485886" } }, "TextOutput": { "versions": { - "0.3.0": "6aba888f4632" + "0.3.0": "6aba888f4632", + "0.3.1": "6aba888f4632" } }, "Webhook": { "versions": { - "0.3.0": "e99e2452d56e" + "0.3.0": "e99e2452d56e", + "0.3.1": "e99e2452d56e" } }, "JigsawStackAIScraper": { "versions": { - "0.3.0": "08a0063e2564" + "0.3.0": "08a0063e2564", + "0.3.1": "08a0063e2564" } }, "JigsawStackAISearch": { "versions": { - "0.3.0": "5e821ee44040" + "0.3.0": "5e821ee44040", + "0.3.1": "5e821ee44040" } }, "JigsawStackFileRead": { "versions": { - "0.3.0": "84d122f46fb1" + "0.3.0": "84d122f46fb1", + "0.3.1": "84d122f46fb1" } }, "JigsawStackFileUpload": { "versions": { - "0.3.0": "8261dc72a655" + "0.3.0": "8261dc72a655", + "0.3.1": "8261dc72a655" } }, "JigsawStackImageGeneration": { "versions": { - "0.3.0": "463f8efbaa18" + "0.3.0": "463f8efbaa18", + "0.3.1": "463f8efbaa18" } }, "JigsawStackNSFW": { "versions": { - "0.3.0": "88b5eee38906" + "0.3.0": "88b5eee38906", + "0.3.1": "88b5eee38906" } }, "JigsawStackObjectDetection": { "versions": { - "0.3.0": "fceaf0286f20" + "0.3.0": "fceaf0286f20", + "0.3.1": "fceaf0286f20" } }, "JigsawStackSentiment": { "versions": { - "0.3.0": "00d0377e5c2e" + "0.3.0": "00d0377e5c2e", + "0.3.1": "00d0377e5c2e" } }, "JigsawStackTextToSQL": { "versions": { - "0.3.0": "30aa0cc15fb8" + "0.3.0": "30aa0cc15fb8", + "0.3.1": "30aa0cc15fb8" } }, "JigsawStackTextTranslate": { "versions": { - "0.3.0": "46233e19961f" + "0.3.0": "46233e19961f", + "0.3.1": "46233e19961f" } }, "JigsawStackVOCR": { "versions": { - "0.3.0": "a44491947ba7" + "0.3.0": "a44491947ba7", + "0.3.1": "a44491947ba7" } }, "CharacterTextSplitter": { "versions": { - "0.3.0": "ea7c81772b05" + "0.3.0": "ea7c81772b05", + "0.3.1": "ea7c81772b05" } }, "ConversationChain": { "versions": { - "0.3.0": "b0e4044d3f08" + "0.3.0": "b0e4044d3f08", + "0.3.1": "b0e4044d3f08" } }, "CSVAgent": { "versions": { - "0.3.0": "1888a727f9dd" + "0.3.0": "1888a727f9dd", + "0.3.1": "1888a727f9dd" } }, "LangChainFakeEmbeddings": { "versions": { - "0.3.0": "faaa1be642a1" + "0.3.0": "faaa1be642a1", + "0.3.1": "faaa1be642a1" } }, "HtmlLinkExtractor": { "versions": { - "0.3.0": "7acefd9ece13" + "0.3.0": "7acefd9ece13", + "0.3.1": "7acefd9ece13" } }, "JsonAgent": { "versions": { - "0.3.0": "117a41e142c6" + "0.3.0": "117a41e142c6", + "0.3.1": "117a41e142c6" } }, "LangChain Hub Prompt": { "versions": { - "0.3.0": "9f59bf236231" + "0.3.0": "9f59bf236231", + "0.3.1": "9f59bf236231" } }, "LanguageRecursiveTextSplitter": { "versions": { - "0.3.0": "ee28cc4c2001" + "0.3.0": "ee28cc4c2001", + "0.3.1": "ee28cc4c2001" } }, "SemanticTextSplitter": { "versions": { - "0.3.0": "8a7e7a5a39ed" + "0.3.0": "8a7e7a5a39ed", + "0.3.1": "8a7e7a5a39ed" } }, "LLMCheckerChain": { "versions": { - "0.3.0": "81aa2c9910db" + "0.3.0": "81aa2c9910db", + "0.3.1": "81aa2c9910db" } }, "LLMMathChain": { "versions": { - "0.3.0": "422d8e0d4614" + "0.3.0": "422d8e0d4614", + "0.3.1": "422d8e0d4614" } }, "NaturalLanguageTextSplitter": { "versions": { - "0.3.0": "6483da1155b8" + "0.3.0": "6483da1155b8", + "0.3.1": "6483da1155b8" } }, "OpenAIToolsAgent": { "versions": { - "0.3.0": "21bfd16796e5" + "0.3.0": "21bfd16796e5", + "0.3.1": "21bfd16796e5" } }, "OpenAPIAgent": { "versions": { - "0.3.0": "a4a7564100da" + "0.3.0": "a4a7564100da", + "0.3.1": "a4a7564100da" } }, "RecursiveCharacterTextSplitter": { "versions": { - "0.3.0": "1cad6dd9957a" + "0.3.0": "1cad6dd9957a", + "0.3.1": "1cad6dd9957a" } }, "RetrievalQA": { "versions": { - "0.3.0": "16c80b234abf" + "0.3.0": "16c80b234abf", + "0.3.1": "16c80b234abf" } }, "RunnableExecutor": { "versions": { - "0.3.0": "32135f41efe5" + "0.3.0": "32135f41efe5", + "0.3.1": "32135f41efe5" } }, "SelfQueryRetriever": { "versions": { - "0.3.0": "3b647e2416be" + "0.3.0": "3b647e2416be", + "0.3.1": "3b647e2416be" } }, "SpiderTool": { "versions": { - "0.3.0": "08c823a20237" + "0.3.0": "08c823a20237", + "0.3.1": "08c823a20237" } }, "SQLAgent": { "versions": { - "0.3.0": "550c16461236" + "0.3.0": "550c16461236", + "0.3.1": "550c16461236" } }, "SQLDatabase": { "versions": { - "0.3.0": "7d07b7faa341" + "0.3.0": "7d07b7faa341", + "0.3.1": "7d07b7faa341" } }, "SQLGenerator": { "versions": { - "0.3.0": "971a82407ec6" + "0.3.0": "971a82407ec6", + "0.3.1": "971a82407ec6" } }, "ToolCallingAgent": { "versions": { - "0.3.0": "cdeb13ac7430" + "0.3.0": "cdeb13ac7430", + "0.3.1": "cdeb13ac7430" } }, "VectorStoreInfo": { "versions": { - "0.3.0": "62f06efeec2c" + "0.3.0": "62f06efeec2c", + "0.3.1": "62f06efeec2c" } }, "VectorStoreRouterAgent": { "versions": { - "0.3.0": "b16515f6e722" + "0.3.0": "b16515f6e722", + "0.3.1": "b16515f6e722" } }, "XMLAgent": { "versions": { - "0.3.0": "17a0cb4c48a8" + "0.3.0": "17a0cb4c48a8", + "0.3.1": "17a0cb4c48a8" } }, "LangWatchEvaluator": { "versions": { - "0.3.0": "6db3bccd5945" + "0.3.0": "6db3bccd5945", + "0.3.1": "6db3bccd5945" } }, "BatchRunComponent": { "versions": { - "0.3.0": "f20d52a329ad" + "0.3.0": "f20d52a329ad", + "0.3.1": "f20d52a329ad" } }, "Smart Transform": { "versions": { - "0.3.0": "4fb127dc371c" + "0.3.0": "4fb127dc371c", + "0.3.1": "e9c638ca3f34" } }, "SmartRouter": { "versions": { - "0.3.0": "7bdefeec6280" + "0.3.0": "7bdefeec6280", + "0.3.1": "86404d4beb95" } }, "LLMSelectorComponent": { "versions": { - "0.3.0": "59de3f4015ff" + "0.3.0": "59de3f4015ff", + "0.3.1": "59de3f4015ff" } }, "StructuredOutput": { "versions": { - "0.3.0": "058ca1f51e9f" + "0.3.0": "058ca1f51e9f", + "0.3.1": "058ca1f51e9f" } }, "LMStudioEmbeddingsComponent": { "versions": { - "0.3.0": "ecd584b7a486" + "0.3.0": "ecd584b7a486", + "0.3.1": "ecd584b7a486" } }, "LMStudioModel": { "versions": { - "0.3.0": "da4b3b155dc4" + "0.3.0": "da4b3b155dc4", + "0.3.1": "da4b3b155dc4" } }, "Maritalk": { "versions": { - "0.3.0": "25c5da31181e" + "0.3.0": "25c5da31181e", + "0.3.1": "25c5da31181e" } }, "mem0_chat_memory": { "versions": { - "0.3.0": "309abc9375f4" + "0.3.0": "309abc9375f4", + "0.3.1": "309abc9375f4" } }, "Milvus": { "versions": { - "0.3.0": "2b5efe313b31" + "0.3.0": "2b5efe313b31", + "0.3.1": "2b5efe313b31" } }, "MistralModel": { "versions": { - "0.3.0": "e21780948144" + "0.3.0": "e21780948144", + "0.3.1": "e21780948144" } }, "MistalAIEmbeddings": { "versions": { - "0.3.0": "42ea92fd2df2" + "0.3.0": "42ea92fd2df2", + "0.3.1": "42ea92fd2df2" } }, "Agent": { "versions": { - "0.3.0": "108da32d83f1" + "0.3.0": "108da32d83f1", + "0.3.1": "40d1976f4718" } }, "EmbeddingModel": { "versions": { - "0.3.0": "c5ce0982da48" + "0.3.0": "c5ce0982da48", + "0.3.1": "b5cf1a06bba8" } }, "LanguageModelComponent": { "versions": { - "0.3.0": "4af3c0cc0dcf" + "0.3.0": "4af3c0cc0dcf", + "0.3.1": "e9233312b063" } }, "MCPTools": { "versions": { - "0.3.0": "a3700ab467a1" + "0.3.0": "a3700ab467a1", + "0.3.1": "a3700ab467a1" } }, "Memory": { "versions": { - "0.3.0": "460243b16a3a" + "0.3.0": "460243b16a3a", + "0.3.1": "460243b16a3a" } }, "Prompt Template": { "versions": { - "0.3.0": "5b3e6730923e" + "0.3.0": "5b3e6730923e", + "0.3.1": "5b3e6730923e" } }, "MongoDBAtlasVector": { "versions": { - "0.3.0": "3a502cb4d313" + "0.3.0": "3a502cb4d313", + "0.3.1": "3a502cb4d313" } }, "needle": { "versions": { - "0.3.0": "5f6cedaa0217" + "0.3.0": "5f6cedaa0217", + "0.3.1": "5f6cedaa0217" } }, "NotDiamond": { "versions": { - "0.3.0": "a26ebf501f67" + "0.3.0": "a26ebf501f67", + "0.3.1": "a26ebf501f67" } }, "NovitaModel": { "versions": { - "0.3.0": "2142c4dd2b64" + "0.3.0": "2142c4dd2b64", + "0.3.1": "2142c4dd2b64" } }, "NVIDIAModelComponent": { "versions": { - "0.3.0": "fd6cae31c2a0" + "0.3.0": "fd6cae31c2a0", + "0.3.1": "fd6cae31c2a0" } }, "NVIDIAEmbeddingsComponent": { "versions": { - "0.3.0": "0fded038082d" + "0.3.0": "0fded038082d", + "0.3.1": "0fded038082d" } }, "NvidiaIngestComponent": { "versions": { - "0.3.0": "ff0c6660d991" + "0.3.0": "ff0c6660d991", + "0.3.1": "ff0c6660d991" } }, "NvidiaRerankComponent": { "versions": { - "0.3.0": "cf3976b241f1" + "0.3.0": "cf3976b241f1", + "0.3.1": "cf3976b241f1" } }, "NvidiaSystemAssistComponent": { "versions": { - "0.3.0": "1affc5dcebe1" + "0.3.0": "1affc5dcebe1", + "0.3.1": "1affc5dcebe1" } }, "OlivyaComponent": { "versions": { - "0.3.0": "b5a5c201c4e0" + "0.3.0": "b5a5c201c4e0", + "0.3.1": "b5a5c201c4e0" } }, "OllamaModel": { "versions": { - "0.3.0": "2aa7e6ecf48c" + "0.3.0": "2aa7e6ecf48c", + "0.3.1": "2aa7e6ecf48c" } }, "OllamaEmbeddings": { "versions": { - "0.3.0": "a8c56d0835de" + "0.3.0": "a8c56d0835de", + "0.3.1": "a8c56d0835de" } }, "OpenAIEmbeddings": { "versions": { - "0.3.0": "8a658ed6d4c9" + "0.3.0": "8a658ed6d4c9", + "0.3.1": "8a658ed6d4c9" } }, "OpenAIModel": { "versions": { - "0.3.0": "128d8ef661d4" + "0.3.0": "128d8ef661d4", + "0.3.1": "128d8ef661d4" } }, "OpenRouterComponent": { "versions": { - "0.3.0": "83c3c312a7a2" + "0.3.0": "83c3c312a7a2", + "0.3.1": "83c3c312a7a2" } }, "PerplexityModel": { "versions": { - "0.3.0": "b970844e376e" + "0.3.0": "b970844e376e", + "0.3.1": "b970844e376e" } }, "pgvector": { "versions": { - "0.3.0": "50117607bf5e" + "0.3.0": "50117607bf5e", + "0.3.1": "50117607bf5e" } }, "Pinecone": { "versions": { - "0.3.0": "564ca0a0e9ab" + "0.3.0": "564ca0a0e9ab", + "0.3.1": "564ca0a0e9ab" } }, "AlterMetadata": { "versions": { - "0.3.0": "a209b85f75c1" + "0.3.0": "a209b85f75c1", + "0.3.1": "a209b85f75c1" } }, "CombineText": { "versions": { - "0.3.0": "10ffb6543dd9" + "0.3.0": "10ffb6543dd9", + "0.3.1": "10ffb6543dd9" } }, "TypeConverterComponent": { "versions": { - "0.3.0": "6ce26e994c2d" + "0.3.0": "6ce26e994c2d", + "0.3.1": "6ce26e994c2d" } }, "CreateData": { "versions": { - "0.3.0": "10b0eae5a063" + "0.3.0": "10b0eae5a063", + "0.3.1": "10b0eae5a063" } }, "CreateList": { "versions": { - "0.3.0": "565738357961" + "0.3.0": "565738357961", + "0.3.1": "565738357961" } }, "DataOperations": { "versions": { - "0.3.0": "957fe86b2c4f" + "0.3.0": "957fe86b2c4f", + "0.3.1": "957fe86b2c4f" } }, "DataToDataFrame": { "versions": { - "0.3.0": "edcdf6feefd2" + "0.3.0": "edcdf6feefd2", + "0.3.1": "edcdf6feefd2" } }, "DataFrameOperations": { "versions": { - "0.3.0": "3a3aca2d9d1f" + "0.3.0": "3a3aca2d9d1f", + "0.3.1": "3a3aca2d9d1f" } }, "DynamicCreateData": { "versions": { - "0.3.0": "8af479187c18" + "0.3.0": "8af479187c18", + "0.3.1": "8af479187c18" } }, "ExtractaKey": { "versions": { - "0.3.0": "eded3f4e1533" + "0.3.0": "eded3f4e1533", + "0.3.1": "eded3f4e1533" } }, "FilterData": { "versions": { - "0.3.0": "5f364efb79fc" + "0.3.0": "5f364efb79fc", + "0.3.1": "5f364efb79fc" } }, "FilterDataValues": { "versions": { - "0.3.0": "274c9e3a6e7e" + "0.3.0": "274c9e3a6e7e", + "0.3.1": "274c9e3a6e7e" } }, "JSONCleaner": { "versions": { - "0.3.0": "5c150997aec4" + "0.3.0": "5c150997aec4", + "0.3.1": "5c150997aec4" } }, "MergeDataComponent": { "versions": { - "0.3.0": "3d8c0fa8f47c" + "0.3.0": "3d8c0fa8f47c", + "0.3.1": "3d8c0fa8f47c" } }, "MessagetoData": { "versions": { - "0.3.0": "cc86df1d6415" + "0.3.0": "cc86df1d6415", + "0.3.1": "cc86df1d6415" } }, "OutputParser": { "versions": { - "0.3.0": "9beb62a3e8fb" + "0.3.0": "9beb62a3e8fb", + "0.3.1": "9beb62a3e8fb" } }, "ParseData": { "versions": { - "0.3.0": "73e818f86943" + "0.3.0": "73e818f86943", + "0.3.1": "73e818f86943" } }, "ParseDataFrame": { "versions": { - "0.3.0": "af6b7e66d77e" + "0.3.0": "af6b7e66d77e", + "0.3.1": "af6b7e66d77e" } }, "ParseJSONData": { "versions": { - "0.3.0": "2ad980f8bac3" + "0.3.0": "2ad980f8bac3", + "0.3.1": "2ad980f8bac3" } }, "ParserComponent": { "versions": { - "0.3.0": "cda7b997a730" + "0.3.0": "cda7b997a730", + "0.3.1": "cda7b997a730" } }, "RegexExtractorComponent": { "versions": { - "0.3.0": "6e5d844f29b3" + "0.3.0": "6e5d844f29b3", + "0.3.1": "6e5d844f29b3" } }, "SelectData": { "versions": { - "0.3.0": "943bab86d962" + "0.3.0": "943bab86d962", + "0.3.1": "943bab86d962" } }, "SplitText": { "versions": { - "0.3.0": "859adebdf672" + "0.3.0": "859adebdf672", + "0.3.1": "859adebdf672" } }, "StoreMessage": { "versions": { - "0.3.0": "9ad1aa08b597" + "0.3.0": "9ad1aa08b597", + "0.3.1": "9ad1aa08b597" } }, "TextOperations": { "versions": { - "0.3.0": "008b8a7b612e" + "0.3.0": "008b8a7b612e", + "0.3.1": "008b8a7b612e" } }, "UpdateData": { "versions": { - "0.3.0": "7d171034c729" + "0.3.0": "7d171034c729", + "0.3.1": "7d171034c729" } }, "PythonFunction": { "versions": { - "0.3.0": "55dc87cf0979" + "0.3.0": "55dc87cf0979", + "0.3.1": "55dc87cf0979" } }, "QdrantVectorStoreComponent": { "versions": { - "0.3.0": "bcfc1728e7b1" + "0.3.0": "bcfc1728e7b1", + "0.3.1": "bcfc1728e7b1" } }, "Redis": { "versions": { - "0.3.0": "fec4041f4017" + "0.3.0": "fec4041f4017", + "0.3.1": "fec4041f4017" } }, "RedisChatMemory": { "versions": { - "0.3.0": "01ec6e1e34a8" + "0.3.0": "01ec6e1e34a8", + "0.3.1": "01ec6e1e34a8" } }, "SambaNovaModel": { "versions": { - "0.3.0": "f12728900b8d" + "0.3.0": "f12728900b8d", + "0.3.1": "f12728900b8d" } }, "ScrapeGraphMarkdownifyApi": { "versions": { - "0.3.0": "c17524dbca7a" + "0.3.0": "c17524dbca7a", + "0.3.1": "c17524dbca7a" } }, "ScrapeGraphSearchApi": { "versions": { - "0.3.0": "4caa0e09ea85" + "0.3.0": "4caa0e09ea85", + "0.3.1": "4caa0e09ea85" } }, "ScrapeGraphSmartScraperApi": { "versions": { - "0.3.0": "229446ce1e37" + "0.3.0": "229446ce1e37", + "0.3.1": "229446ce1e37" } }, "SearchComponent": { "versions": { - "0.3.0": "766aee1dff00" + "0.3.0": "766aee1dff00", + "0.3.1": "766aee1dff00" } }, "Serp": { "versions": { - "0.3.0": "85a6736d5bb3" + "0.3.0": "85a6736d5bb3", + "0.3.1": "85a6736d5bb3" } }, "SupabaseVectorStore": { "versions": { - "0.3.0": "5045b81c340b" + "0.3.0": "5045b81c340b", + "0.3.1": "5045b81c340b" } }, "TavilyExtractComponent": { "versions": { - "0.3.0": "86266b25a045" + "0.3.0": "86266b25a045", + "0.3.1": "86266b25a045" } }, "TavilySearchComponent": { "versions": { - "0.3.0": "5638a305a99c" + "0.3.0": "5638a305a99c", + "0.3.1": "5638a305a99c" } }, "CalculatorTool": { "versions": { - "0.3.0": "30008a17eb27" + "0.3.0": "30008a17eb27", + "0.3.1": "30008a17eb27" } }, "GoogleSearchAPI": { "versions": { - "0.3.0": "5f47bcd2163c" + "0.3.0": "5f47bcd2163c", + "0.3.1": "5f47bcd2163c" } }, "GoogleSerperAPI": { "versions": { - "0.3.0": "f51b15d26440" + "0.3.0": "f51b15d26440", + "0.3.1": "f51b15d26440" } }, "PythonCodeStructuredTool": { "versions": { - "0.3.0": "3f913a303e47" + "0.3.0": "3f913a303e47", + "0.3.1": "3f913a303e47" } }, "PythonREPLTool": { "versions": { - "0.3.0": "695ab612478d" + "0.3.0": "695ab612478d", + "0.3.1": "695ab612478d" } }, "SearchAPI": { "versions": { - "0.3.0": "4a9c8b914b68" + "0.3.0": "4a9c8b914b68", + "0.3.1": "4a9c8b914b68" } }, "SearXNGTool": { "versions": { - "0.3.0": "154e292dad2b" + "0.3.0": "154e292dad2b", + "0.3.1": "154e292dad2b" } }, "SerpAPI": { "versions": { - "0.3.0": "882d7b35a9d7" + "0.3.0": "882d7b35a9d7", + "0.3.1": "882d7b35a9d7" } }, "TavilyAISearch": { "versions": { - "0.3.0": "70b5611e8fd6" + "0.3.0": "70b5611e8fd6", + "0.3.1": "70b5611e8fd6" } }, "WikidataAPI": { "versions": { - "0.3.0": "4752066f8971" + "0.3.0": "4752066f8971", + "0.3.1": "4752066f8971" } }, "WikipediaAPI": { "versions": { - "0.3.0": "d7ae953444c9" + "0.3.0": "d7ae953444c9", + "0.3.1": "d7ae953444c9" } }, "YahooFinanceTool": { "versions": { - "0.3.0": "fda358f6395e" + "0.3.0": "fda358f6395e", + "0.3.1": "fda358f6395e" } }, "ConvertAstraToTwelveLabs": { "versions": { - "0.3.0": "2a65cbf14ce5" + "0.3.0": "2a65cbf14ce5", + "0.3.1": "2a65cbf14ce5" } }, "TwelveLabsPegasusIndexVideo": { "versions": { - "0.3.0": "faa14b3d6a18" + "0.3.0": "faa14b3d6a18", + "0.3.1": "faa14b3d6a18" } }, "SplitVideo": { "versions": { - "0.3.0": "56ccb4106c30" + "0.3.0": "56ccb4106c30", + "0.3.1": "56ccb4106c30" } }, "TwelveLabsTextEmbeddings": { "versions": { - "0.3.0": "5c7501f5088c" + "0.3.0": "5c7501f5088c", + "0.3.1": "5c7501f5088c" } }, "TwelveLabsPegasus": { "versions": { - "0.3.0": "92cc032822a6" + "0.3.0": "92cc032822a6", + "0.3.1": "92cc032822a6" } }, "TwelveLabsVideoEmbeddings": { "versions": { - "0.3.0": "294e3539629c" + "0.3.0": "294e3539629c", + "0.3.1": "294e3539629c" } }, "VideoFile": { "versions": { - "0.3.0": "8dc827e42377" + "0.3.0": "8dc827e42377", + "0.3.1": "8dc827e42377" } }, "Unstructured": { "versions": { - "0.3.0": "bbb4e71aee8e" + "0.3.0": "bbb4e71aee8e", + "0.3.1": "bbb4e71aee8e" } }, "Upstash": { "versions": { - "0.3.0": "b5b2d78e8c44" + "0.3.0": "b5b2d78e8c44", + "0.3.1": "b5b2d78e8c44" } }, "CalculatorComponent": { "versions": { - "0.3.0": "37caa1aba62c" + "0.3.0": "37caa1aba62c", + "0.3.1": "37caa1aba62c" } }, "CurrentDate": { "versions": { - "0.3.0": "4a93d7b489e6" + "0.3.0": "4a93d7b489e6", + "0.3.1": "4a93d7b489e6" } }, "IDGenerator": { "versions": { - "0.3.0": "cddb8b4edbc3" + "0.3.0": "cddb8b4edbc3", + "0.3.1": "cddb8b4edbc3" } }, "PythonREPLComponent": { "versions": { - "0.3.0": "80eeaf032b83" + "0.3.0": "80eeaf032b83", + "0.3.1": "80eeaf032b83" } }, "Vectara": { "versions": { - "0.3.0": "a2309e046c06" + "0.3.0": "a2309e046c06", + "0.3.1": "a2309e046c06" } }, "VectaraRAG": { "versions": { - "0.3.0": "123c9eef9191" + "0.3.0": "123c9eef9191", + "0.3.1": "123c9eef9191" } }, "LocalDB": { "versions": { - "0.3.0": "457b336fd756" + "0.3.0": "457b336fd756", + "0.3.1": "457b336fd756" } }, "VertexAiModel": { "versions": { - "0.3.0": "d9f75281d3fa" + "0.3.0": "d9f75281d3fa", + "0.3.1": "d9f75281d3fa" } }, "VertexAIEmbeddings": { "versions": { - "0.3.0": "3abd5c15264a" + "0.3.0": "3abd5c15264a", + "0.3.1": "3abd5c15264a" } }, "vLLMModel": { "versions": { - "0.3.0": "ed6c1aa73200" + "0.3.0": "ed6c1aa73200", + "0.3.1": "ed6c1aa73200" } }, "vLLMEmbeddings": { "versions": { - "0.3.0": "079b6a2dd397" + "0.3.0": "079b6a2dd397", + "0.3.1": "079b6a2dd397" } }, "VLMRunTranscription": { "versions": { - "0.3.0": "c91e2349a3df" + "0.3.0": "c91e2349a3df", + "0.3.1": "c91e2349a3df" } }, "Weaviate": { "versions": { - "0.3.0": "ccc9f1e0149d" + "0.3.0": "ccc9f1e0149d", + "0.3.1": "ccc9f1e0149d" } }, "WikidataComponent": { "versions": { - "0.3.0": "5f6398d72116" + "0.3.0": "5f6398d72116", + "0.3.1": "5f6398d72116" } }, "WikipediaComponent": { "versions": { - "0.3.0": "e1b58c8ac595" + "0.3.0": "e1b58c8ac595", + "0.3.1": "e1b58c8ac595" } }, "WolframAlphaAPI": { "versions": { - "0.3.0": "44e79cb8a924" + "0.3.0": "44e79cb8a924", + "0.3.1": "44e79cb8a924" } }, "xAIModel": { "versions": { - "0.3.0": "ef0eb1cfadeb" + "0.3.0": "ef0eb1cfadeb", + "0.3.1": "ef0eb1cfadeb" } }, "YfinanceComponent": { "versions": { - "0.3.0": "14ca8af63c82" + "0.3.0": "14ca8af63c82", + "0.3.1": "14ca8af63c82" } }, "YouTubeChannelComponent": { "versions": { - "0.3.0": "c889afb883fa" + "0.3.0": "c889afb883fa", + "0.3.1": "c889afb883fa" } }, "YouTubeCommentsComponent": { "versions": { - "0.3.0": "20398e0d18df" + "0.3.0": "20398e0d18df", + "0.3.1": "20398e0d18df" } }, "YouTubePlaylistComponent": { "versions": { - "0.3.0": "2d6ac9665d53" + "0.3.0": "2d6ac9665d53", + "0.3.1": "2d6ac9665d53" } }, "YouTubeSearchComponent": { "versions": { - "0.3.0": "1c4a94a094e2" + "0.3.0": "1c4a94a094e2", + "0.3.1": "1c4a94a094e2" } }, "YouTubeTrendingComponent": { "versions": { - "0.3.0": "1450a5529486" + "0.3.0": "1450a5529486", + "0.3.1": "1450a5529486" } }, "YouTubeVideoDetailsComponent": { "versions": { - "0.3.0": "6c2be0d5450b" + "0.3.0": "6c2be0d5450b", + "0.3.1": "6c2be0d5450b" } }, "YouTubeTranscripts": { "versions": { - "0.3.0": "0ed75539d58d" + "0.3.0": "0ed75539d58d", + "0.3.1": "0ed75539d58d" } }, "ZepChatMemory": { "versions": { - "0.3.0": "ec825c33caf6" + "0.3.0": "ec825c33caf6", + "0.3.1": "ec825c33caf6" } }, "GuardrailValidator": { "versions": { - "0.3.0": "70918cbb8522" + "0.3.0": "70918cbb8522", + "0.3.1": "70918cbb8522" } }, "LiteLLMProxyModel": { "versions": { - "0.3.0": "386ae52865b5" + "0.3.0": "386ae52865b5", + "0.3.1": "386ae52865b5" } }, "SemanticAggregator": { "versions": { - "0.3.0": "080199fa8b09" + "0.3.0": "080199fa8b09", + "0.3.1": "080199fa8b09" } }, "SemanticMap": { "versions": { - "0.3.0": "ab1e08451407" + "0.3.0": "ab1e08451407", + "0.3.1": "ab1e08451407" } }, "SyntheticDataGenerator": { "versions": { - "0.3.0": "677579fcf15f" + "0.3.0": "677579fcf15f", + "0.3.1": "677579fcf15f" } }, "KnowledgeBase": { "versions": { - "0.3.0": "8b5ca1f38f6e" + "0.3.0": "8b5ca1f38f6e", + "0.3.1": "8b5ca1f38f6e" } } } \ No newline at end of file diff --git a/src/lfx/src/lfx/base/models/unified_models.py b/src/lfx/src/lfx/base/models/unified_models.py index 6178593d35e8..f4dfb6b8ddf1 100644 --- a/src/lfx/src/lfx/base/models/unified_models.py +++ b/src/lfx/src/lfx/base/models/unified_models.py @@ -2,6 +2,7 @@ import importlib import os +import re from functools import lru_cache from typing import TYPE_CHECKING, Any from uuid import UUID @@ -204,31 +205,36 @@ def get_provider_required_variable_keys(provider: str) -> list[str]: return [v["variable_key"] for v in variables if v.get("required")] +def _get_all_provider_specific_field_names() -> set[str]: + """Return set of all field names used as mapping_field by any provider.""" + names: set[str] = set() + for meta in model_provider_metadata.values(): + for v in meta.get("variables", []): + mapping = v.get("component_metadata", {}).get("mapping_field") + if mapping: + names.add(mapping) + return names + + def apply_provider_variable_config_to_build_config( build_config: dict, provider: str, ) -> dict: """Apply provider variable metadata to component build config fields. - This function updates the build config fields based on the provider's variable metadata - stored in the `component_metadata` nested dict: - - Sets `required` based on `component_metadata.required` - - Sets `advanced` based on `component_metadata.advanced` - - Sets `info` based on `component_metadata.info` - - Sets `show` to True for fields that have a mapping_field for this provider - - Args: - build_config: The component's build configuration dict - provider: The selected provider name (e.g., "OpenAI", "IBM WatsonX") - - Returns: - Updated build_config dict + First hides all provider-specific fields (so switching e.g. IBM -> OpenAI + does not leave IBM fields visible), then shows and configures only the + current provider's fields. """ import os - provider_vars = get_provider_all_variables(provider) + all_provider_fields = _get_all_provider_specific_field_names() + for field_name in all_provider_fields: + if field_name in build_config: + build_config[field_name]["show"] = False + build_config[field_name]["required"] = False - # Build a lookup by component_metadata.mapping_field + provider_vars = get_provider_all_variables(provider) vars_by_field = {} for v in provider_vars: component_meta = v.get("component_metadata", {}) @@ -256,23 +262,19 @@ def apply_provider_variable_config_to_build_config( if info: field_config["info"] = info - # Show the field since it's relevant to this provider field_config["show"] = True - # If no value is set, try to get from environment variable env_var_key = var_info.get("variable_key") if env_var_key: - current_value = field_config.get("value") - # Only set from env if field is empty/None - if not current_value or (isinstance(current_value, str) and not current_value.strip()): - env_value = os.environ.get(env_var_key) - if env_value and env_value.strip(): - field_config["value"] = env_value - logger.debug( - "Set field %s from environment variable %s", - field_name, - env_var_key, - ) + env_value = os.environ.get(env_var_key) + if env_value and str(env_value).strip(): + field_config["value"] = env_var_key + field_config["load_from_db"] = True + logger.debug( + "Set field %s to env var name %s (value resolved at runtime)", + field_name, + env_var_key, + ) return build_config @@ -301,6 +303,17 @@ def get_model_providers() -> list[str]: return sorted({md.get("provider", "Unknown") for group in MODELS_DETAILED for md in group}) +def get_provider_for_model_name(model_name: str) -> str: + """Return the provider for a model name by searching MODELS_DETAILED.""" + if not model_name or not isinstance(model_name, str): + return "" + for group in MODELS_DETAILED: + for md in group: + if md.get("name") == model_name: + return md.get("provider", "") or "" + return "" + + def get_unified_models_detailed( providers: list[str] | None = None, model_name: str | None = None, @@ -414,17 +427,58 @@ def get_unified_models_detailed( def get_api_key_for_provider(user_id: UUID | str | None, provider: str, api_key: str | None = None) -> str | None: """Get API key from self.api_key or global variables. + When api_key is set to an environment variable name (e.g. ANTHROPIC_API_KEY), + that name is resolved from os.environ or global variables so imported flows + can reference credentials without storing the raw key. + Args: user_id: The user ID to look up global variables for provider: The provider name (e.g., "OpenAI", "Anthropic") - api_key: An optional API key provided directly + api_key: An optional API key provided directly, or an env var name to resolve Returns: The API key if found, None otherwise """ - # First check if user provided an API key directly - if api_key: - return api_key + + # Resolve variable name (canonical or custom e.g. MY_OPENAI_API_KEY) from env or global vars + def _resolve_var_name(var_name: str) -> str | None: + env_value = os.environ.get(var_name) + if env_value and env_value.strip(): + return env_value.strip() + if user_id and not (isinstance(user_id, str) and user_id == "None"): + + async def _get_by_var_name(): + async with session_scope() as session: + variable_service = get_variable_service() + if variable_service is None: + return None + try: + return await variable_service.get_variable( + user_id=UUID(user_id) if isinstance(user_id, str) else user_id, + name=var_name, + field="", + session=session, + ) + except ValueError: + return None + + value = run_until_complete(_get_by_var_name()) + if value and str(value).strip(): + return str(value).strip() + return None + + if api_key and api_key.strip(): + var_name = api_key.strip() + # Names that look like env/global variables (e.g. MY_OPENAI_API_KEY): resolve from env/DB + if var_name.replace("_", "").isalnum() and var_name[0].isalpha(): + resolved = _resolve_var_name(var_name) + if resolved: + return resolved + # Unresolved variable name: don't use as literal key + if re.match(r"^[A-Z][A-Z0-9_]*$", var_name): + return None + # Literal API key (e.g. sk-...) + return var_name # If no user_id or user_id is the string "None", we can't look up global variables if user_id is None or (isinstance(user_id, str) and user_id == "None"): @@ -940,17 +994,23 @@ def __init__(self, value): param_mapping = get_provider_param_mapping(provider) # Build the option dict + # Get provider-level metadata for max_tokens field name + provider_meta = model_provider_metadata.get(provider, {}) + option_metadata = { + "context_length": 128000, # Default, can be overridden + "model_class": param_mapping.get("model_class", "ChatOpenAI"), + "model_name_param": param_mapping.get("model_param", "model"), + "api_key_param": param_mapping.get("api_key_param", "api_key"), + } + if "max_tokens_field_name" in provider_meta: + option_metadata["max_tokens_field_name"] = provider_meta["max_tokens_field_name"] + option = { "name": model_name, "icon": icon, "category": provider, "provider": provider, - "metadata": { - "context_length": 128000, # Default, can be overridden - "model_class": param_mapping.get("model_class", "ChatOpenAI"), - "model_name_param": param_mapping.get("model_param", "model"), - "api_key_param": param_mapping.get("api_key_param", "api_key"), - }, + "metadata": option_metadata, } # Add reasoning models list for OpenAI @@ -1405,7 +1465,12 @@ def get_llm( try: max_tokens_int = int(max_tokens) if max_tokens_int >= 1: - max_tokens_param = metadata.get("max_tokens_field_name", "max_tokens") + # Look up provider-specific field name from model metadata first, + # then fall back to provider metadata, then default to "max_tokens" + max_tokens_param = metadata.get("max_tokens_field_name") + if not max_tokens_param: + provider_meta = model_provider_metadata.get(provider, {}) + max_tokens_param = provider_meta.get("max_tokens_field_name", "max_tokens") kwargs[max_tokens_param] = max_tokens_int except (TypeError, ValueError): pass # Skip invalid max_tokens (e.g. empty string from form input) diff --git a/src/lfx/src/lfx/components/llm_operations/lambda_filter.py b/src/lfx/src/lfx/components/llm_operations/lambda_filter.py index 9524180f72d6..8d00cdc1c03d 100644 --- a/src/lfx/src/lfx/components/llm_operations/lambda_filter.py +++ b/src/lfx/src/lfx/components/llm_operations/lambda_filter.py @@ -6,8 +6,10 @@ from typing import Any from lfx.base.models.unified_models import ( + apply_provider_variable_config_to_build_config, get_language_model_options, get_llm, + get_provider_for_model_name, update_model_options_in_build_config, ) from lfx.custom.custom_component.component import Component @@ -116,7 +118,7 @@ class LambdaFilterComponent(Component): def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None): """Dynamically update build config with user-filtered model options.""" - return update_model_options_in_build_config( + build_config = update_model_options_in_build_config( component=self, build_config=build_config, cache_key_prefix="language_model_options", @@ -125,6 +127,19 @@ def update_build_config(self, build_config: dict, field_value: str, field_name: field_value=field_value, ) + current_model_value = field_value if field_name == "model" else build_config.get("model", {}).get("value") + provider = "" + if isinstance(current_model_value, list) and current_model_value: + selected_model = current_model_value[0] + provider = (selected_model.get("provider") or "").strip() + if not provider and selected_model.get("name"): + provider = get_provider_for_model_name(str(selected_model["name"])) + + if provider: + build_config = apply_provider_variable_config_to_build_config(build_config, provider) + + return build_config + def get_data_structure(self, data): """Extract the structure of data, replacing values with their types.""" if isinstance(data, list): diff --git a/src/lfx/src/lfx/components/llm_operations/llm_conditional_router.py b/src/lfx/src/lfx/components/llm_operations/llm_conditional_router.py index 32b1df194168..028d599cb892 100644 --- a/src/lfx/src/lfx/components/llm_operations/llm_conditional_router.py +++ b/src/lfx/src/lfx/components/llm_operations/llm_conditional_router.py @@ -1,8 +1,10 @@ from typing import Any from lfx.base.models.unified_models import ( + apply_provider_variable_config_to_build_config, get_language_model_options, get_llm, + get_provider_for_model_name, update_model_options_in_build_config, ) from lfx.custom import Component @@ -136,7 +138,7 @@ def __init__(self, **kwargs): def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None): """Dynamically update build config with user-filtered model options.""" - return update_model_options_in_build_config( + build_config = update_model_options_in_build_config( component=self, build_config=build_config, cache_key_prefix="language_model_options", @@ -145,6 +147,19 @@ def update_build_config(self, build_config: dict, field_value: str, field_name: field_value=field_value, ) + current_model_value = field_value if field_name == "model" else build_config.get("model", {}).get("value") + provider = "" + if isinstance(current_model_value, list) and current_model_value: + selected_model = current_model_value[0] + provider = (selected_model.get("provider") or "").strip() + if not provider and selected_model.get("name"): + provider = get_provider_for_model_name(str(selected_model["name"])) + + if provider: + build_config = apply_provider_variable_config_to_build_config(build_config, provider) + + return build_config + def update_outputs(self, frontend_node: dict, field_name: str, field_value: Any) -> dict: """Create a dynamic output for each category in the categories table.""" if field_name in {"routes", "enable_else_output", "model"}: diff --git a/src/lfx/src/lfx/components/models_and_agents/agent.py b/src/lfx/src/lfx/components/models_and_agents/agent.py index 0fa08f5e6c7c..044aa1dcfdb7 100644 --- a/src/lfx/src/lfx/components/models_and_agents/agent.py +++ b/src/lfx/src/lfx/components/models_and_agents/agent.py @@ -17,6 +17,7 @@ apply_provider_variable_config_to_build_config, get_language_model_options, get_llm, + get_provider_for_model_name, update_model_options_in_build_config, ) from lfx.base.models.watsonx_constants import IBM_WATSONX_URLS @@ -185,21 +186,29 @@ class AgentComponent(ToolCallingAgentComponent): Output(name="response", display_name="Response", method="message_response"), ] - async def get_agent_requirements(self): - """Get the agent requirements for the agent.""" - from langchain_core.tools import StructuredTool + def _get_max_tokens_value(self): + """Return the user-supplied max_tokens or None when unset/zero.""" + val = getattr(self, "max_tokens", None) + if val in {"", 0}: + return None + return val - max_tokens_val = getattr(self, "max_tokens", None) - if max_tokens_val in {"", 0}: - max_tokens_val = None - llm_model = get_llm( + def _get_llm(self): + """Override parent to include max_tokens from the Agent's input field.""" + return get_llm( model=self.model, user_id=self.user_id, - api_key=self.api_key, - max_tokens=max_tokens_val, + api_key=getattr(self, "api_key", None), + max_tokens=self._get_max_tokens_value(), watsonx_url=getattr(self, "base_url_ibm_watsonx", None), watsonx_project_id=getattr(self, "project_id", None), ) + + async def get_agent_requirements(self): + """Get the agent requirements for the agent.""" + from langchain_core.tools import StructuredTool + + llm_model = self._get_llm() if llm_model is None: msg = "No language model selected. Please choose a model to proceed." raise ValueError(msg) @@ -481,29 +490,21 @@ def get_tool_calling_model_options(user_id=None): ) build_config = dotdict(build_config) - # Iterate over all providers in the MODEL_PROVIDERS_DICT if field_name == "model": - # Update input types for all fields build_config = self.update_input_types(build_config) - # Show/hide provider-specific fields based on selected model - # Get current model value - from field_value if model is being changed, otherwise from build_config - current_model_value = field_value if field_name == "model" else build_config.get("model", {}).get("value") - if isinstance(current_model_value, list) and len(current_model_value) > 0: - selected_model = current_model_value[0] - provider = selected_model.get("provider", "") + current_model_value = field_value if field_name == "model" else build_config.get("model", {}).get("value") + provider = "" + if isinstance(current_model_value, list) and current_model_value: + selected_model = current_model_value[0] + provider = (selected_model.get("provider") or "").strip() + if not provider and selected_model.get("name"): + provider = get_provider_for_model_name(str(selected_model["name"])) - # Hide provider-specific fields by default before applying provider config - for field in ["base_url_ibm_watsonx", "project_id"]: - if field in build_config: - build_config[field]["show"] = False - build_config[field]["required"] = False + if provider: + build_config = apply_provider_variable_config_to_build_config(build_config, provider) - # Apply provider variable configuration (advanced, required, info, env var fallback) - if provider: - build_config = apply_provider_variable_config_to_build_config(build_config, provider) - - # Validate required keys + if field_name == "model": default_keys = [ "code", "_type", diff --git a/src/lfx/src/lfx/components/models_and_agents/embedding_model.py b/src/lfx/src/lfx/components/models_and_agents/embedding_model.py index a6103313bcd3..8ea61cd211fc 100644 --- a/src/lfx/src/lfx/components/models_and_agents/embedding_model.py +++ b/src/lfx/src/lfx/components/models_and_agents/embedding_model.py @@ -3,9 +3,11 @@ from lfx.base.embeddings.embeddings_class import EmbeddingsWithModels from lfx.base.embeddings.model import LCEmbeddingsModel from lfx.base.models.unified_models import ( + apply_provider_variable_config_to_build_config, get_api_key_for_provider, get_embedding_class, get_embedding_model_options, + get_provider_for_model_name, get_unified_models_detailed, update_model_options_in_build_config, ) @@ -34,7 +36,6 @@ class EmbeddingModelComponent(LCEmbeddingsModel): def update_build_config(self, build_config: dict, field_value: str, field_name: str | None = None): """Dynamically update build config with user-filtered model options.""" - # Update model options build_config = update_model_options_in_build_config( component=self, build_config=build_config, @@ -44,20 +45,23 @@ def update_build_config(self, build_config: dict, field_value: str, field_name: field_value=field_value, ) - # Show/hide provider-specific fields based on selected model - if field_name == "model" and isinstance(field_value, list) and len(field_value) > 0: - selected_model = field_value[0] - provider = selected_model.get("provider", "") + current_model_value = field_value if field_name == "model" else build_config.get("model", {}).get("value") + provider = "" + if isinstance(current_model_value, list) and current_model_value: + selected_model = current_model_value[0] + provider = (selected_model.get("provider") or "").strip() + if not provider and selected_model.get("name"): + provider = get_provider_for_model_name(str(selected_model["name"])) - # Show/hide watsonx fields + if provider: + build_config = apply_provider_variable_config_to_build_config(build_config, provider) + + # Embedding-specific WatsonX toggles not covered by provider metadata is_watsonx = provider == "IBM WatsonX" - build_config["base_url_ibm_watsonx"]["show"] = is_watsonx - build_config["project_id"]["show"] = is_watsonx - build_config["truncate_input_tokens"]["show"] = is_watsonx - build_config["input_text"]["show"] = is_watsonx - if is_watsonx: - build_config["base_url_ibm_watsonx"]["required"] = True - build_config["project_id"]["required"] = True + if "truncate_input_tokens" in build_config: + build_config["truncate_input_tokens"]["show"] = is_watsonx + if "input_text" in build_config: + build_config["input_text"]["show"] = is_watsonx return build_config diff --git a/src/lfx/src/lfx/components/models_and_agents/language_model.py b/src/lfx/src/lfx/components/models_and_agents/language_model.py index 3e77a7e73e40..ad5cc44e206d 100644 --- a/src/lfx/src/lfx/components/models_and_agents/language_model.py +++ b/src/lfx/src/lfx/components/models_and_agents/language_model.py @@ -3,6 +3,7 @@ apply_provider_variable_config_to_build_config, get_language_model_options, get_llm, + get_provider_for_model_name, update_model_options_in_build_config, ) from lfx.base.models.watsonx_constants import IBM_WATSONX_URLS @@ -122,21 +123,15 @@ def update_build_config(self, build_config: dict, field_value: str, field_name: field_value=field_value, ) - # Hide all provider-specific fields by default - for field in ["api_key", "base_url_ibm_watsonx", "project_id", "ollama_base_url"]: - if field in build_config: - build_config[field]["show"] = False - build_config[field]["required"] = False - - # Show/configure provider-specific fields based on selected model - # Get current model value - from field_value if model is being changed, otherwise from build_config current_model_value = field_value if field_name == "model" else build_config.get("model", {}).get("value") - if isinstance(current_model_value, list) and len(current_model_value) > 0: + provider = "" + if isinstance(current_model_value, list) and current_model_value: selected_model = current_model_value[0] - provider = selected_model.get("provider", "") + provider = (selected_model.get("provider") or "").strip() + if not provider and selected_model.get("name"): + provider = get_provider_for_model_name(str(selected_model["name"])) - if provider: - # Apply provider variable configuration (required_for_component, advanced, env var fallback) - build_config = apply_provider_variable_config_to_build_config(build_config, provider) + if provider: + build_config = apply_provider_variable_config_to_build_config(build_config, provider) return build_config diff --git a/src/lfx/src/lfx/processing/process.py b/src/lfx/src/lfx/processing/process.py index 9214802bfb6c..7adf16f997b6 100644 --- a/src/lfx/src/lfx/processing/process.py +++ b/src/lfx/src/lfx/processing/process.py @@ -196,9 +196,15 @@ def apply_tweaks(node: dict[str, Any], node_tweaks: dict[str, Any]) -> None: for k, v in tweak_value.items(): k_ = "file_path" if field_type == "file" else k template_data[tweak_name][k_] = v + # If the user didn't explicitly set load_from_db in the dict, + # we default to False for the override. + if "load_from_db" not in tweak_value and "load_from_db" in template_data[tweak_name]: + template_data[tweak_name]["load_from_db"] = False else: key = "file_path" if field_type == "file" else "value" template_data[tweak_name][key] = tweak_value + if "load_from_db" in template_data[tweak_name]: + template_data[tweak_name]["load_from_db"] = False def apply_tweaks_on_vertex(vertex: Vertex, node_tweaks: dict[str, Any]) -> None: @@ -206,6 +212,17 @@ def apply_tweaks_on_vertex(vertex: Vertex, node_tweaks: dict[str, Any]) -> None: if tweak_name and tweak_value and tweak_name in vertex.params: vertex.params[tweak_name] = tweak_value + # Determine if we should load from DB + tweak_load_from_db = False + if isinstance(tweak_value, dict): + tweak_load_from_db = tweak_value.get("load_from_db", False) + + if tweak_load_from_db: + if tweak_name not in vertex.load_from_db_fields: + vertex.load_from_db_fields.append(tweak_name) + elif tweak_name in vertex.load_from_db_fields: + vertex.load_from_db_fields.remove(tweak_name) + def process_tweaks( graph_data: dict[str, Any], tweaks: Tweaks | dict[str, dict[str, Any]], *, stream: bool = False diff --git a/src/lfx/tests/unit/inputs/test_max_tokens_propagation.py b/src/lfx/tests/unit/inputs/test_max_tokens_propagation.py new file mode 100644 index 000000000000..c39fa63114bb --- /dev/null +++ b/src/lfx/tests/unit/inputs/test_max_tokens_propagation.py @@ -0,0 +1,346 @@ +"""Regression tests for max_tokens propagation through unified models. + +Verifies that the Agent/LanguageModel max_tokens field reaches the LLM +constructor with the correct provider-specific parameter name. + +Regression: In v1.8.0 the unified models refactor stopped propagating +max_tokens_field_name in the model option metadata returned by +get_language_model_options(). This caused: + - Google Generative AI: max_output_tokens silently dropped (wrong kwarg name) + - Anthropic: always used default 1024 (value never overridden) + - OpenAI: max_tokens absent from payload +""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest +from lfx.base.models.model_metadata import MODEL_PROVIDER_METADATA +from lfx.base.models.unified_models import get_language_model_options, get_llm + +# --------------------------------------------------------------------------- +# 1. get_language_model_options must include max_tokens_field_name +# --------------------------------------------------------------------------- + + +class TestModelOptionsIncludeMaxTokensFieldName: + """Testing the max tokens parameter. + + Every provider option returned by get_language_model_options must carry + the provider's max_tokens_field_name so that get_llm can map the generic + 'max_tokens' input to the correct LLM constructor kwarg. + """ + + def test_all_provider_options_have_max_tokens_field_name(self): + """Each model option must include max_tokens_field_name in metadata.""" + options = get_language_model_options(user_id=None) + + # Group by provider to give a clear failure message + providers_seen: set[str] = set() + for opt in options: + provider = opt.get("provider") + metadata = opt.get("metadata", {}) + + # Skip disabled-provider sentinel entries + if metadata.get("is_disabled_provider"): + continue + + providers_seen.add(provider) + assert "max_tokens_field_name" in metadata, ( + f"Option '{opt.get('name')}' from provider '{provider}' " + "is missing 'max_tokens_field_name' in its metadata" + ) + + # Sanity: we actually checked real providers + assert len(providers_seen) > 0, "No model options were returned" + + @pytest.mark.parametrize( + ("provider", "expected_field"), + [ + ("OpenAI", "max_tokens"), + ("Anthropic", "max_tokens"), + ("Google Generative AI", "max_output_tokens"), + ("Ollama", "max_tokens"), + ("IBM WatsonX", "max_tokens"), + ], + ) + def test_provider_max_tokens_field_name_matches_metadata(self, provider: str, expected_field: str): + """The max_tokens_field_name in options must match MODEL_PROVIDER_METADATA.""" + options = get_language_model_options(user_id=None) + provider_options = [ + o + for o in options + if o.get("provider") == provider and not o.get("metadata", {}).get("is_disabled_provider") + ] + + if not provider_options: + pytest.skip(f"No options returned for provider '{provider}'") + + for opt in provider_options: + actual = opt["metadata"]["max_tokens_field_name"] + assert actual == expected_field, ( + f"Model '{opt['name']}' ({provider}): max_tokens_field_name='{actual}', expected '{expected_field}'" + ) + + +# --------------------------------------------------------------------------- +# 2. get_llm must pass max_tokens under the correct provider-specific kwarg +# --------------------------------------------------------------------------- + + +class TestGetLlmMaxTokensKwarg: + """Testing the max tokens kwarg propagation. + + get_llm must translate the generic max_tokens value into the correct + provider-specific constructor kwarg (e.g. max_output_tokens for Google). + """ + + @staticmethod + def _make_model_selection(provider: str, model_name: str = "test-model") -> list[dict]: + """Build a minimal model selection list as the Agent would pass to get_llm.""" + provider_meta = MODEL_PROVIDER_METADATA.get(provider, {}) + mapping = provider_meta.get("mapping", {}) + metadata = { + "model_class": mapping.get("model_class", "ChatOpenAI"), + "model_name_param": mapping.get("model_param", "model"), + "api_key_param": "api_key", + } + # Include max_tokens_field_name (the fix under test) + if "max_tokens_field_name" in provider_meta: + metadata["max_tokens_field_name"] = provider_meta["max_tokens_field_name"] + return [{"name": model_name, "provider": provider, "metadata": metadata}] + + @pytest.mark.parametrize( + ("provider", "expected_kwarg"), + [ + ("OpenAI", "max_tokens"), + ("Anthropic", "max_tokens"), + ("Google Generative AI", "max_output_tokens"), + ], + ) + def test_max_tokens_kwarg_name_per_provider(self, provider: str, expected_kwarg: str): + """get_llm must pass the user's max_tokens value under the correct kwarg.""" + model_selection = self._make_model_selection(provider) + + mock_cls = MagicMock() + with ( + patch( + "lfx.base.models.unified_models.get_model_class", + return_value=mock_cls, + ), + patch( + "lfx.base.models.unified_models.get_api_key_for_provider", + return_value="fake-key", + ), + ): + get_llm( + model=model_selection, + user_id=None, + api_key="fake-key", + max_tokens=4096, + ) + + # The model class should have been called once + mock_cls.assert_called_once() + call_kwargs = mock_cls.call_args[1] + + assert expected_kwarg in call_kwargs, ( + f"Expected kwarg '{expected_kwarg}' not found in constructor call " + f"for provider '{provider}'. Got: {list(call_kwargs.keys())}" + ) + assert call_kwargs[expected_kwarg] == 4096 + + def test_max_tokens_not_passed_when_none(self): + """When max_tokens is None, no max_tokens kwarg should be passed.""" + model_selection = self._make_model_selection("OpenAI") + + mock_cls = MagicMock() + with ( + patch( + "lfx.base.models.unified_models.get_model_class", + return_value=mock_cls, + ), + patch( + "lfx.base.models.unified_models.get_api_key_for_provider", + return_value="fake-key", + ), + ): + get_llm( + model=model_selection, + user_id=None, + api_key="fake-key", + max_tokens=None, + ) + + call_kwargs = mock_cls.call_args[1] + assert "max_tokens" not in call_kwargs + assert "max_output_tokens" not in call_kwargs + + def test_max_tokens_not_passed_when_zero(self): + """When max_tokens is 0, it should be treated as unset.""" + model_selection = self._make_model_selection("OpenAI") + + mock_cls = MagicMock() + with ( + patch( + "lfx.base.models.unified_models.get_model_class", + return_value=mock_cls, + ), + patch( + "lfx.base.models.unified_models.get_api_key_for_provider", + return_value="fake-key", + ), + ): + get_llm( + model=model_selection, + user_id=None, + api_key="fake-key", + max_tokens=0, + ) + + call_kwargs = mock_cls.call_args[1] + # 0 does not satisfy >= 1, so it should not be passed + assert "max_tokens" not in call_kwargs + + def test_get_llm_falls_back_to_provider_metadata(self): + """Testing the fallback to provider metadata. + + Even if max_tokens_field_name is missing from model metadata, + get_llm should fall back to MODEL_PROVIDER_METADATA. + """ + # Build a model selection WITHOUT max_tokens_field_name in metadata + model_selection = [ + { + "name": "gemini-2.0-flash", + "provider": "Google Generative AI", + "metadata": { + "model_class": "ChatGoogleGenerativeAIFixed", + "model_name_param": "model", + "api_key_param": "google_api_key", + # intentionally omit max_tokens_field_name + }, + } + ] + + mock_cls = MagicMock() + with ( + patch( + "lfx.base.models.unified_models.get_model_class", + return_value=mock_cls, + ), + patch( + "lfx.base.models.unified_models.get_api_key_for_provider", + return_value="fake-key", + ), + ): + get_llm( + model=model_selection, + user_id=None, + api_key="fake-key", + max_tokens=2048, + ) + + call_kwargs = mock_cls.call_args[1] + # Should fall back to "max_output_tokens" from MODEL_PROVIDER_METADATA + assert "max_output_tokens" in call_kwargs, ( + "get_llm should fall back to MODEL_PROVIDER_METADATA for " + "max_tokens_field_name when it is missing from model metadata" + ) + assert call_kwargs["max_output_tokens"] == 2048 + + +# --------------------------------------------------------------------------- +# 3. AgentComponent._get_llm must forward max_tokens +# --------------------------------------------------------------------------- + + +class TestAgentComponentGetLlm: + """Testing AgentComponent._get_llm max_tokens forwarding. + + AgentComponent._get_llm must include the max_tokens field so that + create_agent_runnable (inherited from ToolCallingAgentComponent) uses + a model that respects the user's max_tokens setting. + + Regression: ToolCallingAgentComponent._get_llm did not pass max_tokens, + and create_agent_runnable called _get_llm to build a fresh model, + discarding the model built in get_agent_requirements. + """ + + def test_agent_get_llm_passes_max_tokens(self): + """AgentComponent._get_llm must forward max_tokens to get_llm.""" + from lfx.components.models_and_agents.agent import AgentComponent + + agent = AgentComponent() + # Simulate component state with a model selection and max_tokens + model_sel = [ + { + "name": "gpt-4o", + "provider": "OpenAI", + "metadata": { + "model_class": "ChatOpenAI", + "model_name_param": "model", + "api_key_param": "api_key", + "max_tokens_field_name": "max_tokens", + }, + } + ] + agent._inputs["model"].value = model_sel + agent._inputs["max_tokens"].value = 4096 + agent._inputs["api_key"].value = "fake-key" + + mock_cls = MagicMock() + with ( + patch( + "lfx.base.models.unified_models.get_model_class", + return_value=mock_cls, + ), + patch( + "lfx.base.models.unified_models.get_api_key_for_provider", + return_value="fake-key", + ), + patch.object(agent, "_user_id", "test-user"), + ): + agent._get_llm() + + call_kwargs = mock_cls.call_args[1] + assert "max_tokens" in call_kwargs, "AgentComponent._get_llm must pass max_tokens to the model constructor" + assert call_kwargs["max_tokens"] == 4096 + + def test_agent_get_llm_skips_max_tokens_when_unset(self): + """When max_tokens is 0 (default), it should not be passed.""" + from lfx.components.models_and_agents.agent import AgentComponent + + agent = AgentComponent() + model_sel = [ + { + "name": "gpt-4o", + "provider": "OpenAI", + "metadata": { + "model_class": "ChatOpenAI", + "model_name_param": "model", + "api_key_param": "api_key", + "max_tokens_field_name": "max_tokens", + }, + } + ] + agent._inputs["model"].value = model_sel + agent._inputs["max_tokens"].value = 0 # default / unset + agent._inputs["api_key"].value = "fake-key" + + mock_cls = MagicMock() + with ( + patch( + "lfx.base.models.unified_models.get_model_class", + return_value=mock_cls, + ), + patch( + "lfx.base.models.unified_models.get_api_key_for_provider", + return_value="fake-key", + ), + patch.object(agent, "_user_id", "test-user"), + ): + agent._get_llm() + + call_kwargs = mock_cls.call_args[1] + assert "max_tokens" not in call_kwargs, "AgentComponent._get_llm should not pass max_tokens when value is 0" diff --git a/uv.lock b/uv.lock index 495d823bf2e5..702499301a7f 100644 --- a/uv.lock +++ b/uv.lock @@ -34,7 +34,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, @@ -523,20 +523,23 @@ tools = [ [[package]] name = "astrapy" -version = "2.1.0" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "deprecation" }, { name = "h11" }, { name = "httpx", extra = ["http2"] }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pymongo" }, { name = "toml" }, { name = "typing-extensions" }, { name = "uuid6" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/38/4626371b552589ab8adba48859d20d0b9c85aeff8ae95c411b5e3a154ca0/astrapy-2.1.0.tar.gz", hash = "sha256:4c3aac2b54945615a7e63b531087893664734c1bc9df7edaa576879af837069f", size = 1379905, upload-time = "2025-09-24T13:43:15.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/06/25fb3962a0645157cfc5a9c398a64a049f0fc84c34c558ae87fc010fc8ab/astrapy-2.2.1.tar.gz", hash = "sha256:b1ed99df345adf0adc10a94106346584b7b16550cb515e52b7fb5f8b48c28336", size = 1440976, upload-time = "2026-03-12T12:02:15.032Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/44/8f2781080effad398ac5bdf66be39a52eab4f14a6aa2714930c1cecaaae3/astrapy-2.1.0-py3-none-any.whl", hash = "sha256:6422eaa39b057f736f14263fe7338094c5cab63d8a6f3756858ff7ea92d60f80", size = 333510, upload-time = "2025-09-24T13:43:13.211Z" }, + { url = "https://files.pythonhosted.org/packages/a7/78/93a84f53dec90d364e9133392406ebb65c17eaa92bb0fffab44827b6a2e9/astrapy-2.2.1-py3-none-any.whl", hash = "sha256:cfc8951720cb5d4926710ff3c91ddeac6a452a30a2201f0aa8b1e6f05cf7c1c7", size = 353410, upload-time = "2026-03-12T12:02:13.745Z" }, ] [[package]] @@ -634,15 +637,15 @@ wheels = [ [[package]] name = "azure-core" -version = "1.38.2" +version = "1.38.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests", marker = "python_full_version >= '3.12'" }, { name = "typing-extensions", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/fe/5c7710bc611a4070d06ba801de9a935cc87c3d4b689c644958047bdf2cba/azure_core-1.38.2.tar.gz", hash = "sha256:67562857cb979217e48dc60980243b61ea115b77326fa93d83b729e7ff0482e7", size = 363734, upload-time = "2026-02-18T19:33:05.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/29/9641b73248745774a52c7ce7f965ed1febbdea787ec21caad3ae6891d18a/azure_core-1.38.3.tar.gz", hash = "sha256:a7931fd445cb4af8802c6f39c6a326bbd1e34b115846550a8245fa656ead6f8e", size = 367267, upload-time = "2026-03-12T20:28:21.122Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/23/6371a551800d3812d6019cd813acd985f9fac0fedc1290129211a73da4ae/azure_core-1.38.2-py3-none-any.whl", hash = "sha256:074806c75cf239ea284a33a66827695ef7aeddac0b4e19dda266a93e4665ead9", size = 217957, upload-time = "2026-02-18T19:33:07.696Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3d/ac86083efa45a439d0bbfb7947615227813d368b9e1e93d23fd30de6fec0/azure_core-1.38.3-py3-none-any.whl", hash = "sha256:bf59d29765bf4748ab9edf25f98a30b7ea9797f43e367c06d846a30b29c1f845", size = 218231, upload-time = "2026-03-12T20:28:22.462Z" }, ] [[package]] @@ -761,7 +764,7 @@ wheels = [ [[package]] name = "black" -version = "26.3.0" +version = "26.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -773,29 +776,29 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/5f/25b7b149b8b7d3b958efa4faa56446560408c0f2651108a517526de0320a/black-26.3.0.tar.gz", hash = "sha256:4d438dfdba1c807c6c7c63c4f15794dda0820d2222e7c4105042ac9ddfc5dd0b", size = 664127, upload-time = "2026-03-06T17:42:33.7Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/45/0df73428226c2197b8b1e2ca15654f85cece1efe5f060c910b641a35de4a/black-26.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:135bf8a352e35b3bfba4999c256063d8d86514654599eca7635e914a55d60ec3", size = 1866623, upload-time = "2026-03-06T17:46:07.622Z" }, - { url = "https://files.pythonhosted.org/packages/40/e1/7467fcccf3532853b013bee22c9cdef6aa3314a58ccc73eb5a8a2750e50e/black-26.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6024a2959b6c62c311c564ce23ce0eaa977a50ed52a53f7abc83d2c9eb62b8d8", size = 1703733, upload-time = "2026-03-06T17:46:09.334Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/ceb0a5091b6dff654f77ee6488b91d45fbea1385338798935eb83090d27e/black-26.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:264144203ea3374542a1591b6fb317561662d074bce5d91ad6afa8d8d3e4ec3d", size = 1768094, upload-time = "2026-03-06T17:46:11.182Z" }, - { url = "https://files.pythonhosted.org/packages/49/cc/6af7e15fb728f30f3e3d4257d2f3d3fe5c5f4ada30b0e8feb92f50118d5c/black-26.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:1a15d1386dce3af3993bf9baeb68d3e492cbb003dae05c3ecf8530a9b75edf85", size = 1413004, upload-time = "2026-03-06T17:46:12.867Z" }, - { url = "https://files.pythonhosted.org/packages/c4/04/7f5ffd40078ab54efa738797e1d547a3fce893f1de212a7a2e65b4a36254/black-26.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:d86a70bf048235aff62a79e229fe5d9e7809c7a05a3dd12982e7ccdc2678e096", size = 1219839, upload-time = "2026-03-06T17:46:14.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ec/e4db9f2b2db8226ae20d48b589c69fd64477657bf241c8ccaea3bc4feafa/black-26.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3da07abe65732483e915ab7f9c7c50332c293056436e9519373775d62539607c", size = 1851905, upload-time = "2026-03-06T17:46:15.447Z" }, - { url = "https://files.pythonhosted.org/packages/62/2c/ccecfcbd6a0610ecf554e852a146f053eaeb5b281dd9cb634338518c765e/black-26.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc9fd683ccabc3dc9791b93db494d93b5c6c03b105453b76d71e5474e9dfa6e7", size = 1689299, upload-time = "2026-03-06T17:46:17.396Z" }, - { url = "https://files.pythonhosted.org/packages/1a/53/8dcb860242012d6da9c6b1b930c3e4c947eb42feb1fc70f2a4e7332c90c5/black-26.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2c7e2c5ee09ff575869258b2c07064c952637918fc5e15f6ebd45e45eae0aa", size = 1753902, upload-time = "2026-03-06T17:46:19.592Z" }, - { url = "https://files.pythonhosted.org/packages/5d/21/f37b3efcc8cf2d01ec9eb5466598aa53bed2292db236723ac4571e24c4de/black-26.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:a849286bfc3054eaeb233b6df9056fcf969ee18bf7ecb71b0257e838a0f05e6d", size = 1413841, upload-time = "2026-03-06T17:46:20.981Z" }, - { url = "https://files.pythonhosted.org/packages/eb/74/e70f5f2a74301d8f10276b90715699d51d7db1c3dd79cf13966d32ba7b18/black-26.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:c93c83af43cda73ed8265d001214779ab245fa7a861a75b3e43828f4fb1f5657", size = 1220105, upload-time = "2026-03-06T17:46:23.269Z" }, - { url = "https://files.pythonhosted.org/packages/1d/76/b21711045b7f4c4f1774048d0b34dd10a265c42255658b251ce3303ae3c7/black-26.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2b1e5eec220b419e3591a0aaa6351bd3a9c01fe6291fbaf76d84308eb7a2ede", size = 1895944, upload-time = "2026-03-06T17:46:24.841Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c3/8c56e73283326bc92a36101c660228fff09a2403a57a03cacf3f7f84cf62/black-26.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1bab64de70bccc992432bee56cdffbe004ceeaa07352127c386faa87e81f9261", size = 1718669, upload-time = "2026-03-06T17:46:26.639Z" }, - { url = "https://files.pythonhosted.org/packages/7b/8b/712a3ae8f17c1f3cd6f9ac2fffb167a27192f5c7aba68724e8c4ab8474ad/black-26.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b6c5f734290803b7b26493ffd734b02b72e6c90d82d45ac4d5b862b9bdf7720", size = 1794844, upload-time = "2026-03-06T17:46:28.334Z" }, - { url = "https://files.pythonhosted.org/packages/ba/5b/ee955040e446df86473287dd24dc69c80dd05e02cc358bca90e22059f7b1/black-26.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c767396af15b54e1a6aae99ddf241ae97e589f666b1d22c4b6618282a04e4ca", size = 1420461, upload-time = "2026-03-06T17:46:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/12/77/40b8bd44f032bb34c9ebf47ffc5bb47a2520d29e0a4b8a780ab515223b5a/black-26.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:765fd6ddd00f35c55250fdc6b790c272d54ac3f44da719cc42df428269b45980", size = 1229667, upload-time = "2026-03-06T17:46:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/28/c3/21a834ce3de02c64221243f2adac63fa3c3f441efdb3adbf4136b33dfeb0/black-26.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:59754fd8f43ef457be190594c07a52c999e22cb1534dc5344bff1d46fdf1027d", size = 1895195, upload-time = "2026-03-06T17:46:33.12Z" }, - { url = "https://files.pythonhosted.org/packages/1c/f9/212d9697dd78362dadb778d4616b74c8c2cf7f2e4a55aac2adeb0576f2e9/black-26.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1fd94cfee67b8d336761a0b08629a25938e4a491c440951ce517a7209c99b5ff", size = 1718472, upload-time = "2026-03-06T17:46:34.576Z" }, - { url = "https://files.pythonhosted.org/packages/a2/dd/da980b2f512441375b73cb511f38a2c3db4be83ccaa1302b8d39c9fa2dff/black-26.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b3e653a90ca1ef4e821c20f8edaee80b649c38d2532ed2e9073a9534b14a7", size = 1793741, upload-time = "2026-03-06T17:46:36.261Z" }, - { url = "https://files.pythonhosted.org/packages/93/11/cd69ae8826fe3bc6eaf525c8c557266d522b258154a2968eb46d6d25fac7/black-26.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:f8fb9d7c2496adc83614856e1f6e55a9ce4b7ae7fc7f45b46af9189ddb493464", size = 1422522, upload-time = "2026-03-06T17:46:37.607Z" }, - { url = "https://files.pythonhosted.org/packages/75/f5/647cf50255203eb286be197925e86eedc101d5409147505db3e463229228/black-26.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:e8618c1d06838f56afbcb3ffa1aa16436cec62b86b38c7b32ca86f53948ffb91", size = 1231807, upload-time = "2026-03-06T17:46:39.072Z" }, - { url = "https://files.pythonhosted.org/packages/39/d7/7360654ba4f8b41afcaeb5aca973cfea5591da75aff79b0a8ae0bb8883f6/black-26.3.0-py3-none-any.whl", hash = "sha256:e825d6b121910dff6f04d7691f826d2449327e8e71c26254c030c4f3d2311985", size = 206848, upload-time = "2026-03-06T17:42:31.133Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] [[package]] @@ -835,16 +838,16 @@ wheels = [ [[package]] name = "boto3-stubs" -version = "1.42.63" +version = "1.42.67" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/30/45d21d3df4598b6b37d8de9923603010e490dfc73a130c8ffa806750b1ff/boto3_stubs-1.42.63.tar.gz", hash = "sha256:b64726c86c0b3efbc6ccd4f0510fa5a8279caec46da36a91c037a4b395001e68", size = 101176, upload-time = "2026-03-06T22:50:00.514Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/97/1423bea1e6488646d75ef20359383c362a6367501fdc74472741f5aef079/boto3_stubs-1.42.67.tar.gz", hash = "sha256:c0debecec7fafac41b7977068d2bb0d6e19d08487b3d272fcd4ad5d5a4b045c4", size = 101393, upload-time = "2026-03-12T20:02:20.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c2/933ecf99d0cc1ae1b463086d203b27a29ba05c1d1cdd8a3efb18bc1e8c98/boto3_stubs-1.42.63-py3-none-any.whl", hash = "sha256:132e9c57bfef5ea943745d6cdad16e62d4e82dca64f6ae9ddfba9789fcdc8378", size = 69913, upload-time = "2026-03-06T22:49:49.765Z" }, + { url = "https://files.pythonhosted.org/packages/0f/49/65aeec6b40104e77ef3977867c8420567a01e51b7185e28a21c7a66e83c4/boto3_stubs-1.42.67-py3-none-any.whl", hash = "sha256:29c4b5bfc5fbdc0ba63a2805ddabde36f9b2b877492c2c24ac48d309d78da8ec", size = 70010, upload-time = "2026-03-12T20:02:13.573Z" }, ] [package.optional-dependencies] @@ -935,7 +938,7 @@ dependencies = [ { name = "gymnasium" }, { name = "lxml" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pillow" }, { name = "playwright" }, { name = "pyparsing" }, @@ -963,11 +966,11 @@ wheels = [ [[package]] name = "cachetools" -version = "7.0.4" +version = "7.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/cc/eb3fd22f3b96b8b70ce456d0854ef08434e5ca79c02bf8db3fc07ccfca87/cachetools-7.0.4.tar.gz", hash = "sha256:7042c0e4eea87812f04744ce6ee9ed3de457875eb1f82d8a206c46d6e48b6734", size = 37379, upload-time = "2026-03-08T21:37:17.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/bc/72adfb3f2ed19eb0317f89ea9b1eeccc670ae46bc394ec2c4ba1dd8c22b7/cachetools-7.0.4-py3-none-any.whl", hash = "sha256:0c8bb1b9ec8194fa4d764accfde602dfe52f70d0f311e62792d4c3f8c051b1e9", size = 13900, upload-time = "2026-03-08T21:37:15.805Z" }, + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, ] [[package]] @@ -1037,7 +1040,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cassandra-driver" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/88/00/a9a3a958169677f5f713bb3ed5a4afc1baf7abd1e08f37acad718aa923db/cassio-0.1.10.tar.gz", hash = "sha256:577f0a2ce5898a57c83195bf74811dec8794282477eb6fa4debd4ccec6cfab98", size = 35854, upload-time = "2024-10-03T16:53:57.549Z" } @@ -1221,7 +1224,7 @@ wheels = [ [[package]] name = "chromadb" -version = "1.5.4" +version = "1.5.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bcrypt" }, @@ -1233,7 +1236,7 @@ dependencies = [ { name = "kubernetes" }, { name = "mmh3" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "onnxruntime" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-grpc" }, @@ -1253,13 +1256,13 @@ dependencies = [ { name = "typing-extensions" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/0e/fe9f016a49dcf4bfa318b2106f3564ffa95baa955725f7ae8e074b2b2980/chromadb-1.5.4.tar.gz", hash = "sha256:d9e3284004b5222e97207f8a69366e684189c7de73d3ce2b5003f9043c8d22dd", size = 2410734, upload-time = "2026-03-08T20:08:25.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/6d/ab03e16be3ec663e353166f38be082efb51c0988687f8c8eee1416a7e732/chromadb-1.5.5.tar.gz", hash = "sha256:8d669285b77cc288db27583a57b2f85ba451a9b8e3bef85a260cd78e6b57be35", size = 2411397, upload-time = "2026-03-10T09:30:01.987Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/a0/bf31af8e513c36fe29860d7a2f465fbf5a58ec048936f061997cea55f48b/chromadb-1.5.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5daf789ce4b15b84ee8f2cf6f8b17898e7d0fb859c4f65dcc5a2d3052af820f5", size = 20798815, upload-time = "2026-03-08T20:08:23.295Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b7/5676bfd0868c2018a5123056917d3276a7a499d0e6910315b45e52a3e6f3/chromadb-1.5.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0d27484cc07d3f93489af8203542536ac621f8af52a2a4e590c535e5014794fc", size = 20087220, upload-time = "2026-03-08T20:08:20.628Z" }, - { url = "https://files.pythonhosted.org/packages/1c/76/c747fd05b752ef5f489d01946c77082416141999af57bba484d15eae2586/chromadb-1.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f37204b826b98f8561244382abf99141bc4b7bf21015dbbcf2280d771afeeb94", size = 20732448, upload-time = "2026-03-08T20:08:13.448Z" }, - { url = "https://files.pythonhosted.org/packages/21/7f/7525519b4eb0d2940d6202b40b21eb9f17df8061fb1b280f8569d0378ae7/chromadb-1.5.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be11c6d522319ee4fbe1cb8a9a11f6ba9c7f2db625a0e2ea377843bf8e5f99d5", size = 21595920, upload-time = "2026-03-08T20:08:17.741Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/9661c772fb4e480358241eb2aa781e11f23014084ceb8a14635d6eea6dcb/chromadb-1.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:18797f770c5be655e5f179336f268cccf23058e74cf62b34a6845a2ba70df2da", size = 21917281, upload-time = "2026-03-08T20:08:27.898Z" }, + { url = "https://files.pythonhosted.org/packages/f0/62/ee578f8ccd62928257558b13a3e7c236e402cfb319c9b201b6a75897d644/chromadb-1.5.5-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d590998ed81164afbfb1734bb534b25ec2c9810fc1c5ce53bf8f7ac644a79887", size = 20800888, upload-time = "2026-03-10T09:29:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ce/430a87d906f79cdc7e23efcd89dd237e3dbedaf6704b40ce1da127993bf8/chromadb-1.5.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5ff2912d20a82fdbf4e27ff3e1c91dab25e2ba2c629f9739bc12c11a3151aac7", size = 20091810, upload-time = "2026-03-10T09:29:56.044Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/11543a76ab25c55bec6133bb98ce0dc0f4850acb36600344d8286734a051/chromadb-1.5.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f54e7736ae0eeec436a1c1fb04b77b2c6c4108996790ef16f88327e38ad13cd", size = 20740649, upload-time = "2026-03-10T09:29:49.346Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/e0b35c41be7c02d6fa37f6c8f61a16b7b20607ddc847574e9a5503fe853b/chromadb-1.5.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb238ae508a6ce68fdd7875e040d7e5aa29d6e40fb651b51f5537b7cda789762", size = 21589423, upload-time = "2026-03-10T09:29:52.724Z" }, + { url = "https://files.pythonhosted.org/packages/a2/df/ce1ffcc0ad3eef8bd35b920809b990e6925ba94b2580dc5bd7ccde0fc06a/chromadb-1.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:3953403b63bb1c05405d10db36d183c4d19a027938c15898510d11943499046f", size = 21915873, upload-time = "2026-03-10T09:30:21.349Z" }, ] [[package]] @@ -1468,7 +1471,7 @@ wheels = [ [[package]] name = "composio-client" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1478,9 +1481,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/2d/5faddf107854a843137ee127946db3a738667948933e0903f1250e5e729a/composio_client-1.27.0.tar.gz", hash = "sha256:684d33a4e701f92d6d2cca1a66b638e509afb46263e9f13266673ee7c55efefe", size = 193444, upload-time = "2026-01-22T13:34:36.914Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/45/5f063d364bef7879252c3dab517a42a9f3fdf1ad8943895f5b718c27fd31/composio_client-1.28.0.tar.gz", hash = "sha256:787869c1fba543f07aa26d1f64e5f37e4909465b37af24ec4b5a01fd73115045", size = 209918, upload-time = "2026-03-09T04:34:49.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/37/e121ef2986935d374e85ca74315a8bc7fd5208342cabbb6461a49be466a4/composio_client-1.27.0-py3-none-any.whl", hash = "sha256:45697bb0f8a29290271727d9c1a3233e859dc61c515e6669f59408843fa590f0", size = 210495, upload-time = "2026-01-22T13:34:35.311Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8c/901f59aaf8674f8bda6da1fbae087eb5209f11a9b26f6118c2be49702d0c/composio_client-1.28.0-py3-none-any.whl", hash = "sha256:8f037d8e66b3bfb3aa701b4d26c8a242d22638031b938a363874ebc41e4710ee", size = 239827, upload-time = "2026-03-09T04:34:47.591Z" }, ] [[package]] @@ -1498,11 +1501,11 @@ wheels = [ [[package]] name = "configargparse" -version = "1.7.3" +version = "1.7.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/2a/44a4574b3c6cbbabef5ed132d6fe9ff283b2a67f5dda512def9e9fa4be02/configargparse-1.7.3.tar.gz", hash = "sha256:76dd1a51145fb7ca82621ee08cd38ec0c6316fe27a38b9137b75667d1116399e", size = 48416, upload-time = "2026-03-08T06:47:58.02Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/0b/30328302903c55218ffc5199646d0e9d28348ff26c02ba77b2ffc58d294a/configargparse-1.7.5.tar.gz", hash = "sha256:e3f9a7bb6be34d66b2e3c4a2f58e3045f8dfae47b0dc039f87bcfaa0f193fb0f", size = 53548, upload-time = "2026-03-11T02:19:38.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/27/5ec3deed103817f19e8bd53298e724aab0b62900b3663cdd516ba3ea5b66/configargparse-1.7.3-py3-none-any.whl", hash = "sha256:e7c787eaf6788c760252b6784be6fbf8edc106ca16d7e59b0b5743d1d8ee416d", size = 27425, upload-time = "2026-03-08T06:47:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/fe/19/3ba5e1b0bcc7b91aeab6c258afd70e4907d220fed3972febe38feb40db30/configargparse-1.7.5-py3-none-any.whl", hash = "sha256:1e63fdffedf94da9cd435fc13a1cd24777e76879dd2343912c1f871d4ac8c592", size = 27692, upload-time = "2026-03-11T02:19:36.442Z" }, ] [[package]] @@ -1803,7 +1806,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/ca/d323556e2bf9bfb63 [[package]] name = "cyclopts" -version = "4.8.0" +version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1813,9 +1816,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/7a/3c3623755561c7f283dd769470e99ae36c46810bf3b3f264d69006f6c97a/cyclopts-4.8.0.tar.gz", hash = "sha256:92cc292d18d8be372e58d8bce1aa966d30f819a5fb3fee02bd2ad4a6bb403f29", size = 164066, upload-time = "2026-03-07T19:39:18.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/de/75598ddea1f47589ccecdb23a560715a5a8ec2b3e34396b5628ba98d70e4/cyclopts-4.9.0.tar.gz", hash = "sha256:f292868e4be33a3e622d8cf95d89f49222e987b1ccdbf40caf6514e19dd99a63", size = 166300, upload-time = "2026-03-13T13:43:40.38Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/01/6ec7210775ea5e4989a10d89eda6c5ea7ff06caa614231ad533d74fecac8/cyclopts-4.8.0-py3-none-any.whl", hash = "sha256:ef353da05fec36587d4ebce7a6e4b27515d775d184a23bab4b01426f93ddc8d4", size = 201948, upload-time = "2026-03-07T19:39:19.307Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b2/2e342a876e5b78ce99ecf65ce391f5b2935144a0528c9989c437b8578a54/cyclopts-4.9.0-py3-none-any.whl", hash = "sha256:583ea4090a040c92f9303bc0da26bca7b681c81bcea34097ace279e1acef22c1", size = 203999, upload-time = "2026-03-13T13:43:38.553Z" }, ] [[package]] @@ -1940,7 +1943,7 @@ dependencies = [ { name = "huggingface-hub" }, { name = "multiprocess" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pandas" }, { name = "pyarrow" }, @@ -1956,16 +1959,16 @@ wheels = [ [[package]] name = "ddgs" -version = "9.11.2" +version = "9.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "lxml" }, { name = "primp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/80/eb387dd4291d1624e773f455fd1dfc54596e06469d680fe3b3f8c326ba1a/ddgs-9.11.2.tar.gz", hash = "sha256:b5f072149580773291fd3eb6e9f4de47fa9d910ebd5ef85845a37e59cfe24c40", size = 34722, upload-time = "2026-03-05T05:17:31.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/9e/d89f0c24d78812bad0b4150d9a432925aa756b4bfeb4ef4815fe6ff8f2a6/ddgs-9.11.3.tar.gz", hash = "sha256:6098c030d6806217260071d85e38d9b94b99fe326a3c40ebf5de25f620528ae2", size = 34776, upload-time = "2026-03-11T07:12:02.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/fe/7591bfa694ee26fcf0dd8035811994e28a9699402d3861eea7754958c1bd/ddgs-9.11.2-py3-none-any.whl", hash = "sha256:0023a3633d271e72cdd1da757d3fcea2d996608da3f3c9da2cc0c0607b219c76", size = 43646, upload-time = "2026-03-05T05:17:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9d/018d745128a9a33aff3e6b8f0260f7b970784d4b31573d36ee233b2e4db1/ddgs-9.11.3-py3-none-any.whl", hash = "sha256:596d656d00219b4748d839de1fa9a9c3eb5dd36db07365331f7526201115f18a", size = 43691, upload-time = "2026-03-11T07:12:00.21Z" }, ] [[package]] @@ -2212,7 +2215,7 @@ chunking = [ [[package]] name = "docling-ibm-models" -version = "3.11.0" +version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -2220,7 +2223,7 @@ dependencies = [ { name = "huggingface-hub" }, { name = "jsonlines" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pillow" }, { name = "pydantic" }, { name = "rtree" }, @@ -2236,9 +2239,9 @@ dependencies = [ { name = "tqdm" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/91/f883e0a2b3466e1126dfd4463f386c70f5b90d271c27b6f5a97d2f8312e6/docling_ibm_models-3.11.0.tar.gz", hash = "sha256:454401563a8e79cb33b718bc559d9bacca8a0183583e48f8e616c9184c1f5eb1", size = 87721, upload-time = "2026-01-23T12:29:35.384Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/57/b4cee1ea5a7d34a8a96787aa2dc371e4dd17a1a3bd6131cf3ced9024f1be/docling_ibm_models-3.12.0.tar.gz", hash = "sha256:85c2b6c9dbb7fbb8eaf0f2a462b5984626457a6dc33148643491270c27767b46", size = 98458, upload-time = "2026-03-09T12:27:35.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/5d/97e9c2e10fbd3ee1723ac82c335f8211a9633c0397cc11ed057c3ba4006e/docling_ibm_models-3.11.0-py3-none-any.whl", hash = "sha256:68f7961069d643bfdab21b1c9ef24a979db293496f4c2283d95b1025a9ac5347", size = 87352, upload-time = "2026-01-23T12:29:34.045Z" }, + { url = "https://files.pythonhosted.org/packages/98/ed/820fdcea9aa329119855a658366fb13098b375a2b024358b1d7836f47419/docling_ibm_models-3.12.0-py3-none-any.whl", hash = "sha256:008fe1f5571db413782efe510c1d6327ea9df20b5255d416d0f4b56cfd090238", size = 93800, upload-time = "2026-03-09T12:27:34.477Z" }, ] [[package]] @@ -2305,7 +2308,7 @@ dependencies = [ { name = "json-repair" }, { name = "litellm" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "openai" }, { name = "optuna" }, { name = "orjson" }, @@ -2335,37 +2338,37 @@ wheels = [ [[package]] name = "duckdb" -version = "1.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/9d/ab66a06e416d71b7bdcb9904cdf8d4db3379ef632bb8e9495646702d9718/duckdb-1.4.4.tar.gz", hash = "sha256:8bba52fd2acb67668a4615ee17ee51814124223de836d9e2fdcbc4c9021b3d3c", size = 18419763, upload-time = "2026-01-26T11:50:37.68Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/9f/67a75f1e88f84946909826fa7aadd0c4b0dc067f24956142751fd9d59fe6/duckdb-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e870a441cb1c41d556205deb665749f26347ed13b3a247b53714f5d589596977", size = 28884338, upload-time = "2026-01-26T11:48:41.591Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7a/e9277d0567884c21f345ad43cc01aeaa2abe566d5fdf22e35c3861dd44fa/duckdb-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49123b579e4a6323e65139210cd72dddc593a72d840211556b60f9703bda8526", size = 15339148, upload-time = "2026-01-26T11:48:45.343Z" }, - { url = "https://files.pythonhosted.org/packages/4a/96/3a7630d2779d2bae6f3cdf540a088ed45166adefd3c429971e5b85ce8f84/duckdb-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e1933fac5293fea5926b0ee75a55b8cfe7f516d867310a5b251831ab61fe62b", size = 13668431, upload-time = "2026-01-26T11:48:47.864Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ad/f62a3a65d200e8afc1f75cf0dd3f0aa84ef0dd07c484414a11f2abed810e/duckdb-1.4.4-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:707530f6637e91dc4b8125260595299ec9dd157c09f5d16c4186c5988bfbd09a", size = 18409546, upload-time = "2026-01-26T11:48:51.142Z" }, - { url = "https://files.pythonhosted.org/packages/a2/5f/23bd586ecb21273b41b5aa4b16fd88b7fecb53ed48d897273651c0c3d66f/duckdb-1.4.4-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:453b115f4777467f35103d8081770ac2f223fb5799178db5b06186e3ab51d1f2", size = 20407046, upload-time = "2026-01-26T11:48:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d0/4ce78bf341c930d4a22a56cb686bfc2c975eaf25f653a7ac25e3929d98bb/duckdb-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a3c8542db7ffb128aceb7f3b35502ebaddcd4f73f1227569306cc34bad06680c", size = 12256576, upload-time = "2026-01-26T11:48:58.203Z" }, - { url = "https://files.pythonhosted.org/packages/04/68/19233412033a2bc5a144a3f531f64e3548d4487251e3f16b56c31411a06f/duckdb-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ba684f498d4e924c7e8f30dd157da8da34c8479746c5011b6c0e037e9c60ad2", size = 28883816, upload-time = "2026-01-26T11:49:01.009Z" }, - { url = "https://files.pythonhosted.org/packages/b3/3e/cec70e546c298ab76d80b990109e111068d82cca67942c42328eaa7d6fdb/duckdb-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5536eb952a8aa6ae56469362e344d4e6403cc945a80bc8c5c2ebdd85d85eb64b", size = 15339662, upload-time = "2026-01-26T11:49:04.058Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f0/cf4241a040ec4f571859a738007ec773b642fbc27df4cbcf34b0c32ea559/duckdb-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:47dd4162da6a2be59a0aef640eb08d6360df1cf83c317dcc127836daaf3b7f7c", size = 13670044, upload-time = "2026-01-26T11:49:06.627Z" }, - { url = "https://files.pythonhosted.org/packages/11/64/de2bb4ec1e35ec9ebf6090a95b930fc56934a0ad6f34a24c5972a14a77ef/duckdb-1.4.4-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cb357cfa3403910e79e2eb46c8e445bb1ee2fd62e9e9588c6b999df4256abc1", size = 18409951, upload-time = "2026-01-26T11:49:09.808Z" }, - { url = "https://files.pythonhosted.org/packages/79/a2/ac0f5ee16df890d141304bcd48733516b7202c0de34cd3555634d6eb4551/duckdb-1.4.4-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c25d5b0febda02b7944e94fdae95aecf952797afc8cb920f677b46a7c251955", size = 20411739, upload-time = "2026-01-26T11:49:12.652Z" }, - { url = "https://files.pythonhosted.org/packages/37/a2/9a3402edeedaecf72de05fe9ff7f0303d701b8dfc136aea4a4be1a5f7eee/duckdb-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6703dd1bb650025b3771552333d305d62ddd7ff182de121483d4e042ea6e2e00", size = 12256972, upload-time = "2026-01-26T11:49:15.468Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e6/052ea6dcdf35b259fd182eff3efd8d75a071de4010c9807556098df137b9/duckdb-1.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:bf138201f56e5d6fc276a25138341b3523e2f84733613fc43f02c54465619a95", size = 13006696, upload-time = "2026-01-26T11:49:18.054Z" }, - { url = "https://files.pythonhosted.org/packages/58/33/beadaa69f8458afe466126f2c5ee48c4759cc9d5d784f8703d44e0b52c3c/duckdb-1.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ddcfd9c6ff234da603a1edd5fd8ae6107f4d042f74951b65f91bc5e2643856b3", size = 28896535, upload-time = "2026-01-26T11:49:21.232Z" }, - { url = "https://files.pythonhosted.org/packages/76/66/82413f386df10467affc87f65bac095b7c88dbd9c767584164d5f4dc4cb8/duckdb-1.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6792ca647216bd5c4ff16396e4591cfa9b4a72e5ad7cdd312cec6d67e8431a7c", size = 15349716, upload-time = "2026-01-26T11:49:23.989Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8c/c13d396fd4e9bf970916dc5b4fea410c1b10fe531069aea65f1dcf849a71/duckdb-1.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f8d55843cc940e36261689054f7dfb6ce35b1f5b0953b0d355b6adb654b0d52", size = 13672403, upload-time = "2026-01-26T11:49:26.741Z" }, - { url = "https://files.pythonhosted.org/packages/db/77/2446a0b44226bb95217748d911c7ca66a66ca10f6481d5178d9370819631/duckdb-1.4.4-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c65d15c440c31e06baaebfd2c06d71ce877e132779d309f1edf0a85d23c07e92", size = 18419001, upload-time = "2026-01-26T11:49:29.353Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a3/97715bba30040572fb15d02c26f36be988d48bc00501e7ac02b1d65ef9d0/duckdb-1.4.4-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b297eff642503fd435a9de5a9cb7db4eccb6f61d61a55b30d2636023f149855f", size = 20437385, upload-time = "2026-01-26T11:49:32.302Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0a/18b9167adf528cbe3867ef8a84a5f19f37bedccb606a8a9e59cfea1880c8/duckdb-1.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d525de5f282b03aa8be6db86b1abffdceae5f1055113a03d5b50cd2fb8cf2ef8", size = 12267343, upload-time = "2026-01-26T11:49:34.985Z" }, - { url = "https://files.pythonhosted.org/packages/f8/15/37af97f5717818f3d82d57414299c293b321ac83e048c0a90bb8b6a09072/duckdb-1.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:50f2eb173c573811b44aba51176da7a4e5c487113982be6a6a1c37337ec5fa57", size = 13007490, upload-time = "2026-01-26T11:49:37.413Z" }, - { url = "https://files.pythonhosted.org/packages/7f/fe/64810fee20030f2bf96ce28b527060564864ce5b934b50888eda2cbf99dd/duckdb-1.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:337f8b24e89bc2e12dadcfe87b4eb1c00fd920f68ab07bc9b70960d6523b8bc3", size = 28899349, upload-time = "2026-01-26T11:49:40.294Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9b/3c7c5e48456b69365d952ac201666053de2700f5b0144a699a4dc6854507/duckdb-1.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0509b39ea7af8cff0198a99d206dca753c62844adab54e545984c2e2c1381616", size = 15350691, upload-time = "2026-01-26T11:49:43.242Z" }, - { url = "https://files.pythonhosted.org/packages/a6/7b/64e68a7b857ed0340045501535a0da99ea5d9d5ea3708fec0afb8663eb27/duckdb-1.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fb94de6d023de9d79b7edc1ae07ee1d0b4f5fa8a9dcec799650b5befdf7aafec", size = 13672311, upload-time = "2026-01-26T11:49:46.069Z" }, - { url = "https://files.pythonhosted.org/packages/09/5b/3e7aa490841784d223de61beb2ae64e82331501bf5a415dc87a0e27b4663/duckdb-1.4.4-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d636ceda422e7babd5e2f7275f6a0d1a3405e6a01873f00d38b72118d30c10b", size = 18422740, upload-time = "2026-01-26T11:49:49.034Z" }, - { url = "https://files.pythonhosted.org/packages/53/32/256df3dbaa198c58539ad94f9a41e98c2c8ff23f126b8f5f52c7dcd0a738/duckdb-1.4.4-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df7351328ffb812a4a289732f500d621e7de9942a3a2c9b6d4afcf4c0e72526", size = 20435578, upload-time = "2026-01-26T11:49:51.946Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f0/620323fd87062ea43e527a2d5ed9e55b525e0847c17d3b307094ddab98a2/duckdb-1.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:6fb1225a9ea5877421481d59a6c556a9532c32c16c7ae6ca8d127e2b878c9389", size = 12268083, upload-time = "2026-01-26T11:49:54.615Z" }, - { url = "https://files.pythonhosted.org/packages/e5/07/a397fdb7c95388ba9c055b9a3d38dfee92093f4427bc6946cf9543b1d216/duckdb-1.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:f28a18cc790217e5b347bb91b2cab27aafc557c58d3d8382e04b4fe55d0c3f66", size = 13006123, upload-time = "2026-01-26T11:49:57.092Z" }, +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/11/e05a7eb73a373d523e45d83c261025e02bc31ebf868e6282c30c4d02cc59/duckdb-1.5.0.tar.gz", hash = "sha256:f974b61b1c375888ee62bc3125c60ac11c4e45e4457dd1bb31a8f8d3cf277edd", size = 17981141, upload-time = "2026-03-09T12:50:26.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/5d/8fa129bbd604d0e91aa9a0a407e7d2acc559b6024c3f887868fd7a13871d/duckdb-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:47fbb1c053a627a91fa71ec883951561317f14a82df891c00dcace435e8fea78", size = 30012348, upload-time = "2026-03-09T12:48:39.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/31/db320641a262a897755e634d16838c98d5ca7dc91f4e096e104e244a3a01/duckdb-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b546a30a6ac020165a86ab3abac553255a6e8244d5437d17859a6aa338611aa", size = 15940515, upload-time = "2026-03-09T12:48:41.905Z" }, + { url = "https://files.pythonhosted.org/packages/0b/45/5725684794fbabf54d8dbae5247685799a6bf8e1e930ebff3a76a726772c/duckdb-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:122396041c0acb78e66d7dc7d36c55f03f67fe6ad012155c132d82739722e381", size = 14193724, upload-time = "2026-03-09T12:48:44.105Z" }, + { url = "https://files.pythonhosted.org/packages/27/68/f110c66b43e27191d7e53d3587e118568b73d66f23cb9bd6c7e0a560fd6d/duckdb-1.5.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a2cd73d50ea2c2bf618a4b7d22fe7c4115a1c9083d35654a0d5d421620ed999", size = 19218777, upload-time = "2026-03-09T12:48:46.399Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9d/46affc9257377cbc865e494650312a7a08a56e85aa8d702eb297bec430b7/duckdb-1.5.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63a8ea3b060a881c90d1c1b9454abed3daf95b6160c39bbb9506fee3a9711730", size = 21311205, upload-time = "2026-03-09T12:48:48.895Z" }, + { url = "https://files.pythonhosted.org/packages/3b/34/dac03ab7340989cda258655387959c88342ea3b44949751391267bcbc830/duckdb-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:238d576ae1dda441f8c79ed1370c5ccf863e4a5d59ca2563f9c96cd26b2188ac", size = 13043217, upload-time = "2026-03-09T12:48:51.262Z" }, + { url = "https://files.pythonhosted.org/packages/01/0c/0282b10a1c96810606b916b8d58a03f2131bd3ede14d2851f58b0b860e7c/duckdb-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3298bd17cf0bb5f342fb51a4edc9aadacae882feb2b04161a03eb93271c70c86", size = 30014615, upload-time = "2026-03-09T12:48:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/71/e8/cbbc920078a794f24f63017fc55c9cbdb17d6fb94d3973f479b2d9f2983d/duckdb-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:13f94c49ca389731c439524248e05007fb1a86cd26f1e38f706abc261069cd41", size = 15940493, upload-time = "2026-03-09T12:48:57.85Z" }, + { url = "https://files.pythonhosted.org/packages/31/b6/6cae794d5856259b0060f79d5db71c7fdba043950eaa6a9d72b0bad16095/duckdb-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab9d597b1e8668466f1c164d0ea07eaf0ebb516950f5a2e794b0f52c81ff3b16", size = 14194663, upload-time = "2026-03-09T12:49:00.416Z" }, + { url = "https://files.pythonhosted.org/packages/82/07/aba3887658b93a36ce702dd00ca6a6422de3d14c7ee3a4b4c03ea20a99c0/duckdb-1.5.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a43f8289b11c0b50d13f96ab03210489d37652f3fd7911dc8eab04d61b049da2", size = 19220501, upload-time = "2026-03-09T12:49:03.431Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a2/723e6df48754e468fa50d7878eb860906c975eafe317c4134a8482ca220e/duckdb-1.5.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f514e796a116c5de070e99974e42d0b8c2e6c303386790e58408c481150d417", size = 21316142, upload-time = "2026-03-09T12:49:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/03/af/4dcbdf8f2349ed0b054c254ec59bc362ce6ddf603af35f770124c0984686/duckdb-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cf503ba2c753d97c76beb111e74572fef8803265b974af2dca67bba1de4176d2", size = 13043445, upload-time = "2026-03-09T12:49:08.892Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/1bb7e75a63bf3dc49bc5a2cd27a65ffeef151f52a32db980983516f2d9f6/duckdb-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:a1156e91e4e47f0e7d9c9404e559a1d71b372cd61790a407d65eb26948ae8298", size = 13883145, upload-time = "2026-03-09T12:49:11.566Z" }, + { url = "https://files.pythonhosted.org/packages/43/73/120e673e48ae25aaf689044c25ef51b0ea1d088563c9a2532612aea18e0a/duckdb-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ea988d1d5c8737720d1b2852fd70e4d9e83b1601b8896a1d6d31df5e6afc7dd", size = 30057869, upload-time = "2026-03-09T12:49:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/21/e9/61143471958d36d3f3e764cb4cd43330be208ddbff1c78d3310b9ee67fe8/duckdb-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb786d5472afc16cc3c7355eb2007172538311d6f0cc6f6a0859e84a60220375", size = 15963092, upload-time = "2026-03-09T12:49:17.478Z" }, + { url = "https://files.pythonhosted.org/packages/4f/71/76e37c9a599ad89dd944e6cbb3e6a8ad196944a421758e83adea507637b6/duckdb-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc92b238f4122800a7592e99134124cc9048c50f766c37a0778dd2637f5cbe59", size = 14220562, upload-time = "2026-03-09T12:49:23.518Z" }, + { url = "https://files.pythonhosted.org/packages/db/b8/de1831656d5d13173e27c79c7259c8b9a7bdc314fdc8920604838ea4c46d/duckdb-1.5.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b74cb205c21d3696d8f8b88adca401e1063d6e6f57c1c4f56a243610b086e30", size = 19245329, upload-time = "2026-03-09T12:49:26.307Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8d/33d349a3bcbd3e9b7b4e904c19d5b97f058c4c20791b89a8d6323bb93dce/duckdb-1.5.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e56c19ffd1ffe3642fa89639e71e2e00ab0cf107b62fe16e88030acaebcbde6", size = 21348041, upload-time = "2026-03-09T12:49:30.283Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ec/591a4cad582fae04bc8f8b4a435eceaaaf3838cf0ca771daae16a3c2995b/duckdb-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:86525e565ec0c43420106fd34ba2c739a54c01814d476c7fed3007c9ed6efd86", size = 13053781, upload-time = "2026-03-09T12:49:33.574Z" }, + { url = "https://files.pythonhosted.org/packages/db/62/42e0a13f9919173bec121c0ff702406e1cdd91d8084c3e0b3412508c3891/duckdb-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:5faeebc178c986a7bfa68868a023001137a95a1110bf09b7356442a4eae0f7e7", size = 13862906, upload-time = "2026-03-09T12:49:36.598Z" }, + { url = "https://files.pythonhosted.org/packages/35/5d/af5501221f42e4e3662c047ecec4dcd0761229fceeba3c67ad4d9d8741df/duckdb-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11dd05b827846c87f0ae2f67b9ae1d60985882a7c08ce855379e4a08d5be0e1d", size = 30057396, upload-time = "2026-03-09T12:49:39.95Z" }, + { url = "https://files.pythonhosted.org/packages/43/bd/a278d73fedbd3783bf9aedb09cad4171fe8e55bd522952a84f6849522eb6/duckdb-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ad8d9c91b7c280ab6811f59deff554b845706c20baa28c4e8f80a95690b252b", size = 15962700, upload-time = "2026-03-09T12:49:43.504Z" }, + { url = "https://files.pythonhosted.org/packages/76/fc/c916e928606946209c20fb50898dabf120241fb528a244e2bd8cde1bd9e2/duckdb-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee4dabe03ed810d64d93927e0fd18cd137060b81ee75dcaeaaff32cbc816656", size = 14220272, upload-time = "2026-03-09T12:49:46.867Z" }, + { url = "https://files.pythonhosted.org/packages/53/07/1390e69db922423b2e111e32ed342b3e8fad0a31c144db70681ea1ba4d56/duckdb-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409ed1184b363ddea239609c5926f5148ee412b8d9e5ffa617718d755d942f6", size = 19244401, upload-time = "2026-03-09T12:49:49.865Z" }, + { url = "https://files.pythonhosted.org/packages/54/13/b58d718415cde993823a54952ea511d2612302f1d2bc220549d0cef752a4/duckdb-1.5.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1df8c4f9c853a45f3ec1e79ed7fe1957a203e5ec893bbbb853e727eb93e0090f", size = 21345827, upload-time = "2026-03-09T12:49:52.977Z" }, + { url = "https://files.pythonhosted.org/packages/e0/96/4460429651e371eb5ff745a4790e7fa0509c7a58c71fc4f0f893404c9646/duckdb-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a3d3dfa2d8bc74008ce3ad9564761ae23505a9e4282f6a36df29bd87249620b", size = 13053101, upload-time = "2026-03-09T12:49:56.134Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/6d5b805113214b830fa3c267bb3383fb8febaa30760d0162ef59aadb110a/duckdb-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:2deebcbafd9d39c04f31ec968f4dd7cee832c021e10d96b32ab0752453e247c8", size = 13865071, upload-time = "2026-03-09T12:49:59.282Z" }, ] [[package]] @@ -2437,7 +2440,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ninja" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "opencv-python-headless", version = "4.13.0.92", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pillow" }, @@ -2490,7 +2493,7 @@ wheels = [ [package.optional-dependencies] vectorstore-mmr = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "simsimd" }, ] @@ -2631,7 +2634,7 @@ version = "1.9.0.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, ] wheels = [ @@ -2668,14 +2671,14 @@ wheels = [ [[package]] name = "faker" -version = "40.8.0" +version = "40.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/03/14428edc541467c460d363f6e94bee9acc271f3e62470630fc9a647d0cf2/faker-40.8.0.tar.gz", hash = "sha256:936a3c9be6c004433f20aa4d99095df5dec82b8c7ad07459756041f8c1728875", size = 1956493, upload-time = "2026-03-04T16:18:48.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/dc/b68e5378e5a7db0ab776efcdd53b6fe374b29d703e156fd5bb4c5437069e/faker-40.11.0.tar.gz", hash = "sha256:7c419299103b13126bd02ec14bd2b47b946edb5a5eedf305e66a193b25f9a734", size = 1957570, upload-time = "2026-03-13T14:36:11.844Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/3b/c6348f1e285e75b069085b18110a4e6325b763a5d35d5e204356fc7c20b3/faker-40.8.0-py3-none-any.whl", hash = "sha256:eb21bdba18f7a8375382eb94fb436fce07046893dc94cb20817d28deb0c3d579", size = 1989124, upload-time = "2026-03-04T16:18:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/a86c6ba66f0308c95b9288b1e3eaccd934b545646f63494a86f1ec2f8c8e/faker-40.11.0-py3-none-any.whl", hash = "sha256:0e9816c950528d2a37d74863f3ef389ea9a3a936cbcde0b11b8499942e25bf90", size = 1989457, upload-time = "2026-03-13T14:36:09.792Z" }, ] [[package]] @@ -2738,7 +2741,7 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.14.1" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastar" }, @@ -2750,9 +2753,9 @@ dependencies = [ { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/30/1665ad6bd1c285d1c6947e6ab0eae168bc44a9b45d5fc11fa930603db1c7/fastapi_cloud_cli-0.14.1.tar.gz", hash = "sha256:5b086182570008f67d9ae989870102f595e4e216493cabcd270ef6cd0401339f", size = 39999, upload-time = "2026-03-08T01:40:24.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/e1/05c44e7bbc619e980fab0236cff9f5f323ac1aaa79434b4906febf98b1d3/fastapi_cloud_cli-0.15.0.tar.gz", hash = "sha256:d02515231f3f505f7669c20920343934570a88a08af9f9a6463ca2807f27ffe5", size = 45309, upload-time = "2026-03-11T22:31:32.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/c2/0117d2a1b93eb7c6d2084e6be320d34a404f621eb01a26c1471c0eb4ee82/fastapi_cloud_cli-0.14.1-py3-none-any.whl", hash = "sha256:99ab3a2fbd1880121a62fb9c4f584e15c42ef0fe762cb5f7380a548081e60d05", size = 28359, upload-time = "2026-03-08T01:40:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/40/cc/1ccca747f5609be27186ea8c9219449142f40e3eded2c6089bba6a6ecc82/fastapi_cloud_cli-0.15.0-py3-none-any.whl", hash = "sha256:9ffcf90bd713747efa65447620d29cfbb7b3f7de38d97467952ca6346e418d70", size = 32267, upload-time = "2026-03-11T22:31:33.499Z" }, ] [[package]] @@ -2982,7 +2985,7 @@ dependencies = [ { name = "cramjam" }, { name = "fsspec" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pandas" }, ] @@ -3024,11 +3027,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.25.0" +version = "3.25.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] [[package]] @@ -3482,16 +3485,15 @@ wheels = [ [[package]] name = "google-auth" -version = "2.49.0" +version = "2.49.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, - { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/59/7371175bfd949abfb1170aa076352131d7281bd9449c0f978604fc4431c3/google_auth-2.49.0.tar.gz", hash = "sha256:9cc2d9259d3700d7a257681f81052db6737495a1a46b610597f4b8bafe5286ae", size = 333444, upload-time = "2026-03-06T21:53:06.07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/45/de64b823b639103de4b63dd193480dce99526bd36be6530c2dba85bf7817/google_auth-2.49.0-py3-none-any.whl", hash = "sha256:f893ef7307f19cf53700b7e2f61b5a6affe3aa0edf9943b13788920ab92d8d87", size = 240676, upload-time = "2026-03-06T21:52:38.304Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, ] [package.optional-dependencies] @@ -3527,7 +3529,7 @@ wheels = [ [[package]] name = "google-cloud-aiplatform" -version = "1.140.0" +version = "1.141.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -3543,9 +3545,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/14/1c223faf986afffdd61c994a10c30a04985ed5ba072201058af2c6e1e572/google_cloud_aiplatform-1.140.0.tar.gz", hash = "sha256:ea7eb1870b4cf600f8c2472102e21c3a1bcaf723d6e49f00ed51bc6b88d54fff", size = 10146640, upload-time = "2026-03-04T00:56:38.95Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/dc/1209c7aab43bd7233cf631165a3b1b4284d22fc7fe7387c66228d07868ab/google_cloud_aiplatform-1.141.0.tar.gz", hash = "sha256:e3b1cdb28865dd862aac9c685dfc5ac076488705aba0a5354016efadcddd59c6", size = 10152688, upload-time = "2026-03-10T22:20:08.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/5c/bb64aee2da24895d57611eed00fac54739bfa34f98ab344020a6605875bf/google_cloud_aiplatform-1.140.0-py2.py3-none-any.whl", hash = "sha256:e94493a2682b9d17efa7146a53bb3665bf1595c3394fd3d0f45d18f71623fddc", size = 8355660, upload-time = "2026-03-04T00:56:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/428af69a69ff2e477e7f5e12d227b31fe5790f1a8234aacd54297f49c836/google_cloud_aiplatform-1.141.0-py2.py3-none-any.whl", hash = "sha256:6bd25b4d514c40b8181ca703e1b313ad6d0454ab8006fc9907fb3e9f672f31d1", size = 8358409, upload-time = "2026-03-10T22:20:04.871Z" }, ] [[package]] @@ -3645,7 +3647,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.66.0" +version = "1.67.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -3659,9 +3661,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/ba/0b343b0770d4710ad2979fd9301d7caa56c940174d5361ed4a7cc4979241/google_genai-1.66.0.tar.gz", hash = "sha256:ffc01647b65046bca6387320057aa51db0ad64bcc72c8e3e914062acfa5f7c49", size = 504386, upload-time = "2026-03-04T22:15:28.156Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/07/59a498f81f2c7b0649eacda2ea470b7fd8bd7149f20caba22962081bdd51/google_genai-1.67.0.tar.gz", hash = "sha256:897195a6a9742deb6de240b99227189ada8b2d901d61bdfba836c3092021eab6", size = 506972, upload-time = "2026-03-12T20:39:16.241Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/dd/403949d922d4e261b08b64aaa132af4e456c3b15c8e2a2d9e6ef693f66e2/google_genai-1.66.0-py3-none-any.whl", hash = "sha256:7f127a39cf695277104ce4091bb26e417c59bb46e952ff3699c3a982d9c474ee", size = 732174, upload-time = "2026-03-04T22:15:26.63Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/562aa1f086e53529ffbeb5b43d5d8bc42c1b968102b5e2163fad005ce298/google_genai-1.67.0-py3-none-any.whl", hash = "sha256:58b0484ff2d4335fa53c724b489e9f807fcca8115d9cdbd8fdf341121fbd6d2d", size = 733542, upload-time = "2026-03-12T20:39:14.615Z" }, ] [[package]] @@ -3748,7 +3750,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "immutabledict" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pytest" }, { name = "typing-extensions" }, ] @@ -4009,7 +4011,7 @@ dependencies = [ { name = "cloudpickle" }, { name = "farama-notifications" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/59/653a9417d98ed3e29ef9734ba52c3495f6c6823b8d5c0c75369f25111708/gymnasium-1.2.3.tar.gz", hash = "sha256:2b2cb5b5fbbbdf3afb9f38ca952cc48aa6aa3e26561400d940747fda3ad42509", size = 829230, upload-time = "2025-12-18T16:51:10.234Z" } @@ -4041,26 +4043,26 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.3.2" +version = "1.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, - { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, - { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, - { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, - { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, - { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, - { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/d4/b73ebab01cbf60777323b7de9ef05550790451eb5172a220d6b9845385ec/hf_xet-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d2f8ee39fa9fba9af929f8c0d0482f8ee6e209179ad14a909b6ad78ffcb7c81", size = 3555985, upload-time = "2026-03-13T06:58:31.797Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e7/ded6d1bd041c3f2bca9e913a0091adfe32371988e047dd3a68a2463c15a2/hf_xet-1.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4642a6cf249c09da8c1f87fe50b24b2a3450b235bf8adb55700b52f0ea6e2eb6", size = 4212085, upload-time = "2026-03-13T06:58:24.323Z" }, + { url = "https://files.pythonhosted.org/packages/97/c1/a0a44d1f98934f7bdf17f7a915b934f9fca44bb826628c553589900f6df8/hf_xet-1.4.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:769431385e746c92dc05492dde6f687d304584b89c33d79def8367ace06cb555", size = 3988266, upload-time = "2026-03-13T06:58:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/7a/82/be713b439060e7d1f1d93543c8053d4ef2fe7e6922c5b31642eaa26f3c4b/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c9dd1c1bc4cc56168f81939b0e05b4c36dd2d28c13dc1364b17af89aa0082496", size = 4188513, upload-time = "2026-03-13T06:58:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" }, + { url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" }, + { url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" }, + { url = "https://files.pythonhosted.org/packages/68/47/d6cf4a39ecf6c7705f887a46f6ef5c8455b44ad9eb0d391aa7e8a2ff7fea/hf_xet-1.4.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3b3c6a882016b94b6c210957502ff7877802d0dbda8ad142c8595db8b944271", size = 3992847, upload-time = "2026-03-13T06:58:25.989Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ef/e80815061abff54697239803948abc665c6b1d237102c174f4f7a9a5ffc5/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d9a634cc929cfbaf2e1a50c0e532ae8c78fa98618426769480c58501e8c8ac2", size = 4193843, upload-time = "2026-03-13T06:58:44.59Z" }, + { url = "https://files.pythonhosted.org/packages/54/75/07f6aa680575d9646c4167db6407c41340cbe2357f5654c4e72a1b01ca14/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b0932eb8b10317ea78b7da6bab172b17be03bbcd7809383d8d5abd6a2233e04", size = 4432751, upload-time = "2026-03-13T06:58:46.533Z" }, + { url = "https://files.pythonhosted.org/packages/cd/71/193eabd7e7d4b903c4aa983a215509c6114915a5a237525ec562baddb868/hf_xet-1.4.2-cp37-abi3-win_amd64.whl", hash = "sha256:ad185719fb2e8ac26f88c8100562dbf9dbdcc3d9d2add00faa94b5f106aea53f", size = 3671149, upload-time = "2026-03-13T06:58:57.07Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" }, ] [[package]] @@ -4424,16 +4426,16 @@ wheels = [ [[package]] name = "imageio" -version = "2.37.2" +version = "2.37.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, ] [[package]] @@ -5128,7 +5130,7 @@ dependencies = [ { name = "astrapy" }, { name = "langchain-core" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b6/91/95b1beff70577856018c4ae3908fe51d497af512b321adc5c7cddb85a154/langchain_astradb-0.6.1.tar.gz", hash = "sha256:55281d84d226d909ad0edb92e7f5e0cb7102b0fd7cbbdce7dc18f674cce3e91e", size = 53665, upload-time = "2025-08-28T17:16:25.086Z" } wheels = [ @@ -5143,7 +5145,7 @@ dependencies = [ { name = "boto3" }, { name = "langchain-core" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/7a/19a903725acbb1c4481dc0391b2551250bf4e04cbe5a891a55e09319772b/langchain_aws-0.2.35.tar.gz", hash = "sha256:45793a34fe45d365f4292cc768db74669ca24601d2c5da1ac6f44403750d70af", size = 120567, upload-time = "2025-10-02T23:59:57.204Z" } @@ -5159,7 +5161,7 @@ dependencies = [ { name = "chromadb" }, { name = "langchain-core" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/94/93ab8f6e96429a60da50eea2d3f3ac988852f6b8da4e817d73ec026a0bd3/langchain_chroma-0.2.6.tar.gz", hash = "sha256:ec5ca0f6f7692ac053741e076ea086c4be0cfcb5846c8693b1bcc3089c88b65e", size = 17296, upload-time = "2025-09-11T19:54:47.991Z" } wheels = [ @@ -5194,7 +5196,7 @@ dependencies = [ { name = "langchain-core" }, { name = "langsmith" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pydantic-settings" }, { name = "pyyaml" }, { name = "requests" }, @@ -5443,7 +5445,7 @@ dependencies = [ { name = "langchain-text-splitters" }, { name = "lark" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pymongo" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f2/30/89ea882d3b13642049afdef94782185cf030a0f430e3785c5b8445c808d1/langchain_mongodb-0.7.0.tar.gz", hash = "sha256:f89479b8902239f29c3664dedbc35ef802d86662a6a847213c145fdd46747fb6", size = 280207, upload-time = "2025-08-19T21:12:53.447Z" } @@ -5500,7 +5502,7 @@ dependencies = [ { name = "langchain-core" }, { name = "langchain-openai" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pinecone", extra = ["asyncio"] }, { name = "pydantic" }, { name = "simsimd" }, @@ -5565,7 +5567,7 @@ wheels = [ [[package]] name = "langflow" -version = "1.8.0" +version = "1.8.1" source = { editable = "." } dependencies = [ { name = "langflow-base", extra = ["complete"] }, @@ -5741,7 +5743,7 @@ dev = [ [[package]] name = "langflow-base" -version = "0.8.0" +version = "0.8.1" source = { editable = "src/backend/base" } dependencies = [ { name = "aiofile" }, @@ -6474,7 +6476,7 @@ requires-dist = [ { name = "emoji", specifier = ">=2.12.0,<3.0.0" }, { name = "faiss-cpu", marker = "extra == 'faiss'", specifier = "==1.9.0.post1" }, { name = "fake-useragent", marker = "extra == 'fake-useragent'", specifier = ">=2.0.0" }, - { name = "fastapi", specifier = ">=0.115.2,<1.0.0" }, + { name = "fastapi", specifier = ">=0.135.0,<1.0.0" }, { name = "fastapi-pagination", specifier = ">=0.13.1,<1.0.0" }, { name = "fastavro", marker = "python_full_version >= '3.13' and extra == 'fastavro'", specifier = ">=1.9.8,<2.0.0" }, { name = "fastavro", marker = "python_full_version < '3.13' and extra == 'fastavro'", specifier = "==1.9.7" }, @@ -6840,7 +6842,7 @@ wheels = [ [[package]] name = "langsmith" -version = "0.7.14" +version = "0.7.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -6853,9 +6855,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/32/b3931027ff7d635a66a0edbeec9f8a285fe77b04f1f0cbbc58fd20f2555a/langsmith-0.7.14.tar.gz", hash = "sha256:95606314a8dea0ea1ff3650da4cf0433737b14c4c296579c6b770b43cb5e0b37", size = 1113666, upload-time = "2026-03-06T20:13:17.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/79/81041dde07a974e728db7def23c1c7255950b8874102925cc77093bc847d/langsmith-0.7.17.tar.gz", hash = "sha256:6c1b0c2863cdd6636d2a58b8d5b1b80060703d98cac2593f4233e09ac25b5a9d", size = 1132228, upload-time = "2026-03-12T20:41:10.808Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/4f/b81ee2d06e1d69aa689b43d2b777901c060d257507806cad7cd9035d5ca4/langsmith-0.7.14-py3-none-any.whl", hash = "sha256:754dcb474a3f3f83cfefbd9694b897bce2a1a0b412bf75e256f85a64206ddcb7", size = 347350, upload-time = "2026-03-06T20:13:15.706Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/62689d57f4d25792bd6a3c05c868771899481be2f3e31f9e71d31e1ac4ab/langsmith-0.7.17-py3-none-any.whl", hash = "sha256:cbec10460cb6c6ecc94c18c807be88a9984838144ae6c4693c9f859f378d7d02", size = 359147, upload-time = "2026-03-12T20:41:08.758Z" }, ] [[package]] @@ -6898,11 +6900,11 @@ wheels = [ [[package]] name = "latex2mathml" -version = "3.78.1" +version = "3.79.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/26/57b1034c08922d0aefea79430a5e0006ffaee4f0ec59d566613f667ab2f7/latex2mathml-3.78.1.tar.gz", hash = "sha256:f941db80bf41db33f31df87b304e8b588f8166b813b0257c11c98f7a9d0aac71", size = 74030, upload-time = "2025-08-29T23:34:23.178Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/8d/2161f46485d9c36c0fa0e1c997faf08bb7843027e59b549598e49f55f8bf/latex2mathml-3.79.0.tar.gz", hash = "sha256:11bde318c2d2d6fcdd105a07509d867cee2208f653278eb80243dec7ea77a0ce", size = 151103, upload-time = "2026-03-12T23:25:08.028Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/76/d661ea2e529c3d464f9efd73f9ac31626b45279eb4306e684054ea20e3d4/latex2mathml-3.78.1-py3-none-any.whl", hash = "sha256:f089b6d75e85b937f99693c93e8c16c0804008672c3dd2a3d25affd36f238100", size = 73892, upload-time = "2025-08-29T23:34:21.98Z" }, + { url = "https://files.pythonhosted.org/packages/fd/92/56a954dd59637dd2ee013581fa3beea0821f17f2c07f818fc51dcc11fd10/latex2mathml-3.79.0-py3-none-any.whl", hash = "sha256:9f10720d4fcf6b22d1b81f6628237832419a7a29783c13aa92fa8d680165e63d", size = 73945, upload-time = "2026-03-12T23:25:09.466Z" }, ] [[package]] @@ -6919,7 +6921,7 @@ wheels = [ [[package]] name = "lfx" -version = "0.3.0" +version = "0.3.1" source = { editable = "src/lfx" } dependencies = [ { name = "ag-ui-protocol" }, @@ -6993,7 +6995,7 @@ requires-dist = [ { name = "defusedxml", specifier = ">=0.7.1,<1.0.0" }, { name = "docstring-parser", specifier = ">=0.16,<1.0.0" }, { name = "emoji", specifier = ">=2.14.1,<3.0.0" }, - { name = "fastapi", specifier = ">=0.115.13,<1.0.0" }, + { name = "fastapi", specifier = ">=0.135.0,<1.0.0" }, { name = "filelock", specifier = ">=3.20.1,<4.0.0" }, { name = "httpx", extras = ["http2"], specifier = ">=0.24.0,<1.0.0" }, { name = "json-repair", specifier = ">=0.30.3,<1.0.0" }, @@ -7130,7 +7132,7 @@ dependencies = [ { name = "diskcache" }, { name = "jinja2" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/19/89836022affc1bf470e2485e28872b489254a66fe587155edba731a07112/llama_cpp_python-0.2.90.tar.gz", hash = "sha256:419b041c62dbdb9f7e67883a6ef2f247d583d08417058776be0bff05b4ec9e3d", size = 63762953, upload-time = "2024-08-29T07:00:35.267Z" } @@ -7150,7 +7152,7 @@ dependencies = [ { name = "nest-asyncio", marker = "python_full_version >= '3.12'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "nltk", marker = "python_full_version >= '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "openai", marker = "python_full_version >= '3.12'" }, { name = "pandas", marker = "python_full_version >= '3.12'" }, { name = "pillow", marker = "python_full_version >= '3.12'" }, @@ -7243,11 +7245,11 @@ wheels = [ [[package]] name = "logfire-api" -version = "4.27.0" +version = "4.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ff/70dce6fe0bcb8f12b1b3293cfdd01805767ee7e5078dbdb004dba71bc8a3/logfire_api-4.27.0.tar.gz", hash = "sha256:479740825cf9a3cbf76caef003f44a8d8bd5b6959974b07e8fe9df65f2a672e2", size = 76411, upload-time = "2026-03-06T18:24:29.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/a4/ed2d823b4ad9a4c9dad1959c3399705c90ed3d96e6faaea5b897deb0f17c/logfire_api-4.29.0.tar.gz", hash = "sha256:55430c554cf198dcbddee390eca259a10a26d5f7e3527d51f859ddc31a83c840", size = 76407, upload-time = "2026-03-13T15:30:25.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/32/1375fb46671bce46cee984d8aef271ace714d91c7ee35b7b71385dc7e4c5/logfire_api-4.27.0-py3-none-any.whl", hash = "sha256:d01e7cb5d5ab0875e6400c0dc7b6f61ce5eeb0181845112c1fda64303fd41d6f", size = 121465, upload-time = "2026-03-06T18:24:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cc/62df4abc3e4650c25b81a8e39a1d498d3246c43f3aa4bfab7a73689317b4/logfire_api-4.29.0-py3-none-any.whl", hash = "sha256:48a1361b818357f5a37c71f9683f97e626e5df6c17f35212bfc1f19dddc6771c", size = 121457, upload-time = "2026-03-13T15:30:22.652Z" }, ] [[package]] @@ -7431,7 +7433,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "onnxruntime" }, { name = "python-dotenv" }, ] @@ -7719,24 +7721,24 @@ wheels = [ [[package]] name = "mlx" -version = "0.31.0" +version = "0.31.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mlx-metal", marker = "python_full_version >= '3.12' and sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/73/54/269d13847b04b07523d44cf903e1d3c6d48f56e6e89dda7e16418b411629/mlx-0.31.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:38680838e0dd9a621ed4adc5a9ed8b94aeb6a4798142fbe215b821b8c6b8fc36", size = 575395, upload-time = "2026-02-27T23:49:11.886Z" }, - { url = "https://files.pythonhosted.org/packages/3d/86/1fbe1f8f3a23c92c821c235ab7a28395c86c900b0a2b2425f3c8862bbeb6/mlx-0.31.0-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:7aded590bcf6839307c3acc899e196936991f97b499ddbdd0cd3b228bf10792f", size = 575394, upload-time = "2026-02-27T23:49:13.738Z" }, - { url = "https://files.pythonhosted.org/packages/20/01/02b79132e91182c779bb6c4f586c5fb86d49c32e8f07f307d2d4ca64cca6/mlx-0.31.0-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:6e3ae83607b798b44cb3e44437095cfd26886fecc15f90f29f9eafd206d4d170", size = 575411, upload-time = "2026-02-27T23:49:15.374Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d3/fcb8b9f645ae70b3295a353999c3c6c7a66fd43ed8aa716b13da12bf40d4/mlx-0.31.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:285313eaeba425e58cbb3238c2d1a3894e6252d58f243ce56681d5419a568d6c", size = 575602, upload-time = "2026-02-27T23:49:19.314Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/d35072e8dc31d9550f8218cfc388c1cd12c7fd89e8246540a9c7b873d958/mlx-0.31.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:acf4f04ff33a80784a0f15c492166dc889e65659b41c410ca5a7c2d78bee2a3a", size = 575603, upload-time = "2026-02-27T23:49:20.651Z" }, - { url = "https://files.pythonhosted.org/packages/43/fa/eca64a514cd50a4a38cc9b8827db85d9e554c3fe407ede043d061055b1ab/mlx-0.31.0-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:f624571e23a86654496c42a507b4bb42ded0edb91f33161fabafdbf6b81ba024", size = 575637, upload-time = "2026-02-27T23:49:22.02Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7d/87fb0daa006dbbbd8894c3d496c7d9dfc52e4ade260482276d3eca137a15/mlx-0.31.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:de6c0a3e8aa0e7d1365d46634fdbb3f835c164fbdb6ba8a239e039a4efa07fe2", size = 575834, upload-time = "2026-02-27T23:49:26.61Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e3/aa0fac5a9d52b1a4686c7097e56775c1a96dee3084f9c587b74e4c2cd284/mlx-0.31.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:d6af01b15177da995336a6fd9878e7c5994720a9f1614d8f4d1dbe9293167c30", size = 575836, upload-time = "2026-02-27T23:49:28.505Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/6aa3edaa34aeef370634756b7d131b8dc1cdb0002ddecdd3d876b5f9fa0c/mlx-0.31.0-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:1ad14ddc3a15818f5bba0de35e88559ed8dcb93ccff2ef879ff604d02d663b25", size = 575828, upload-time = "2026-02-27T23:49:29.684Z" }, - { url = "https://files.pythonhosted.org/packages/4a/09/35d1192cf1f655438213d8baa2264a8bc2426b44d93802dabfc177fd8e81/mlx-0.31.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4f33e9aafc6d3ad29e72743dfb786c4ce67397414f0a091469058626381fc1bc", size = 575815, upload-time = "2026-02-27T23:49:34.607Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/29e0cb154a31ed05c9d24c776513bf1ec506b8570e214b4563b55bb19ef6/mlx-0.31.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:242806b8ad6a4d3ce86cdff513f86520552de7592786712770b2e1ebd178816a", size = 575821, upload-time = "2026-02-27T23:49:35.947Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6c/437aefdca17216aab02d0fb7528cd63e2c3d8d9c1b079c07d579a770645f/mlx-0.31.0-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:7f0bdbac084017820ce513a12318771a06c7ec10fad159839e27c998bc5dad89", size = 575810, upload-time = "2026-02-27T23:49:37.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f9/f1663dafd45af02467f4f41777c13ec34b9104b2b0450d870c3f906285cd/mlx-0.31.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:bc46c911cc060d2eaf21b9e24a1712dc56763b660b53631b9057a32ab1c0271a", size = 574137, upload-time = "2026-03-12T02:15:54.996Z" }, + { url = "https://files.pythonhosted.org/packages/c6/26/1fd632f537a5160a21475a70aaef252090c62f9629f45ad20f5acfe810f3/mlx-0.31.1-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:fa132def5b3d959362077521c80f1fc80f64c45060d2940dc1d66a1aa19ce5f6", size = 574140, upload-time = "2026-03-12T02:15:56.709Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/e790fa8ddc1b27fea7ba749699883f31c65e166b18e4598beab4574e4686/mlx-0.31.1-cp310-cp310-macosx_26_0_arm64.whl", hash = "sha256:877ff2f98debd035b922825a0d7e7e1be0959fc5ca1d24cb5020a23e510ff16d", size = 574124, upload-time = "2026-03-12T02:15:58.323Z" }, + { url = "https://files.pythonhosted.org/packages/75/32/25dc2eae1d6f867224ef2bca2c644e3e913fe8067991f8394c090b720e3e/mlx-0.31.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8863835fb36c7c4f65008b1426ddb9ff7931a13c975e0ef58a40002ae8048922", size = 574311, upload-time = "2026-03-12T02:16:02.651Z" }, + { url = "https://files.pythonhosted.org/packages/9b/bf/c5aa1d1154f5a216139c8162cd3e6568b7eb427390d655f7f5ae3a1a61e7/mlx-0.31.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:0de504c1f1fe73b32fc3cf457b8eac30d1f7ce22440ef075c1970f96712e6fff", size = 574312, upload-time = "2026-03-12T02:16:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/3a/88/ef57747552c9e9da0c28465d9266c05a0009b698d90fb0bc63eb81840b8d/mlx-0.31.1-cp311-cp311-macosx_26_0_arm64.whl", hash = "sha256:10715b895e1f3e984c2c54257b7db956ff8af1fa93255412794a3724fe2dd3b1", size = 574385, upload-time = "2026-03-12T02:16:05.528Z" }, + { url = "https://files.pythonhosted.org/packages/38/29/71fe1f68756f515856e6930973c23245810d4aa3cd22fddd719d86a709dc/mlx-0.31.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8a63b31a398c9519f2bb0c81cf3865d9baca4ff573ffc31ead465d18286184e8", size = 574308, upload-time = "2026-03-12T02:16:10.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/be/70654a2cee0d71fd10bd237a50a79d06ae51679a194db6a3b16c0c84e6a5/mlx-0.31.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:a7a9347df4dcc41f0d16ff70b65650820af4879f686534b233b16826a22afa00", size = 574309, upload-time = "2026-03-12T02:16:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/ad/69/c7bc7b04f76b0cbd678f328011d1634bd0bcfc2da45aba06e084cb031127/mlx-0.31.1-cp312-cp312-macosx_26_0_arm64.whl", hash = "sha256:6cdb797ea31787d1ce9e5be77991c4bd5cbf129ab15f7253b78e09737f535fce", size = 574289, upload-time = "2026-03-12T02:16:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/44/45/04465da443634b23fb11670bbd2f7538b1ed43ffc5e0de44a95b3c29e9c1/mlx-0.31.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9a6d3410fc951bd28508fed9c1ab5d9903f6f6bb101c3a5d63d4191d49a384a1", size = 574268, upload-time = "2026-03-12T02:16:17.27Z" }, + { url = "https://files.pythonhosted.org/packages/85/7b/84956960356ff36e8c1bbed68fac96709e98e6a1adbc8e3d0ff71022d84e/mlx-0.31.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:20bd7ba19882603ac22711092d0e799f1ff7b5183c2c641d417dab4d2423d99e", size = 574265, upload-time = "2026-03-12T02:16:18.479Z" }, + { url = "https://files.pythonhosted.org/packages/86/01/d6f0ef5b8c0b390af08246d1301e9717dfb076b3920012b53105a888ed8c/mlx-0.31.1-cp313-cp313-macosx_26_0_arm64.whl", hash = "sha256:4c4565d6f4f8ce295613ee342d313ee5ab0b0eab9a6272954450f8343f7876bc", size = 574172, upload-time = "2026-03-12T02:16:19.898Z" }, ] [[package]] @@ -7746,7 +7748,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2", marker = "python_full_version >= '3.12'" }, { name = "mlx", marker = "python_full_version >= '3.12' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "protobuf", marker = "python_full_version >= '3.12'" }, { name = "pyyaml", marker = "python_full_version >= '3.12'" }, { name = "sentencepiece", marker = "python_full_version >= '3.12'" }, @@ -7759,12 +7761,12 @@ wheels = [ [[package]] name = "mlx-metal" -version = "0.31.0" +version = "0.31.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/4f/0a0671dfa62b59bf429edab0e2c9c7f9bc77865aa4218cd46f2f41d7d11a/mlx_metal-0.31.0-py3-none-macosx_14_0_arm64.whl", hash = "sha256:1c572a6e3634a63060c103b0c38ac309e2d217be15519e3d8f0d6b452bb015f5", size = 38596752, upload-time = "2026-02-27T23:29:39.52Z" }, - { url = "https://files.pythonhosted.org/packages/8d/42/c6d7bfd097b777f932d6cf8c79e41b565070b63cc452a069b8804e505140/mlx_metal-0.31.0-py3-none-macosx_15_0_arm64.whl", hash = "sha256:554dc7cb29e0ea5fb6941df42f11a1de385b095848e6183c7a99d7c1f1a11f5d", size = 38595434, upload-time = "2026-02-27T23:29:43.285Z" }, - { url = "https://files.pythonhosted.org/packages/ed/8f/cdaffd759b4c71e74c294e773daacad8aafabac103b93e0aa56d4468d279/mlx_metal-0.31.0-py3-none-macosx_26_0_arm64.whl", hash = "sha256:7fd412f55ddf9f1d90c2cd86ce281d19e8eb93d093c6dbd784a49f8bd7d0a22c", size = 47879607, upload-time = "2026-02-27T23:29:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/39/66/2313497fdbc7fbadf8e026c09366e3f049f9114e65ca4edc23cdb8699186/mlx_metal-0.31.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:70741174131dbf7fdd479cb730e06e08c358eac3bf7905d9e884e7960cfdd5b8", size = 38624074, upload-time = "2026-03-12T02:15:48.036Z" }, + { url = "https://files.pythonhosted.org/packages/c7/34/4c3c6890ce6095b2ab2ba2f5f15c9a7ba17208d47f8cacb572885a2dc0eb/mlx_metal-0.31.1-py3-none-macosx_15_0_arm64.whl", hash = "sha256:6c56bd8cd27743e635f5a90a22535af7c31bd22b4b126d46b6da2da52d72e413", size = 38618950, upload-time = "2026-03-12T02:15:51.908Z" }, + { url = "https://files.pythonhosted.org/packages/51/bc/987cb99e3aafb296aa11ce5133838a10eae8447edd53168d0804d4fb3a14/mlx_metal-0.31.1-py3-none-macosx_26_0_arm64.whl", hash = "sha256:e7324b7c56b519ae67c025d3ced07e5d35bc3a9f19d4c45fe4927f385148c59e", size = 49256543, upload-time = "2026-03-12T02:15:54.851Z" }, ] [[package]] @@ -7776,7 +7778,7 @@ dependencies = [ { name = "fastapi", marker = "python_full_version >= '3.12'" }, { name = "mlx", marker = "python_full_version >= '3.12'" }, { name = "mlx-lm", marker = "python_full_version >= '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "opencv-python", version = "4.13.0.92", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pillow", marker = "python_full_version >= '3.12'" }, { name = "requests", marker = "python_full_version >= '3.12'" }, @@ -8267,7 +8269,7 @@ version = "2.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/67/c7415cf04ebe418193cfd6595ae03e3a64d76dac7b9c010098b39cc7992e/numexpr-2.10.2.tar.gz", hash = "sha256:b0aff6b48ebc99d2f54f27b5f73a58cb92fde650aeff1b397c71c8788b4fff1a", size = 106787, upload-time = "2024-11-23T13:34:23.127Z" } wheels = [ @@ -8345,7 +8347,7 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.2" +version = "2.4.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and sys_platform == 'win32'", @@ -8356,58 +8358,58 @@ resolution-markers = [ "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, - { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, - { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, - { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, - { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, - { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, - { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, - { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, - { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, - { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, + { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, + { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, ] [[package]] @@ -8512,7 +8514,7 @@ dependencies = [ { name = "coloredlogs" }, { name = "flatbuffers" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "protobuf" }, { name = "sympy" }, @@ -8614,7 +8616,7 @@ resolution-markers = [ "python_full_version == '3.12.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, @@ -8664,7 +8666,7 @@ resolution-markers = [ "python_full_version == '3.12.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, @@ -8729,7 +8731,7 @@ wheels = [ [[package]] name = "openinference-instrumentation-openai" -version = "0.1.41" +version = "0.1.42" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openinference-instrumentation" }, @@ -8740,23 +8742,23 @@ dependencies = [ { name = "typing-extensions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/06/77b2fe7171336f71313936daf1b644a9968da85ff0b473a03ca05cc3d5c1/openinference_instrumentation_openai-0.1.41.tar.gz", hash = "sha256:ef4db680986a613b1639720f9beaa315c9e388c20bc985dbbbdf0f4df007c6e9", size = 22848, upload-time = "2025-12-04T19:58:35.349Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/e4/cf114f6fedc90dde6e1d4062e55686542f8b7636a4d3340b81a49b1a09a8/openinference_instrumentation_openai-0.1.42.tar.gz", hash = "sha256:6f6b340292ab7dd7dc2e9a944958f7f812108efaafbfbcaa3f7ba205744ad1ce", size = 22839, upload-time = "2026-03-11T04:45:51.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/db/48f1f540d335f98fa67891e9c25ad56020be7e7b2c0d4fd5014875fe5ddf/openinference_instrumentation_openai-0.1.41-py3-none-any.whl", hash = "sha256:6fad453446835e51333b660882eacababbf1052689ca53cba444a7d97fa2e910", size = 30273, upload-time = "2025-12-04T19:58:34.17Z" }, + { url = "https://files.pythonhosted.org/packages/c5/88/eaaa4840bf1ed8ff8c0927cd6ad5653ee0cfac14bfcb4e1e8f06fb0be9e8/openinference_instrumentation_openai-0.1.42-py3-none-any.whl", hash = "sha256:e7ff7b98612102d4a3e342842d3dd231709ff51abdc4b193e5df09e9afcfac0f", size = 30333, upload-time = "2026-03-11T04:45:48.535Z" }, ] [[package]] name = "openinference-semantic-conventions" -version = "0.1.27" +version = "0.1.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b4/cf/7c0ea344b99ecdb9f55cb5287ecb6a6ad68e6098df32728691fa7846f112/openinference_semantic_conventions-0.1.27.tar.gz", hash = "sha256:db0b1bbc1cd66f8108b17f972976fa1833413a01967ff930e2707a77e0f66bd3", size = 12744, upload-time = "2026-03-04T08:38:56.974Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/32/c79bf8bd3ea5a00e492449b31ca600bbc2a8e88a301e42c872af925a156c/openinference_semantic_conventions-0.1.28.tar.gz", hash = "sha256:6388465174e8ab3f27ebc6a9e9bb2e1b804d30caefb57234e16db874da1c6a7b", size = 12893, upload-time = "2026-03-11T04:45:46.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/b4/1c218ed1e68c8fbd68f37258ce40f908cdad86ee9d38b12c48c9e4c4e030/openinference_semantic_conventions-0.1.27-py3-none-any.whl", hash = "sha256:3871fd2cc9d203bdb444ab66ff2ba9bdbf1013dc48c64c5700dd449d47b338c5", size = 10430, upload-time = "2026-03-04T08:38:56.166Z" }, + { url = "https://files.pythonhosted.org/packages/04/40/34b570462c3ce250277254bb0cca655eb39b64c0dffe63cd7751f103f8d6/openinference_semantic_conventions-0.1.28-py3-none-any.whl", hash = "sha256:a2fed5bb167aa56c1c7448cdb7a8d775f989339ba1f8b04a7b45d4f8388cccfb", size = 10522, upload-time = "2026-03-11T04:45:45.423Z" }, ] [[package]] name = "openlayer" -version = "0.18.0" +version = "0.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -8772,9 +8774,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/e5/4269ab17a699c63b8c62c8f45c978d937a43f6e94a7a1ac1941de0a28474/openlayer-0.18.0.tar.gz", hash = "sha256:b79b76b6e68dc30abb8c06dd255b5ab050fc4a6367e4020a2e927efd533f5c84", size = 333438, upload-time = "2026-03-06T20:56:31.248Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/49/48547befa7b75f55beb3d0fec9ae45be589ae5c4009ca92fcc9a9c3a783c/openlayer-0.19.1.tar.gz", hash = "sha256:9fbe99f31815a3c84ee12a33fcb5c85655e937e64b5beaeb28d5ead44c93b350", size = 334960, upload-time = "2026-03-12T19:48:04.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/7e/f600e84ce0534e02231bf0268233ee0d0bcd113006becc1df76b47299a7d/openlayer-0.18.0-py3-none-any.whl", hash = "sha256:fbb939d13f1e2171758ade2c2331265f539c1a6f278e491e80e974c83e0f9d21", size = 301916, upload-time = "2026-03-06T20:56:29.535Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9e/bf5d3b5e5e0e7495533e7b30150cca1c07e51e789743bbac75090daf4690/openlayer-0.19.1-py3-none-any.whl", hash = "sha256:365d2c0b85f7e67055352f0b4a95ae314f9b0bebf52a30c65ac96d777a38cb6a", size = 303043, upload-time = "2026-03-12T19:48:02.85Z" }, ] [[package]] @@ -9592,7 +9594,7 @@ dependencies = [ { name = "alembic" }, { name = "colorlog" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "sqlalchemy" }, @@ -9739,7 +9741,7 @@ version = "2.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, @@ -9820,7 +9822,7 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/27/1d/297ff2c7ea50a768a2247621d6451abb2a07c0e9be7ca6d36ebe371658e5/pandas_stubs-3.0.0.260204.tar.gz", hash = "sha256:bf9294b76352effcffa9cb85edf0bed1339a7ec0c30b8e1ac3d66b4228f1fbc3", size = 109383, upload-time = "2026-02-04T15:17:17.247Z" } wheels = [ @@ -9890,7 +9892,7 @@ version = "0.3.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/d8/fd6009cee3e03214667df488cdcf9609461d729968da94e4f95d6359d304/pgvector-0.3.6.tar.gz", hash = "sha256:31d01690e6ea26cea8a633cde5f0f55f5b246d9c8292d68efdef8c22ec994ade", size = 25421, upload-time = "2024-10-27T00:15:09.632Z" } wheels = [ @@ -10155,25 +10157,25 @@ wheels = [ [[package]] name = "primp" -version = "1.1.2" +version = "1.1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/35/80be154508529f753fb82cb81298bdeb33e90f39f9901d7cfa0f488a581f/primp-1.1.2.tar.gz", hash = "sha256:c4707ab374a77c0cbead3d9a65605919fa4997fa910ef06e37b65df42a1d4d04", size = 313908, upload-time = "2026-03-01T05:52:49.773Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/0e/62ed44af95c66fd6fa8ad49c8bde815f64c7e976772d6979730be2b7cd97/primp-1.1.3.tar.gz", hash = "sha256:56adc3b8a5048cbd5f926b21fdff839195f3a9181512ca33f56ddc66f4c95897", size = 311356, upload-time = "2026-03-11T06:42:51.763Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/13/dc9588356d983f988877ae065c842cdd6cf95073615b56b460cbe857f3dc/primp-1.1.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:181bb9a6d5544e0483592f693f33f5874a60726ea0da1f41685aa2267f084a4d", size = 4002669, upload-time = "2026-03-01T05:52:31.977Z" }, - { url = "https://files.pythonhosted.org/packages/70/af/6a6c26141583a5081bad69b9753c85df81b466939663742ef5bec35ee868/primp-1.1.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f362424ffa83e1de55a7573300a416fa71dc5516829526a9bf77dc0cfa42256b", size = 3743010, upload-time = "2026-03-01T05:52:38.452Z" }, - { url = "https://files.pythonhosted.org/packages/a9/99/03db937e031a02885d8c80d073d7424967d629721b5044dcb4e80b6cbdcf/primp-1.1.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:736820326eb1ed19c6b0e971f852316c049c36bdd251a03757056a74182796df", size = 3889905, upload-time = "2026-03-01T05:52:20.616Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/faecef36238f464e2dd52056420676eb2d541cd20ff478d3b967815079e3/primp-1.1.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed37d1bc89fa8cad8b60481c81ea7b3bd42dc757868009ad3bb0b1e74c17fd22", size = 3524521, upload-time = "2026-03-01T05:52:08.403Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d5/8954e5b5b454139ff35063d5a143a1570f865b736cfd8a46cc7ce9575a5a/primp-1.1.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e78355b1c495bc7e3d92121067760c7e7a1d419519542ed9dd88688ce43aab", size = 3738228, upload-time = "2026-03-01T05:52:05.127Z" }, - { url = "https://files.pythonhosted.org/packages/26/e7/dc93dbeddb7642e12f4575aaf2c9fda7234b241050a112a9baa288971b16/primp-1.1.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c4c560d018dad4e3a3f17b07f9f5d894941e3acbbb5b566f6b6baf42786012f", size = 4013704, upload-time = "2026-03-01T05:52:48.529Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3d/2cc2e0cd310f585df05a7008fd6de4542d7c0bc61e62b6797f28a9ede28b/primp-1.1.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2494b52cf3056d3e41c0746a11cbeca7f2f882a92a09d87383646cd75e2f3d8c", size = 3920174, upload-time = "2026-03-01T05:52:06.635Z" }, - { url = "https://files.pythonhosted.org/packages/35/60/dc4572ba96911374b43b4f5d1f012706c3f27fd2c12dd3e158fcf74ac3dd/primp-1.1.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c424a46f48ccd8fd309215a15bc098b47198b8f779c43ed8d95b3f53a382ffa8", size = 4113822, upload-time = "2026-03-01T05:52:51.061Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2e/90f5f8e138f8bc6652c5134aa59a746775623a820f92164c6690217e49d6/primp-1.1.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba51cf19f17fd4bab4567d96b4cd7dcb6a4e0f0d4721819180b46af9794ae310", size = 4068028, upload-time = "2026-03-01T05:52:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ea/753d8edcb85c3c36d5731fbd2b215528738d917ae9cf3dce651ae0f1c529/primp-1.1.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:77ebae43c6735328051beb08e7e2360b6cf79d50f6cef77629beba880c99222d", size = 3754469, upload-time = "2026-03-01T05:52:15.671Z" }, - { url = "https://files.pythonhosted.org/packages/ae/51/b417cd741bf8eacea86debad358a6dc5821e2849a22e2c91cff926bebbb2/primp-1.1.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5f3252d47e9d0f4a567990c79cd388be43353fc7c78efea2a6a5734e8a425598", size = 3859330, upload-time = "2026-03-01T05:52:46.979Z" }, - { url = "https://files.pythonhosted.org/packages/3e/20/19db933c878748e9a7b9ad4057e9caf7ad9c91fd27d2a2692ac629453a66/primp-1.1.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9e094417825df9748e179a1104b2df4459c3dbd1eea994f05a136860b847f0e1", size = 4365491, upload-time = "2026-03-01T05:52:35.007Z" }, - { url = "https://files.pythonhosted.org/packages/fc/0f/48a57ee744cc6dc64fb7daff7bc04e9ec3cefd0594d008a775496dddaeb1/primp-1.1.2-cp310-abi3-win32.whl", hash = "sha256:bc67112b61a8dc1d40ddcc81ff5c47a1cb7b620954fee01a529e28bebb359e20", size = 3266998, upload-time = "2026-03-01T05:52:02.059Z" }, - { url = "https://files.pythonhosted.org/packages/9c/0a/119d497fb098c739142d4a47b062a8a9cc0b4b87aca65334150066d075a0/primp-1.1.2-cp310-abi3-win_amd64.whl", hash = "sha256:4509850301c669c04e124762e953946ed10fe9039f059ec40b818c085697d9a4", size = 3601691, upload-time = "2026-03-01T05:52:12.34Z" }, - { url = "https://files.pythonhosted.org/packages/95/1f/2b8f218aebb4f236d94ae148b4f5c0471b3d00316b0ef5d0b7c2222d8417/primp-1.1.2-cp310-abi3-win_arm64.whl", hash = "sha256:de5958dc7ce78ce107dd776056a58f9da7a7164a912e908cb9b66b84f87967f6", size = 3613756, upload-time = "2026-03-01T05:52:28.279Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6b/36794b5758a0dd1251e67b6ab3ea946e53fa69745e0ecc29facc072ddf5b/primp-1.1.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:24383cfc267f620769be102b7fa4b64c7d47105f86bd21d047f1e07709e83c6e", size = 4000660, upload-time = "2026-03-11T06:42:58.092Z" }, + { url = "https://files.pythonhosted.org/packages/98/18/ebbe318a926d158c57f9e9cf49bbea70e8f0bd7f87e7675ed68e0d6ab433/primp-1.1.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:61bcb8c53b41e4bac43d04a1374b6ab7d8ded0f3517d32c5cdd5c30562756805", size = 3737318, upload-time = "2026-03-11T06:42:50.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4c/430c9154284b53b771e6713a18dec4ad0159e4a501a20b222d67c730ced9/primp-1.1.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0c6b9388578ee9d903f30549a792c5f391fdeb9d36b508da2ffb8e13c764954", size = 3881005, upload-time = "2026-03-11T06:43:12.894Z" }, + { url = "https://files.pythonhosted.org/packages/93/34/2466ef66386a1b50e6aaf7832f9f603628407bb33342378faf4b38c4aee8/primp-1.1.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09a8bfa870c92c81d76611846ec53b2520845e3ec5f4139f47604986bcf4bc25", size = 3514480, upload-time = "2026-03-11T06:43:06.058Z" }, + { url = "https://files.pythonhosted.org/packages/ff/42/ca7a71df6493dd6c1971c0cc3b20b8125e2547eb3bf88b4429715cb6ed81/primp-1.1.3-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac372cb9959fff690b255fad91c5b3bc948c14065da9fc00ad80d139651515af", size = 3734658, upload-time = "2026-03-11T06:43:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7c/0fb34db619e9935e11140929713c2c7b5323c1e8ba75cad6f0aade51c89d/primp-1.1.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3034672a007f04e12b8fe7814c97ea172e8b9c5d45bd7b00cf6e7334fdd4222a", size = 4011898, upload-time = "2026-03-11T06:43:41.121Z" }, + { url = "https://files.pythonhosted.org/packages/da/8b/afd1bd8b14f38d58c5ebd0d45fc6b74914956907aa4e981bb2e5231626d3/primp-1.1.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a07d5b7d7278dc63452a59f3bf851dc4d1f8ddc2aada7844cbdb68002256e2f4", size = 3910728, upload-time = "2026-03-11T06:43:01.819Z" }, + { url = "https://files.pythonhosted.org/packages/32/9e/1ec3a9678efcbb51e50d7b4886d9195f956c9fd7f4efcff13ccb152248b0/primp-1.1.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08eec2f58abbcc1060032a2af81dabacec87a580a364a75862039f7422ac82e6", size = 4114189, upload-time = "2026-03-11T06:42:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/76de611027c0688be188d5a833be45b1e36d9c0c98baefab27bf6336ab9d/primp-1.1.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9716d4cd36db2c175443fe1bbd54045a944fc9c49d01a385af8ada1fe9c948df", size = 4061973, upload-time = "2026-03-11T06:43:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/37/3b/a30a5ea366705d0ece265b12ad089793d644bd5730b18201e3a0a7fa7b5f/primp-1.1.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e19daca65dc6df369c33e711fa481ad2afe5d26c5bde926c069b3ab067c4fd45", size = 3747920, upload-time = "2026-03-11T06:43:10.403Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/e3c323221c371cdfe6c2ed971f7a70e3b69f30b561977715c55230bd5fda/primp-1.1.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:ee357537712aa486364b0194cf403c5f9eaaa1354e23e9ac8322a22003f31e6b", size = 3861184, upload-time = "2026-03-11T06:43:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7f/babaf00753daad7d80061003d7ae1bdfca64ea94c181cdea8d25c8a7226a/primp-1.1.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06c53e77ebf6ac00633bc09e7e5a6d1a994592729d399ca8f065451a2574b92e", size = 4364610, upload-time = "2026-03-11T06:42:56.223Z" }, + { url = "https://files.pythonhosted.org/packages/03/48/c7bca8045c681f5f60972c180d2a20582c7a0857b3b07b12e0a0ee062ac4/primp-1.1.3-cp310-abi3-win32.whl", hash = "sha256:4b1ea3693c118bf04a6e05286f0a73637cf6fe5c9fd77fa1e29a01f190adf512", size = 3265160, upload-time = "2026-03-11T06:43:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/45/3e/4a4b8a0f6f15734cded91e85439e68912b2bb8eafe7132420c13c2db8340/primp-1.1.3-cp310-abi3-win_amd64.whl", hash = "sha256:5ea386a4c8c4d8c1021d17182f4ee24dbb6f17c107c4e9ee5500b6372cf08f32", size = 3603953, upload-time = "2026-03-11T06:43:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/70/46/1baf13a7f5fbed6052deb3e4822c69441a8d0fd990fe2a50e4cec802130b/primp-1.1.3-cp310-abi3-win_arm64.whl", hash = "sha256:63c7b1a1ccbcd07213f438375df186f807cdc5214bc2debb055737db9b5078de", size = 3619917, upload-time = "2026-03-11T06:42:44.76Z" }, ] [[package]] @@ -11022,11 +11024,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.11.0" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, + { url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" }, ] [package.optional-dependencies] @@ -11055,7 +11057,7 @@ wheels = [ [[package]] name = "pymilvus" -version = "2.6.9" +version = "2.6.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -11064,11 +11066,12 @@ dependencies = [ { name = "pandas" }, { name = "protobuf" }, { name = "python-dotenv" }, + { name = "requests" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/0c/92adff800a04cd3e9b3f17c06fa972c8d590846b1e0bac0ccf39e054b596/pymilvus-2.6.9.tar.gz", hash = "sha256:c53a3d84ff15814e251be13edda70a98a1c8a6090d7597a908387cbb94a9504a", size = 1493560, upload-time = "2026-02-10T11:01:27.415Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/85/90362066ccda5ff6fec693a55693cde659fdcd36d08f1bd7012ae958248d/pymilvus-2.6.10.tar.gz", hash = "sha256:58a44ee0f1dddd7727ae830ef25325872d8946f029d801a37105164e6699f1b8", size = 1561042, upload-time = "2026-03-13T09:54:22.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/56/ab7f0a5aba6fc06dc210a059d6f6d2ee1f3371d40e2b4366a409576554b8/pymilvus-2.6.9-py3-none-any.whl", hash = "sha256:3e14e8072f6429dcd79d52a24dc021c594cb80841ddd76cb974bc539d1f4cdda", size = 301225, upload-time = "2026-02-10T11:01:25.796Z" }, + { url = "https://files.pythonhosted.org/packages/88/10/fe7fbb6795aa20038afd55e9c653991e7c69fb24c741ebb39ba3b0aa5c13/pymilvus-2.6.10-py3-none-any.whl", hash = "sha256:a048b6f3ebad93742bca559beabf44fe578f0983555a109c4436b5fb2c1dbd40", size = 312797, upload-time = "2026-03-13T09:54:21.081Z" }, ] [[package]] @@ -11560,15 +11563,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.1.1" +version = "1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ec/67/09765eacf4e44413c4f8943ba5a317fcb9c7b447c3b8b0b7fce7e3090b0b/python_discovery-1.1.1.tar.gz", hash = "sha256:584c08b141c5b7029f206b4e8b78b1a1764b22121e21519b89dec56936e95b0a", size = 56016, upload-time = "2026-03-07T00:00:56.354Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/7e/9f3b0dd3a074a6c3e1e79f35e465b1f2ee4b262d619de00cfce523cc9b24/python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5", size = 56945, upload-time = "2026-03-10T15:08:15.038Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/0f/2bf7e3b5a4a65f623cb820feb5793e243fad58ae561015ee15a6152f67a2/python_discovery-1.1.1-py3-none-any.whl", hash = "sha256:69f11073fa2392251e405d4e847d60ffffd25fd762a0dc4d1a7d6b9c3f79f1a3", size = 30732, upload-time = "2026-03-07T00:00:55.143Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/73211fc5bfbfc562369b4aa61dc1e4bf07dc7b34df7b317e4539316b809c/python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e", size = 31485, upload-time = "2026-03-10T15:08:13.06Z" }, ] [[package]] @@ -11877,7 +11880,7 @@ dependencies = [ { name = "grpcio-tools" }, { name = "httpx", extra = ["http2"] }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "portalocker" }, { name = "pydantic" }, { name = "urllib3" }, @@ -11899,7 +11902,7 @@ dependencies = [ { name = "ijson" }, { name = "multiprocess" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "prompt-toolkit" }, { name = "pyarrow" }, { name = "pydantic" }, @@ -12004,7 +12007,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorlog" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "omegaconf" }, { name = "opencv-python", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "opencv-python", version = "4.13.0.92", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, @@ -12026,7 +12029,7 @@ version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "onnxruntime" }, { name = "opencv-python", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "opencv-python", version = "4.13.0.92", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, @@ -12477,18 +12480,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - [[package]] name = "rtree" version = "1.4.1" @@ -12632,7 +12623,7 @@ wheels = [ [package.optional-dependencies] torch = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "torch", version = "2.2.2", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "torch", version = "2.2.2+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, @@ -12703,7 +12694,7 @@ dependencies = [ { name = "lazy-loader", marker = "python_full_version >= '3.11'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging", marker = "python_full_version >= '3.11'" }, { name = "pillow", marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -12810,7 +12801,7 @@ resolution-markers = [ dependencies = [ { name = "joblib", marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] @@ -12923,7 +12914,7 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -13023,12 +13014,12 @@ wheels = [ [[package]] name = "sentence-transformers" -version = "5.2.3" +version = "5.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -13041,9 +13032,9 @@ dependencies = [ { name = "transformers" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/30/21664028fc0776eb1ca024879480bbbab36f02923a8ff9e4cae5a150fa35/sentence_transformers-5.2.3.tar.gz", hash = "sha256:3cd3044e1f3fe859b6a1b66336aac502eaae5d3dd7d5c8fc237f37fbf58137c7", size = 381623, upload-time = "2026-02-17T14:05:20.238Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/26/448453925b6ce0c29d8b54327caa71ee4835511aef02070467402273079c/sentence_transformers-5.3.0.tar.gz", hash = "sha256:414a0a881f53a4df0e6cbace75f823bfcb6b94d674c42a384b498959b7c065e2", size = 403330, upload-time = "2026-03-12T14:53:40.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/9f/dba4b3e18ebbe1eaa29d9f1764fbc7da0cd91937b83f2b7928d15c5d2d36/sentence_transformers-5.2.3-py3-none-any.whl", hash = "sha256:6437c62d4112b615ddebda362dfc16a4308d604c5b68125ed586e3e95d5b2e30", size = 494225, upload-time = "2026-02-17T14:05:18.596Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9c/2fa7224058cad8df68d84bafee21716f30892cecc7ad1ad73bde61d23754/sentence_transformers-5.3.0-py3-none-any.whl", hash = "sha256:dca6b98db790274a68185d27a65801b58b4caf653a4e556b5f62827509347c7d", size = 512390, upload-time = "2026-03-12T14:53:39.035Z" }, ] [[package]] @@ -13100,7 +13091,7 @@ version = "2.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } wheels = [ @@ -13232,11 +13223,11 @@ wheels = [ [[package]] name = "smmap" -version = "5.0.2" +version = "5.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] [[package]] @@ -13280,7 +13271,7 @@ version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "python_full_version >= '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } wheels = [ @@ -13631,7 +13622,7 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } wheels = [ @@ -13913,7 +13904,7 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "pillow", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "torch", version = "2.2.2", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, ] @@ -13931,7 +13922,7 @@ resolution-markers = [ "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "pillow", marker = "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "torch", version = "2.2.2+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, ] @@ -13948,7 +13939,7 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "pillow", marker = "platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine != 'x86_64' and sys_platform == 'darwin'" }, ] @@ -13975,7 +13966,7 @@ resolution-markers = [ ] dependencies = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' and sys_platform != 'darwin'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and sys_platform != 'darwin'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and sys_platform != 'darwin'" }, { name = "pillow", marker = "sys_platform != 'darwin'" }, { name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, ] @@ -13999,21 +13990,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.4" +version = "6.5.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, - { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, - { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, - { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, - { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, ] [[package]] @@ -14105,7 +14094,7 @@ dependencies = [ { name = "filelock" }, { name = "huggingface-hub" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, @@ -14512,60 +14501,65 @@ wheels = [ [[package]] name = "ujson" -version = "5.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0", size = 7156583, upload-time = "2025-08-20T11:57:02.452Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/8bf7a4fabfd01c7eed92d9b290930ce6d14910dec708e73538baa38885d1/ujson-5.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25", size = 55248, upload-time = "2025-08-20T11:55:02.368Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2e/eeab0b8b641817031ede4f790db4c4942df44a12f44d72b3954f39c6a115/ujson-5.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89", size = 53157, upload-time = "2025-08-20T11:55:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/21/1b/a4e7a41870797633423ea79618526747353fd7be9191f3acfbdee0bf264b/ujson-5.11.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6", size = 57657, upload-time = "2025-08-20T11:55:05.169Z" }, - { url = "https://files.pythonhosted.org/packages/94/ae/4e0d91b8f6db7c9b76423b3649612189506d5a06ddd3b6334b6d37f77a01/ujson-5.11.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb", size = 59780, upload-time = "2025-08-20T11:55:06.325Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cc/46b124c2697ca2da7c65c4931ed3cb670646978157aa57a7a60f741c530f/ujson-5.11.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db", size = 57307, upload-time = "2025-08-20T11:55:07.493Z" }, - { url = "https://files.pythonhosted.org/packages/39/eb/20dd1282bc85dede2f1c62c45b4040bc4c389c80a05983515ab99771bca7/ujson-5.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c", size = 1036369, upload-time = "2025-08-20T11:55:09.192Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/80072439065d493e3a4b1fbeec991724419a1b4c232e2d1147d257cac193/ujson-5.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138", size = 1195738, upload-time = "2025-08-20T11:55:11.402Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7e/d77f9e9c039d58299c350c978e086a804d1fceae4fd4a1cc6e8d0133f838/ujson-5.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915", size = 1088718, upload-time = "2025-08-20T11:55:13.297Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f1/697559d45acc849cada6b3571d53522951b1a64027400507aabc6a710178/ujson-5.11.0-cp310-cp310-win32.whl", hash = "sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723", size = 39653, upload-time = "2025-08-20T11:55:14.869Z" }, - { url = "https://files.pythonhosted.org/packages/86/a2/70b73a0f55abe0e6b8046d365d74230c20c5691373e6902a599b2dc79ba1/ujson-5.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0", size = 43720, upload-time = "2025-08-20T11:55:15.897Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5f/b19104afa455630b43efcad3a24495b9c635d92aa8f2da4f30e375deb1a2/ujson-5.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105", size = 38410, upload-time = "2025-08-20T11:55:17.556Z" }, - { url = "https://files.pythonhosted.org/packages/da/ea/80346b826349d60ca4d612a47cdf3533694e49b45e9d1c07071bb867a184/ujson-5.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f", size = 55248, upload-time = "2025-08-20T11:55:19.033Z" }, - { url = "https://files.pythonhosted.org/packages/57/df/b53e747562c89515e18156513cc7c8ced2e5e3fd6c654acaa8752ffd7cd9/ujson-5.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58", size = 53156, upload-time = "2025-08-20T11:55:20.174Z" }, - { url = "https://files.pythonhosted.org/packages/41/b8/ab67ec8c01b8a3721fd13e5cb9d85ab2a6066a3a5e9148d661a6870d6293/ujson-5.11.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26", size = 57657, upload-time = "2025-08-20T11:55:21.296Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/fb84f27cd80a2c7e2d3c6012367aecade0da936790429801803fa8d4bffc/ujson-5.11.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a", size = 59779, upload-time = "2025-08-20T11:55:22.772Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/48706f7c1e917ecb97ddcfb7b1d756040b86ed38290e28579d63bd3fcc48/ujson-5.11.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6", size = 57284, upload-time = "2025-08-20T11:55:24.01Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ce/48877c6eb4afddfd6bd1db6be34456538c07ca2d6ed233d3f6c6efc2efe8/ujson-5.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b", size = 1036395, upload-time = "2025-08-20T11:55:25.725Z" }, - { url = "https://files.pythonhosted.org/packages/8b/7a/2c20dc97ad70cd7c31ad0596ba8e2cf8794d77191ba4d1e0bded69865477/ujson-5.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba", size = 1195731, upload-time = "2025-08-20T11:55:27.915Z" }, - { url = "https://files.pythonhosted.org/packages/15/f5/ca454f2f6a2c840394b6f162fff2801450803f4ff56c7af8ce37640b8a2a/ujson-5.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3", size = 1088710, upload-time = "2025-08-20T11:55:29.426Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d3/9ba310e07969bc9906eb7548731e33a0f448b122ad9705fed699c9b29345/ujson-5.11.0-cp311-cp311-win32.whl", hash = "sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34", size = 39648, upload-time = "2025-08-20T11:55:31.194Z" }, - { url = "https://files.pythonhosted.org/packages/57/f7/da05b4a8819f1360be9e71fb20182f0bb3ec611a36c3f213f4d20709e099/ujson-5.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01", size = 43717, upload-time = "2025-08-20T11:55:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cc/f3f9ac0f24f00a623a48d97dc3814df5c2dc368cfb00031aa4141527a24b/ujson-5.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835", size = 38402, upload-time = "2025-08-20T11:55:33.641Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ef/a9cb1fce38f699123ff012161599fb9f2ff3f8d482b4b18c43a2dc35073f/ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702", size = 55434, upload-time = "2025-08-20T11:55:34.987Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/dba51a00eb30bd947791b173766cbed3492269c150a7771d2750000c965f/ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d", size = 53190, upload-time = "2025-08-20T11:55:36.384Z" }, - { url = "https://files.pythonhosted.org/packages/03/3c/fd11a224f73fbffa299fb9644e425f38b38b30231f7923a088dd513aabb4/ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80", size = 57600, upload-time = "2025-08-20T11:55:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/55/b9/405103cae24899df688a3431c776e00528bd4799e7d68820e7ebcf824f92/ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc", size = 59791, upload-time = "2025-08-20T11:55:38.877Z" }, - { url = "https://files.pythonhosted.org/packages/17/7b/2dcbc2bbfdbf68f2368fb21ab0f6735e872290bb604c75f6e06b81edcb3f/ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c", size = 57356, upload-time = "2025-08-20T11:55:40.036Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/fea2ca18986a366c750767b694430d5ded6b20b6985fddca72f74af38a4c/ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5", size = 1036313, upload-time = "2025-08-20T11:55:41.408Z" }, - { url = "https://files.pythonhosted.org/packages/a3/bb/d4220bd7532eac6288d8115db51710fa2d7d271250797b0bfba9f1e755af/ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b", size = 1195782, upload-time = "2025-08-20T11:55:43.357Z" }, - { url = "https://files.pythonhosted.org/packages/80/47/226e540aa38878ce1194454385701d82df538ccb5ff8db2cf1641dde849a/ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc", size = 1088817, upload-time = "2025-08-20T11:55:45.262Z" }, - { url = "https://files.pythonhosted.org/packages/7e/81/546042f0b23c9040d61d46ea5ca76f0cc5e0d399180ddfb2ae976ebff5b5/ujson-5.11.0-cp312-cp312-win32.whl", hash = "sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88", size = 39757, upload-time = "2025-08-20T11:55:46.522Z" }, - { url = "https://files.pythonhosted.org/packages/44/1b/27c05dc8c9728f44875d74b5bfa948ce91f6c33349232619279f35c6e817/ujson-5.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f", size = 43859, upload-time = "2025-08-20T11:55:47.987Z" }, - { url = "https://files.pythonhosted.org/packages/22/2d/37b6557c97c3409c202c838aa9c960ca3896843b4295c4b7bb2bbd260664/ujson-5.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6", size = 38361, upload-time = "2025-08-20T11:55:49.122Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ec/2de9dd371d52c377abc05d2b725645326c4562fc87296a8907c7bcdf2db7/ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53", size = 55435, upload-time = "2025-08-20T11:55:50.243Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a4/f611f816eac3a581d8a4372f6967c3ed41eddbae4008d1d77f223f1a4e0a/ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752", size = 53193, upload-time = "2025-08-20T11:55:51.373Z" }, - { url = "https://files.pythonhosted.org/packages/e9/c5/c161940967184de96f5cbbbcce45b562a4bf851d60f4c677704b1770136d/ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50", size = 57603, upload-time = "2025-08-20T11:55:52.583Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d6/c7b2444238f5b2e2d0e3dab300b9ddc3606e4b1f0e4bed5a48157cebc792/ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a", size = 59794, upload-time = "2025-08-20T11:55:53.69Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a3/292551f936d3d02d9af148f53e1bc04306b00a7cf1fcbb86fa0d1c887242/ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03", size = 57363, upload-time = "2025-08-20T11:55:54.843Z" }, - { url = "https://files.pythonhosted.org/packages/90/a6/82cfa70448831b1a9e73f882225980b5c689bf539ec6400b31656a60ea46/ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701", size = 1036311, upload-time = "2025-08-20T11:55:56.197Z" }, - { url = "https://files.pythonhosted.org/packages/84/5c/96e2266be50f21e9b27acaee8ca8f23ea0b85cb998c33d4f53147687839b/ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60", size = 1195783, upload-time = "2025-08-20T11:55:58.081Z" }, - { url = "https://files.pythonhosted.org/packages/8d/20/78abe3d808cf3bb3e76f71fca46cd208317bf461c905d79f0d26b9df20f1/ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328", size = 1088822, upload-time = "2025-08-20T11:55:59.469Z" }, - { url = "https://files.pythonhosted.org/packages/d8/50/8856e24bec5e2fc7f775d867aeb7a3f137359356200ac44658f1f2c834b2/ujson-5.11.0-cp313-cp313-win32.whl", hash = "sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241", size = 39753, upload-time = "2025-08-20T11:56:01.345Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/1baee0f4179a4d0f5ce086832147b6cc9b7731c24ca08e14a3fdb8d39c32/ujson-5.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0", size = 43866, upload-time = "2025-08-20T11:56:02.552Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8c/6d85ef5be82c6d66adced3ec5ef23353ed710a11f70b0b6a836878396334/ujson-5.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9", size = 38363, upload-time = "2025-08-20T11:56:03.688Z" }, - { url = "https://files.pythonhosted.org/packages/50/17/30275aa2933430d8c0c4ead951cc4fdb922f575a349aa0b48a6f35449e97/ujson-5.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362", size = 51206, upload-time = "2025-08-20T11:56:48.797Z" }, - { url = "https://files.pythonhosted.org/packages/c3/15/42b3924258eac2551f8f33fa4e35da20a06a53857ccf3d4deb5e5d7c0b6c/ujson-5.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39", size = 48907, upload-time = "2025-08-20T11:56:50.136Z" }, - { url = "https://files.pythonhosted.org/packages/94/7e/0519ff7955aba581d1fe1fb1ca0e452471250455d182f686db5ac9e46119/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc", size = 50319, upload-time = "2025-08-20T11:56:51.63Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/209d90506b7d6c5873f82c5a226d7aad1a1da153364e9ebf61eff0740c33/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844", size = 56584, upload-time = "2025-08-20T11:56:52.89Z" }, - { url = "https://files.pythonhosted.org/packages/e9/97/bd939bb76943cb0e1d2b692d7e68629f51c711ef60425fa5bb6968037ecd/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49", size = 51588, upload-time = "2025-08-20T11:56:54.054Z" }, - { url = "https://files.pythonhosted.org/packages/52/5b/8c5e33228f7f83f05719964db59f3f9f276d272dc43752fa3bbf0df53e7b/ujson-5.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04", size = 43835, upload-time = "2025-08-20T11:56:55.237Z" }, +version = "5.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/3e/c35530c5ffc25b71c59ae0cd7b8f99df37313daa162ce1e2f7925f7c2877/ujson-5.12.0.tar.gz", hash = "sha256:14b2e1eb528d77bc0f4c5bd1a7ebc05e02b5b41beefb7e8567c9675b8b13bcf4", size = 7158451, upload-time = "2026-03-11T22:19:30.397Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/ee/45c7c1f9268b0fecdd68f9ada490bc09632b74f5f90a9be759e51a746ddc/ujson-5.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:38051f36423f084b909aaadb3b41c9c6a2958e86956ba21a8489636911e87504", size = 56145, upload-time = "2026-03-11T22:17:49.409Z" }, + { url = "https://files.pythonhosted.org/packages/6d/dc/ed181dbfb2beee598e91280c6903ba71e10362b051716317e2d3664614bb/ujson-5.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:457fabc2700a8e6ddb85bc5a1d30d3345fe0d3ec3ee8161a4e032ec585801dfa", size = 53839, upload-time = "2026-03-11T22:17:50.973Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d8/eb9ef42c660f431deeedc2e1b09c4ba29aa22818a439ddda7da6ae23ddfa/ujson-5.12.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57930ac9519099b852e190d2c04b1fb5d97ea128db33bce77ed874eccb4c7f09", size = 57844, upload-time = "2026-03-11T22:17:53.029Z" }, + { url = "https://files.pythonhosted.org/packages/68/37/0b586d079d3f2a5be5aa58ab5c423cbb4fae2ee4e65369c87aa74ac7e113/ujson-5.12.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:9b3b86ec3e818f3dd3e13a9de628e88a9990f4af68ecb0b12dd3de81227f0a26", size = 59923, upload-time = "2026-03-11T22:17:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/6a4b69eb397502767f438b5a2b4c066dccc9e3b263115f5ee07510250fc7/ujson-5.12.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:460e76a4daff214ae33ab959494962c93918cb44714ea3e3f748b14aa37f8a87", size = 57427, upload-time = "2026-03-11T22:17:55.317Z" }, + { url = "https://files.pythonhosted.org/packages/bb/4b/ae118440a72e85e68ee8dd26cfc47ea7857954a3341833cde9da7dc40ca3/ujson-5.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e584d0cdd37cac355aca52ed788d1a2d939d6837e2870d3b70e585db24025a50", size = 1037301, upload-time = "2026-03-11T22:17:56.427Z" }, + { url = "https://files.pythonhosted.org/packages/c2/76/834caa7905f65d3a695e4f5ff8d5d4a98508e396a9e8ab0739ab4fe2d422/ujson-5.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0fe9128e75c6aa6e9ae06c1408d6edd9179a2fef0fe6d9cda3166b887eba521d", size = 1196664, upload-time = "2026-03-11T22:17:58.061Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/1f3c1543c1d3f18c54bb3f8c1e74314fd6ad3c1aa375f01433e89a86bfa6/ujson-5.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3ed5cb149892141b1e77ef312924a327f2cc718b34247dae346ed66329e1b8be", size = 1089668, upload-time = "2026-03-11T22:17:59.617Z" }, + { url = "https://files.pythonhosted.org/packages/db/52/07d9da456a78296f61893b9d2bbfb2512f4233394748aae80b8d08c7d96e/ujson-5.12.0-cp310-cp310-win32.whl", hash = "sha256:973b7d7145b1ac553a7466a64afa8b31ec2693d7c7fff6a755059e0a2885dfd2", size = 39644, upload-time = "2026-03-11T22:18:01.212Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e5/c1de3041672fa1ab97aae0f0b9f4e30a9b15d4104c734d5627779206c878/ujson-5.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d072a403d82aef8090c6d4f728e3a727dfdba1ad3b7fa3a052c3ecbd37e73cb", size = 43875, upload-time = "2026-03-11T22:18:02.268Z" }, + { url = "https://files.pythonhosted.org/packages/8b/49/714a9240d9e6bd86c9684a72f100a0005459165fb2b0f6bf1a1156be0b9f/ujson-5.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:55ede2a7a051b3b7e71a394978a098d71b3783e6b904702ff45483fad434ae2d", size = 38563, upload-time = "2026-03-11T22:18:03.546Z" }, + { url = "https://files.pythonhosted.org/packages/10/22/fd22e2f6766bae934d3050517ca47d463016bd8688508d1ecc1baa18a7ad/ujson-5.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58a11cb49482f1a095a2bd9a1d81dd7c8fb5d2357f959ece85db4e46a825fd00", size = 56139, upload-time = "2026-03-11T22:18:04.591Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fd/6839adff4fc0164cbcecafa2857ba08a6eaeedd7e098d6713cb899a91383/ujson-5.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9b3cf13facf6f77c283af0e1713e5e8c47a0fe295af81326cb3cb4380212e797", size = 53836, upload-time = "2026-03-11T22:18:05.662Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b0/0c19faac62d68ceeffa83a08dc3d71b8462cf5064d0e7e0b15ba19898dad/ujson-5.12.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb94245a715b4d6e24689de12772b85329a1f9946cbf6187923a64ecdea39e65", size = 57851, upload-time = "2026-03-11T22:18:06.744Z" }, + { url = "https://files.pythonhosted.org/packages/04/f6/e7fd283788de73b86e99e08256726bb385923249c21dcd306e59d532a1a1/ujson-5.12.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:0fe6b8b8968e11dd9b2348bd508f0f57cf49ab3512064b36bc4117328218718e", size = 59906, upload-time = "2026-03-11T22:18:07.791Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3a/b100735a2b43ee6e8fe4c883768e362f53576f964d4ea841991060aeaf35/ujson-5.12.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89e302abd3749f6d6699691747969a5d85f7c73081d5ed7e2624c7bd9721a2ab", size = 57409, upload-time = "2026-03-11T22:18:08.79Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/f97cc20c99ca304662191b883ae13ae02912ca7244710016ba0cb8a5be34/ujson-5.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0727363b05ab05ee737a28f6200dc4078bce6b0508e10bd8aab507995a15df61", size = 1037339, upload-time = "2026-03-11T22:18:10.424Z" }, + { url = "https://files.pythonhosted.org/packages/10/7a/53ddeda0ffe1420db2f9999897b3cbb920fbcff1849d1f22b196d0f34785/ujson-5.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b62cb9a7501e1f5c9ffe190485501349c33e8862dde4377df774e40b8166871f", size = 1196625, upload-time = "2026-03-11T22:18:11.82Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1a/4c64a6bef522e9baf195dd5be151bc815cd4896c50c6e2489599edcda85f/ujson-5.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6ec5bf6bc361f2f0f9644907a36ce527715b488988a8df534120e5c34eeda94", size = 1089669, upload-time = "2026-03-11T22:18:13.343Z" }, + { url = "https://files.pythonhosted.org/packages/18/11/8ccb109f5777ec0d9fb826695a9e2ac36ae94c1949fc8b1e4d23a5bd067a/ujson-5.12.0-cp311-cp311-win32.whl", hash = "sha256:006428d3813b87477d72d306c40c09f898a41b968e57b15a7d88454ecc42a3fb", size = 39648, upload-time = "2026-03-11T22:18:14.785Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/87fc4c27b20d5125cff7ce52d17ea7698b22b74426da0df238e3efcb0cf2/ujson-5.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:40aa43a7a3a8d2f05e79900858053d697a88a605e3887be178b43acbcd781161", size = 43876, upload-time = "2026-03-11T22:18:15.768Z" }, + { url = "https://files.pythonhosted.org/packages/9e/21/324f0548a8c8c48e3e222eaed15fb6d48c796593002b206b4a28a89e445f/ujson-5.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:561f89cc82deeae82e37d4a4764184926fb432f740a9691563a391b13f7339a4", size = 38553, upload-time = "2026-03-11T22:18:17.251Z" }, + { url = "https://files.pythonhosted.org/packages/84/f6/ac763d2108d28f3a40bb3ae7d2fafab52ca31b36c2908a4ad02cd3ceba2a/ujson-5.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:09b4beff9cc91d445d5818632907b85fb06943b61cb346919ce202668bf6794a", size = 56326, upload-time = "2026-03-11T22:18:18.467Z" }, + { url = "https://files.pythonhosted.org/packages/25/46/d0b3af64dcdc549f9996521c8be6d860ac843a18a190ffc8affeb7259687/ujson-5.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca0c7ce828bb76ab78b3991904b477c2fd0f711d7815c252d1ef28ff9450b052", size = 53910, upload-time = "2026-03-11T22:18:19.502Z" }, + { url = "https://files.pythonhosted.org/packages/9a/10/853c723bcabc3e9825a079019055fc99e71b85c6bae600607a2b9d31d18d/ujson-5.12.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d79c6635ccffcbfc1d5c045874ba36b594589be81d50d43472570bb8de9c57", size = 57754, upload-time = "2026-03-11T22:18:20.874Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c6/6e024830d988f521f144ead641981c1f7a82c17ad1927c22de3242565f5c/ujson-5.12.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:7e07f6f644d2c44d53b7a320a084eef98063651912c1b9449b5f45fcbdc6ccd2", size = 59936, upload-time = "2026-03-11T22:18:21.924Z" }, + { url = "https://files.pythonhosted.org/packages/34/c9/c5f236af5abe06b720b40b88819d00d10182d2247b1664e487b3ed9229cf/ujson-5.12.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:085b6ce182cdd6657481c7c4003a417e0655c4f6e58b76f26ee18f0ae21db827", size = 57463, upload-time = "2026-03-11T22:18:22.924Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/41342d9ef68e793a87d84e4531a150c2b682f3bcedfe59a7a5e3f73e9213/ujson-5.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:16b4fe9c97dc605f5e1887a9e1224287291e35c56cbc379f8aa44b6b7bcfe2bb", size = 1037239, upload-time = "2026-03-11T22:18:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/d4/81/dc2b7617d5812670d4ff4a42f6dd77926430ee52df0dedb2aec7990b2034/ujson-5.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0d2e8db5ade3736a163906154ca686203acc7d1d30736cbf577c730d13653d84", size = 1196713, upload-time = "2026-03-11T22:18:25.391Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9c/80acff0504f92459ed69e80a176286e32ca0147ac6a8252cd0659aad3227/ujson-5.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93bc91fdadcf046da37a214eaa714574e7e9b1913568e93bb09527b2ceb7f759", size = 1089742, upload-time = "2026-03-11T22:18:26.738Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f0/123ffaac17e45ef2b915e3e3303f8f4ea78bb8d42afad828844e08622b1e/ujson-5.12.0-cp312-cp312-win32.whl", hash = "sha256:2a248750abce1c76fbd11b2e1d88b95401e72819295c3b851ec73399d6849b3d", size = 39773, upload-time = "2026-03-11T22:18:28.244Z" }, + { url = "https://files.pythonhosted.org/packages/b5/20/f3bd2b069c242c2b22a69e033bfe224d1d15d3649e6cd7cc7085bb1412ff/ujson-5.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:1b5c6ceb65fecd28a1d20d1eba9dbfa992612b86594e4b6d47bb580d2dd6bcb3", size = 44040, upload-time = "2026-03-11T22:18:29.236Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/01b5a0bcded14cd2522b218f2edc3533b0fcbccdea01f3e14a2b699071aa/ujson-5.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:9a5fcbe7b949f2e95c47ea8a80b410fcdf2da61c98553b45a4ee875580418b68", size = 38526, upload-time = "2026-03-11T22:18:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f1/0ef0eeab1db8493e1833c8b440fe32cf7538f7afa6e7f7c7e9f62cef464d/ujson-5.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:15d416440148f3e56b9b244fdaf8a09fcf5a72e4944b8e119f5bf60417a2bfc8", size = 56331, upload-time = "2026-03-11T22:18:31.539Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2f/9159f6f399b3f572d20847a2b80d133e3a03c14712b0da4971a36879fb64/ujson-5.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0dd3676ea0837cd70ea1879765e9e9f6be063be0436de9b3ea4b775caf83654", size = 53910, upload-time = "2026-03-11T22:18:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a9/f96376818d71495d1a4be19a0ab6acf0cc01dd8826553734c3d4dac685b2/ujson-5.12.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bbf05c38debc90d1a195b11340cc85cb43ab3e753dc47558a3a84a38cbc72da", size = 57757, upload-time = "2026-03-11T22:18:33.866Z" }, + { url = "https://files.pythonhosted.org/packages/98/8d/dd4a151caac6fdcb77f024fbe7f09d465ebf347a628ed6dd581a0a7f6364/ujson-5.12.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:3c2f947e55d3c7cfe124dd4521ee481516f3007d13c6ad4bf6aeb722e190eb1b", size = 59940, upload-time = "2026-03-11T22:18:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/17/0d36c2fee0a8d8dc37b011ccd5bbdcfaff8b8ec2bcfc5be998661cdc935b/ujson-5.12.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ea6206043385343aff0b7da65cf73677f6f5e50de8f1c879e557f4298cac36a", size = 57465, upload-time = "2026-03-11T22:18:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/8c/04/b0ee4a4b643a01ba398441da1e357480595edb37c6c94c508dbe0eb9eb60/ujson-5.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb349dbba57c76eec25e5917e07f35aabaf0a33b9e67fc13d188002500106487", size = 1037236, upload-time = "2026-03-11T22:18:37.743Z" }, + { url = "https://files.pythonhosted.org/packages/2d/08/0e7780d0bbb48fe57ded91f550144bcc99c03b5360bf2886dd0dae0ea8f5/ujson-5.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:937794042342006f707837f38d721426b11b0774d327a2a45c0bd389eb750a87", size = 1196717, upload-time = "2026-03-11T22:18:39.101Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/e0e34107715bb4dd2d4dcc1ce244d2f074638837adf38aff85a37506efe4/ujson-5.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ad57654570464eb1b040b5c353dee442608e06cff9102b8fcb105565a44c9ed", size = 1089748, upload-time = "2026-03-11T22:18:40.473Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/814f4e2b5374d0d505c254ba4bed43eb25d2d046f19f5fd88555f81a7bd0/ujson-5.12.0-cp313-cp313-win32.whl", hash = "sha256:76bf3e7406cf23a3e1ca6a23fb1fb9ea82f4f6bd226fe226e09146b0194f85dc", size = 39778, upload-time = "2026-03-11T22:18:41.791Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/19310d848ebe93315b6cb171277e4ce29f47ef9d46caabd63ff05d5be548/ujson-5.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:15e555c4caca42411270b2ed2b2ebc7b3a42bb04138cef6c956e1f1d49709fe2", size = 44038, upload-time = "2026-03-11T22:18:43.094Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e4/7a39103d7634691601a02bd1ca7268fba4da47ed586365e6ee68168f575a/ujson-5.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bd03472c36fa3a386a6deb887113b9e3fa40efba8203eb4fe786d3c0ccc724f6", size = 38529, upload-time = "2026-03-11T22:18:44.167Z" }, + { url = "https://files.pythonhosted.org/packages/95/3c/5ee154d505d1aad2debc4ba38b1a60ae1949b26cdb5fa070e85e320d6b64/ujson-5.12.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:bf85a00ac3b56a1e7a19c5be7b02b5180a0895ac4d3c234d717a55e86960691c", size = 54494, upload-time = "2026-03-11T22:19:13.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b3/9496ec399ec921e434a93b340bd5052999030b7ac364be4cbe5365ac6b20/ujson-5.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:64df53eef4ac857eb5816a56e2885ccf0d7dff6333c94065c93b39c51063e01d", size = 57999, upload-time = "2026-03-11T22:19:14.385Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/e9ae98133336e7c0d50b43626c3f2327937cecfa354d844e02ac17379ed1/ujson-5.12.0-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c0aed6a4439994c9666fb8a5b6c4eac94d4ef6ddc95f9b806a599ef83547e3b", size = 54518, upload-time = "2026-03-11T22:19:15.4Z" }, + { url = "https://files.pythonhosted.org/packages/58/10/978d89dded6bb1558cd46ba78f4351198bd2346db8a8ee1a94119022ce40/ujson-5.12.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efae5df7a8cc8bdb1037b0f786b044ce281081441df5418c3a0f0e1f86fe7bb3", size = 55736, upload-time = "2026-03-11T22:19:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/1df8e6217c92e57a1266bf5be750b1dddc126ee96e53fe959d5693503bc6/ujson-5.12.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:8712b61eb1b74a4478cfd1c54f576056199e9f093659334aeb5c4a6b385338e5", size = 44615, upload-time = "2026-03-11T22:19:17.53Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/f4a957dddb99bd68c8be91928c0b6fefa7aa8aafc92c93f5d1e8b32f6702/ujson-5.12.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:871c0e5102e47995b0e37e8df7819a894a6c3da0d097545cd1f9f1f7d7079927", size = 52145, upload-time = "2026-03-11T22:19:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/55/6e/50b5cf612de1ca06c7effdc5a5d7e815774dee85a5858f1882c425553b82/ujson-5.12.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:56ba3f7abbd6b0bb282a544dc38406d1a188d8bb9164f49fdb9c2fee62cb29da", size = 49577, upload-time = "2026-03-11T22:19:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/6e/24/b6713fa9897774502cd4c2d6955bb4933349f7d84c3aa805531c382a4209/ujson-5.12.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c5a52987a990eb1bae55f9000994f1afdb0326c154fb089992f839ab3c30688", size = 50807, upload-time = "2026-03-11T22:19:20.778Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/c0e0f7901180ef80d16f3a4bccb5dc8b01515a717336a62928963a07b80b/ujson-5.12.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:adf28d13a33f9d750fe7a78fb481cac298fa257d8863d8727b2ea4455ea41235", size = 56972, upload-time = "2026-03-11T22:19:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/02/a9/05d91b4295ea7239151eb08cf240e5a2ba969012fda50bc27bcb1ea9cd71/ujson-5.12.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51acc750ec7a2df786cdc868fb16fa04abd6269a01d58cf59bafc57978773d8e", size = 52045, upload-time = "2026-03-11T22:19:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/92047d32bf6f2d9db64605fc32e8eb0e0dd68b671eaafc12a464f69c4af4/ujson-5.12.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ab9056d94e5db513d9313b34394f3a3b83e6301a581c28ad67773434f3faccab", size = 44053, upload-time = "2026-03-11T22:19:23.918Z" }, ] [[package]] @@ -14757,7 +14751,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.1.0" +version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -14766,9 +14760,9 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/c9/18d4b36606d6091844daa3bd93cf7dc78e6f5da21d9f21d06c221104b684/virtualenv-21.1.0.tar.gz", hash = "sha256:1990a0188c8f16b6b9cf65c9183049007375b26aad415514d377ccacf1e4fb44", size = 5840471, upload-time = "2026-02-27T08:49:29.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/55/896b06bf93a49bec0f4ae2a6f1ed12bd05c8860744ac3a70eda041064e4d/virtualenv-21.1.0-py3-none-any.whl", hash = "sha256:164f5e14c5587d170cf98e60378eb91ea35bf037be313811905d3a24ea33cc07", size = 5825072, upload-time = "2026-02-27T08:49:27.516Z" }, + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] [[package]] @@ -14803,7 +14797,7 @@ wheels = [ [package.optional-dependencies] all = [ { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "openai" }, { name = "pypdfium2" }, { name = "rich" }, @@ -14932,7 +14926,7 @@ wheels = [ [[package]] name = "weaviate-client" -version = "4.20.3" +version = "4.20.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, @@ -14943,9 +14937,9 @@ dependencies = [ { name = "pydantic" }, { name = "validators" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/86/42a588b0acb490988804d6ab732368b96eaae692b25c518b763aeee9462b/weaviate_client-4.20.3.tar.gz", hash = "sha256:0d9eaff8ec556af1b0c10a245ed08e07c8656ce7f224c65b1e76c0e6635191f3", size = 809078, upload-time = "2026-03-05T09:22:41.176Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/1c/82b560254f612f95b644849d86e092da6407f17965d61e22b583b30b72cf/weaviate_client-4.20.4.tar.gz", hash = "sha256:08703234b59e4e03739f39e740e9e88cb50cd0aa147d9408b88ea6ce995c37b6", size = 809529, upload-time = "2026-03-10T15:08:13.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/f6/25ada0555a368286278d4ce7cedfd83896e5353a8cf582612d302659a328/weaviate_client-4.20.3-py3-none-any.whl", hash = "sha256:5998701ef9c7f025c8d034bf5f3048db47d55ad304bb073d5fdce0bd4198bc1b", size = 619412, upload-time = "2026-03-05T09:22:39.847Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d7/9461c3e7d8c44080d2307078e33dc7fefefa3171c8f930f2b83a5cbf67f2/weaviate_client-4.20.4-py3-none-any.whl", hash = "sha256:7af3a213bebcb30dcf456b0db8b6225d8926106b835d7b883276de9dc1c301fe", size = 619517, upload-time = "2026-03-10T15:08:12.047Z" }, ] [[package]] @@ -15372,7 +15366,7 @@ dependencies = [ { name = "lxml" }, { name = "multitasking" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pandas" }, { name = "peewee" }, { name = "platformdirs" }, From 1dafc75b3032215066cb058cc267d68fe0e04baa Mon Sep 17 00:00:00 2001 From: Mendon Kissling <59585235+mendonk@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:04:35 -0400 Subject: [PATCH 19/29] fix: make LANGFUSE_BASE_URL the preferred URL variable (#12154) * fall-back-to-langfuse-base-url * baseurl-first-and-docs * Apply suggestions from code review Co-authored-by: April I. Murphy <36110273+aimurphy@users.noreply.github.com> * set-in-docker-compose --------- Co-authored-by: April I. Murphy <36110273+aimurphy@users.noreply.github.com> --- docs/docs/Develop/integrations-langfuse.mdx | 69 ++++++++----------- .../langflow/services/tracing/langfuse.py | 2 +- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/docs/docs/Develop/integrations-langfuse.mdx b/docs/docs/Develop/integrations-langfuse.mdx index 74c634c4af94..2a4e3f52ce56 100644 --- a/docs/docs/Develop/integrations-langfuse.mdx +++ b/docs/docs/Develop/integrations-langfuse.mdx @@ -27,52 +27,37 @@ If you need a flow to test the Langfuse integration, see the [Langflow quickstar 2. Copy the following API key information: - - Secret Key - - Public Key - - Host URL + - Secret key + - Public key + - Base URL -3. Set your Langfuse project credentials as environment variables in the same environment where you run Langflow. + :::tip + Langflow previously used `LANGFUSE_HOST` as the variable for the Langfuse base URL. + This is still supported for backward compatibility, but `LANGFUSE_BASE_URL` is now the preferred environment variable and will be used if both values are set. + ::: - In the following examples, replace `SECRET_KEY`, `PUBLIC_KEY`, and `HOST_URL` with your API key details from Langfuse. +3. Set your Langfuse project credentials as environment variables. - - + In the following examples, replace `SECRET_KEY`, `PUBLIC_KEY`, and `LANGFUSE_BASE_URL` with your API key details from Langfuse. + Add the following entries to your `.env` file: - These commands set the environment variables in a Linux or macOS terminal session: - - ``` - export LANGFUSE_SECRET_KEY=SECRET_KEY - export LANGFUSE_PUBLIC_KEY=PUBLIC_KEY - export LANGFUSE_HOST=HOST_URL - ``` - - - - - These commands set the environment variables in a Windows command prompt session: - - ``` - set LANGFUSE_SECRET_KEY=SECRET_KEY - set LANGFUSE_PUBLIC_KEY=PUBLIC_KEY - set LANGFUSE_HOST=HOST_URL + ```bash + LANGFUSE_SECRET_KEY=sk-... + LANGFUSE_PUBLIC_KEY=pk-... + LANGFUSE_BASE_URL=https://us.cloud.langfuse.com ``` - - - -## Start Langflow and view traces in Langfuse - -1. Start Langflow in the same environment where you set the Langfuse environment variables: +4. Start Langflow with the configuration in the `.env` file: ```bash - uv run langflow run + uv run langflow run --env-file .env ``` -2. Run a flow. +5. Run a flow. Langflow automatically collects and sends tracing data about the flow execution to Langfuse. -3. View the collected data in your [Langfuse dashboard](https://langfuse.com/docs/analytics/overview). +6. View the collected data in your [Langfuse dashboard](https://langfuse.com/docs/analytics/overview). Langfuse also provides a [public live trace example dashboard](https://cloud.langfuse.com/project/cm0nywmaa005c3ol2msoisiho/traces/f016ae6d-4527-43f5-93ba-9d78388cd3d9). @@ -88,9 +73,15 @@ As an alternative to the previous setup, particularly for self-hosted Langfuse, 2. Copy the following API key information: - - Secret Key - - Public Key - - Host URL + - Secret key + - Public key + - Base URL + + :::tip + Langflow previously used `LANGFUSE_HOST` as the variable for the Langfuse base URL. + `LANGFUSE_HOST` is still supported for backward compatibility, but `LANGFUSE_BASE_URL` is the preferred environment variable. + If both values are set, then `LANGFLOW_BASE_URL` is used. + ::: 3. Add your Langflow credentials to your Langflow `docker-compose.yml` file in the `environment` section. @@ -111,7 +102,7 @@ As an alternative to the previous setup, particularly for self-hosted Langfuse, - LANGFLOW_CONFIG_DIR=app/langflow - LANGFUSE_SECRET_KEY=sk-... - LANGFUSE_PUBLIC_KEY=pk-... - - LANGFUSE_HOST=https://us.cloud.langfuse.com + - LANGFUSE_BASE_URL=https://us.cloud.langfuse.com volumes: - langflow-data:/app/langflow @@ -140,10 +131,10 @@ As an alternative to the previous setup, particularly for self-hosted Langfuse, 5. To confirm Langfuse is connected to your Langflow container, run the following command: ```sh - docker compose exec langflow python -c "import requests, os; addr = os.environ.get('LANGFUSE_HOST'); print(addr); res = requests.get(addr, timeout=5); print(res.status_code)" + docker compose exec langflow python -c "import requests, os; addr = os.environ.get('LANGFUSE_BASE_URL'); print(addr); res = requests.get(addr, timeout=5); print(res.status_code)" ``` - If there is an error, make sure you have set the `LANGFUSE_HOST` environment variable in your terminal session. + If there is an error, make sure you have set the `LANGFUSE_BASE_URL` environment variable in your `docker-compose.yml` file. Output similar to the following indicates success: diff --git a/src/backend/base/langflow/services/tracing/langfuse.py b/src/backend/base/langflow/services/tracing/langfuse.py index c606b5fbe505..47405320f458 100644 --- a/src/backend/base/langflow/services/tracing/langfuse.py +++ b/src/backend/base/langflow/services/tracing/langfuse.py @@ -164,7 +164,7 @@ def get_langchain_callback(self) -> BaseCallbackHandler | None: def _get_config() -> dict: secret_key = os.getenv("LANGFUSE_SECRET_KEY", None) public_key = os.getenv("LANGFUSE_PUBLIC_KEY", None) - host = os.getenv("LANGFUSE_HOST", None) + host = os.getenv("LANGFUSE_BASE_URL") or os.getenv("LANGFUSE_HOST") if secret_key and public_key and host: return {"secret_key": secret_key, "public_key": public_key, "host": host} return {} From 67d56947e845084dd24b3f5ca0565df0b054258f Mon Sep 17 00:00:00 2001 From: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com> Date: Mon, 16 Mar 2026 06:42:08 -0400 Subject: [PATCH 20/29] chore: remove hash history (#12183) * Remove hash history Custom component checks will be done directly through the component index. Removing hash history as it no longer fits into any planned functionality. * [autofix.ci] apply automated fixes * baseline format fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .github/workflows/nightly_build.yml | 252 +-- .secrets.baseline | 1422 +---------------- Makefile | 1 - scripts/build_hash_history.py | 200 --- .../base/langflow/initial_setup/setup.py | 5 - .../tests/unit/test_build_hash_history.py | 119 -- src/backend/tests/unit/test_initial_setup.py | 184 --- .../test_starter_projects_no_hash_history.py | 87 - src/lfx/src/lfx/base/models/unified_models.py | 89 +- 9 files changed, 73 insertions(+), 2286 deletions(-) delete mode 100755 scripts/build_hash_history.py delete mode 100644 src/backend/tests/unit/test_build_hash_history.py delete mode 100644 src/backend/tests/unit/test_starter_projects_no_hash_history.py diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index b5b16c517845..adba3027b100 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -32,11 +32,6 @@ on: required: false type: boolean default: true - auto_merge_hash_history: - description: "Create PR to merge hash history back to main. Set to false when not running from main." - required: false - type: boolean - default: true schedule: # Run job at 00:00 UTC (4:00 PM PST / 5:00 PM PDT) - cron: "0 0 * * *" @@ -181,90 +176,6 @@ jobs: exit 1 fi - build-hash-history: - if: github.repository == 'langflow-ai/langflow' - name: Build Nightly Hash History - needs: create-nightly-tag - runs-on: ubuntu-latest - defaults: - run: - shell: bash -ex -o pipefail {0} - permissions: - contents: write - steps: - - name: Checkout nightly tag - uses: actions/checkout@v6 - with: - ref: ${{ needs.create-nightly-tag.outputs.main_tag }} - persist-credentials: true - - - name: "Setup Environment" - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - python-version: ${{ env.PYTHON_VERSION }} - prune-cache: false - - - name: Install the project - run: uv sync - - - name: Force reinstall LFX to pick up version change - run: | - echo "Force reinstalling lfx-nightly to ensure version metadata is updated..." - uv pip install --reinstall --no-deps src/lfx - - - name: Verify LFX version - run: | - echo "Checking installed LFX version..." - uv run python -c "from importlib.metadata import version; print(f'lfx-nightly version: {version(\"lfx-nightly\")}')" || \ - uv run python -c "from importlib.metadata import version; print(f'lfx version: {version(\"lfx\")}')" - - - name: Build and validate nightly hash history - run: | - # The script includes append-only validation - uv run python scripts/build_hash_history.py --nightly - - - name: Commit and push hash history changes - run: | - echo "=== Configuring git ===" - git config --global user.email "bot-nightly-builds@langflow.org" - git config --global user.name "Langflow Bot" - - echo "=== Checking for hash history changes ===" - # Check if there are changes to commit (handles if it's a new file for first run) - git add src/lfx/src/lfx/_assets/nightly_hash_history.json - if git diff --cached --quiet src/lfx/src/lfx/_assets/nightly_hash_history.json; then - echo "No changes to nightly hash history" - else - echo "Hash history changes detected, committing..." - # Commit to the nightly tag itself so builds include the hash history - git commit -m "Update nightly hash history for ${{ needs.create-nightly-tag.outputs.main_tag }}" - - echo "=== Updating nightly tag ${{ needs.create-nightly-tag.outputs.main_tag }} ===" - # Update the tag to include the hash history - git tag -f ${{ needs.create-nightly-tag.outputs.main_tag }} - git push -f origin ${{ needs.create-nightly-tag.outputs.main_tag }} - - echo "=== Creating clean branch from main with only hash history changes ===" - # Fetch latest main - git fetch origin main - - # Create a new branch from main - git checkout -B nightly-hash-history-updates origin/main - - # Copy ONLY the hash history file from the nightly tag - git checkout ${{ needs.create-nightly-tag.outputs.main_tag }} -- src/lfx/src/lfx/_assets/nightly_hash_history.json - - # Commit only this file - git add src/lfx/src/lfx/_assets/nightly_hash_history.json - git commit -m "Update nightly hash history for ${{ needs.create-nightly-tag.outputs.main_tag }}" - - # Force push the clean branch - git push origin nightly-hash-history-updates --force - echo "Updated nightly tag and created clean branch with only hash history changes" - fi - frontend-tests: if: github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests name: Run Frontend Tests @@ -304,9 +215,9 @@ jobs: # ref: ${{ needs.create-nightly-tag.outputs.tag }} release-nightly-build: - if: github.repository == 'langflow-ai/langflow' && always() && needs.frontend-tests.result != 'failure' && needs.backend-unit-tests.result != 'failure' && needs.build-hash-history.result != 'failure' + if: github.repository == 'langflow-ai/langflow' && always() && needs.frontend-tests.result != 'failure' && needs.backend-unit-tests.result != 'failure' name: Run Nightly Langflow Build - needs: [create-nightly-tag, frontend-tests, backend-unit-tests, build-hash-history] + needs: [create-nightly-tag, frontend-tests, backend-unit-tests] uses: ./.github/workflows/release_nightly.yml with: build_docker_base: true @@ -323,12 +234,12 @@ jobs: slack-notification: name: Send Slack Notification - needs: [frontend-tests, backend-unit-tests, release-nightly-build, merge-hash-history-to-main] - if: ${{ github.repository == 'langflow-ai/langflow' && !inputs.skip_slack && always() && (needs.release-nightly-build.result == 'failure' || needs.frontend-tests.result == 'failure' || needs.backend-unit-tests.result == 'failure' || needs.merge-hash-history-to-main.result == 'failure' || needs.release-nightly-build.result == 'success') }} + needs: [frontend-tests, backend-unit-tests, release-nightly-build] + if: ${{ github.repository == 'langflow-ai/langflow' && !inputs.skip_slack && always() && (needs.release-nightly-build.result == 'failure' || needs.frontend-tests.result == 'failure' || needs.backend-unit-tests.result == 'failure' || needs.release-nightly-build.result == 'success') }} runs-on: ubuntu-latest steps: - name: Send failure notification to Slack - if: ${{ needs.release-nightly-build.result == 'failure' || needs.frontend-tests.result == 'failure' || needs.backend-unit-tests.result == 'failure' || needs.merge-hash-history-to-main.result == 'failure' }} + if: ${{ needs.release-nightly-build.result == 'failure' || needs.frontend-tests.result == 'failure' || needs.backend-unit-tests.result == 'failure' }} run: | # Determine which job failed FAILED_JOB="unknown" @@ -338,8 +249,6 @@ jobs: FAILED_JOB="frontend-tests" elif [ "${{ needs.backend-unit-tests.result }}" == "failure" ]; then FAILED_JOB="backend-unit-tests" - elif [ "${{ needs.merge-hash-history-to-main.result }}" == "failure" ]; then - FAILED_JOB="merge-hash-history-to-main" fi curl -X POST -H 'Content-type: application/json' \ @@ -377,157 +286,6 @@ jobs: ] }" ${{ secrets.LANGFLOW_ENG_SLACK_WEBHOOK_URL }} - merge-hash-history-to-main: - name: Create PR to Merge Hash History to Main - needs: [create-nightly-tag, build-hash-history, release-nightly-build] - # Run if auto_merge_hash_history is true (default) or not set (scheduled runs) - # When triggered by schedule, inputs.auto_merge_hash_history is not set, so default to true - # Only run when the base branch is 'main' to prevent PRs from feature branches - if: github.repository == 'langflow-ai/langflow' && github.ref == 'refs/heads/main' && always() && needs.build-hash-history.result == 'success' && needs.release-nightly-build.result == 'success' && (github.event_name == 'schedule' || inputs.auto_merge_hash_history != false) - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout main branch - uses: actions/checkout@v6 - with: - ref: main - fetch-depth: 0 - persist-credentials: true - - - name: Configure Git - run: | - git config --global user.email "bot-nightly-builds@langflow.org" - git config --global user.name "Langflow Bot" - - - name: "Setup Environment" - uses: astral-sh/setup-uv@v6 - with: - enable-cache: true - cache-dependency-glob: "uv.lock" - python-version: ${{ env.PYTHON_VERSION }} - prune-cache: false - - - name: Fetch nightly hash history branch - run: | - echo "=== Fetching nightly-hash-history-updates branch ===" - git fetch origin nightly-hash-history-updates || true - echo "Fetch completed" - - - name: Check if branch exists and has changes - id: check_branch - run: | - echo "=== Checking branch status ===" - if git rev-parse --verify origin/nightly-hash-history-updates >/dev/null 2>&1; then - echo "branch_exists=true" >> $GITHUB_OUTPUT - echo "Branch exists" - - # Check if there are differences between main and the nightly branch - echo "=== Comparing with main branch ===" - if git diff --quiet main origin/nightly-hash-history-updates -- src/lfx/src/lfx/_assets/nightly_hash_history.json; then - echo "has_changes=false" >> $GITHUB_OUTPUT - echo "No changes to merge" - else - echo "has_changes=true" >> $GITHUB_OUTPUT - echo "Changes detected - PR will be created/updated" - fi - else - echo "branch_exists=false" >> $GITHUB_OUTPUT - echo "has_changes=false" >> $GITHUB_OUTPUT - echo "ERROR: Branch nightly-hash-history-updates does not exist!" - echo "This should have been created by the build-hash-history job." - exit 1 - fi - - - name: Validate and prepare PR - if: steps.check_branch.outputs.branch_exists == 'true' && steps.check_branch.outputs.has_changes == 'true' - run: | - echo "=== Checking out hash history file from nightly branch ===" - # Checkout the nightly hash history file to verify it - git checkout origin/nightly-hash-history-updates -- src/lfx/src/lfx/_assets/nightly_hash_history.json - - echo "=== Verifying only hash history file was modified ===" - # Verify that ONLY the nightly_hash_history.json file was modified - CHANGED_FILES=$(git diff --name-only main) - if [ "$CHANGED_FILES" != "src/lfx/src/lfx/_assets/nightly_hash_history.json" ]; then - echo "ERROR: Unexpected files were modified: $CHANGED_FILES" - echo "Only nightly_hash_history.json should be changed" - exit 1 - fi - echo "Only hash history file modified" - - echo "=== Validating hash history file ===" - # Basic validation - ensure file exists and is valid JSON - - if [ ! -f "src/lfx/src/lfx/_assets/nightly_hash_history.json" ]; then - echo "ERROR: nightly_hash_history.json file was deleted!" - exit 1 - fi - - if ! uv run python -c "import json; json.load(open('src/lfx/src/lfx/_assets/nightly_hash_history.json'))"; then - echo "ERROR: nightly_hash_history.json is not valid JSON!" - exit 1 - fi - - echo "Hash history file validation passed" - echo "Note: Append-only validation was performed during the build step" - - - name: Create Pull Request - if: steps.check_branch.outputs.branch_exists == 'true' && steps.check_branch.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_BODY: >- - This PR updates the nightly hash history file with the latest component hashes from the nightly build. - - - **Automated PR** - Generated by nightly build workflow - - - - Updates: `src/lfx/src/lfx/_assets/nightly_hash_history.json` - - - Source: Nightly build tag ${{ needs.create-nightly-tag.outputs.main_tag || 'latest nightly' }} - - - This PR is automatically updated each night with the latest hash history. - - Auto-merge is enabled - PR will merge automatically when checks pass. - run: | - echo "=== Creating or updating PR ===" - - # Check if PR already exists - EXISTING_PR=$(gh pr list --head nightly-hash-history-updates --base main --json number --jq '.[0].number' 2>/dev/null || echo "") - - if [ -n "$EXISTING_PR" ]; then - echo "PR #$EXISTING_PR already exists, updating..." - gh pr edit "$EXISTING_PR" \ - --title "chore: update nightly hash history" \ - --body "$PR_BODY" - - echo "PR updated successfully" - PR_NUMBER="$EXISTING_PR" - else - echo "Creating new PR..." - PR_URL=$(gh pr create \ - --title "chore: update nightly hash history" \ - --body "$PR_BODY" \ - --base main \ - --head nightly-hash-history-updates) - - echo "PR created: $PR_URL" - # Extract PR number from URL (works with both formats) - PR_NUMBER=$(echo "$PR_URL" | sed 's/.*\/pull\///' | sed 's/[^0-9].*//') - fi - - # Enable auto-merge on the PR - echo "=== Enabling auto-merge ===" - if gh pr merge "$PR_NUMBER" --auto --squash; then - echo "Auto-merge enabled successfully" - else - echo "ERROR: Could not enable auto-merge on PR" - # TODO: Fail the job here (once you've got everything working on happy path) - fi - - name: Send success notification to Slack if: ${{ !inputs.skip_slack && needs.release-nightly-build.result == 'success' }} run: | diff --git a/.secrets.baseline b/.secrets.baseline index f75b4573d541..16a945791484 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -5243,1424 +5243,6 @@ "is_secret": false } ], - "src/lfx/src/lfx/_assets/stable_hash_history.json": [ - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "5717a1ee406aa657a2dacc80e2816c8f7dcae7e2", - "is_verified": false, - "line_number": 16, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "d43f7dd3e51ce7cb8b9f3c26531a9e4c3a685785", - "is_verified": false, - "line_number": 34, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "1be2449adf6092e0729be455a98c93034cc90bc8", - "is_verified": false, - "line_number": 58, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "42a810efde880424b1aec6d80360d8befa6c6521", - "is_verified": false, - "line_number": 70, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "7014798bb60656a38da4a856545a06c773976112", - "is_verified": false, - "line_number": 94, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a45df4ec5e76a1eb1199091a12fa8ee5e7af12a8", - "is_verified": false, - "line_number": 100, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "b664327352fbd206a6ab38a8903fcabf1b1036a9", - "is_verified": false, - "line_number": 106, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "59d43c509612f89c187f862266890ae0dd5fbb9a", - "is_verified": false, - "line_number": 112, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "c2258af5c2c23419d7469b26f77c954af427b4b8", - "is_verified": false, - "line_number": 172, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "597868714ac401a26b57be0f857457eeb984be18", - "is_verified": false, - "line_number": 184, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a178830480afc434270a7a53512d97758ec6d139", - "is_verified": false, - "line_number": 190, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "6c7724fbb114bfc616ee7bbbb3214e58907abaf1", - "is_verified": false, - "line_number": 196, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "794ae8fea8a51838b63423486552f5398a47e6fc", - "is_verified": false, - "line_number": 202, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "97e68220b094141268772b8b601fa6cd7432de92", - "is_verified": false, - "line_number": 208, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a5af47522dc8a08746c380da81917bdd6eda057a", - "is_verified": false, - "line_number": 220, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "9f66cbc518bb79dc6f0a78af0aa52bbadefe2399", - "is_verified": false, - "line_number": 226, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "b3c2f9fda15f2d3816c7edc667bb24267be41a58", - "is_verified": false, - "line_number": 232, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "72be8a21dd766c795332576419e6864eddc5db4e", - "is_verified": false, - "line_number": 238, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "1659f95bebec345a9e20e32fa71e8eac4f32f6a2", - "is_verified": false, - "line_number": 268, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "15e5f792860e53987a756bed19fba1204a671e19", - "is_verified": false, - "line_number": 274, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "91700b2378ff5d682d1d57cff40818586609015d", - "is_verified": false, - "line_number": 286, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "4b9838e8ff9ae89c3d23d3c853e0d07935618f00", - "is_verified": false, - "line_number": 304, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "1aa0d90add98cf00965a327eed79bf65d589e3ce", - "is_verified": false, - "line_number": 310, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "3698dc86868353e8ff5ed4564f78d45f1e6c08b7", - "is_verified": false, - "line_number": 316, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "def35d315dd1ab5b0b4a05fc66847f6b73d0d853", - "is_verified": false, - "line_number": 352, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "932fd84fba062a90506c3086945b53d4a6a3f169", - "is_verified": false, - "line_number": 364, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "d1a66c6f4de1b56cc6e24cb0a9c78f5ba0230f56", - "is_verified": false, - "line_number": 370, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "ddd35c43ce79e9b7ffc5f2894a1a92ad4da3297d", - "is_verified": false, - "line_number": 376, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "bfa2c52c96d82a086f93287e90c3c889e292989e", - "is_verified": false, - "line_number": 382, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "ac40271e91c0d84c26bf3613a94545872a801998", - "is_verified": false, - "line_number": 412, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "691ee8aa156c92e8ae67859d9463020d1d5bec11", - "is_verified": false, - "line_number": 436, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f0e0ec0ff365d37b4fe860d63a9625ae529d3079", - "is_verified": false, - "line_number": 442, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "5c33c0e3b39aa99ab095bf885b5f0688a9332b95", - "is_verified": false, - "line_number": 448, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "7bfbc3a0161bb7553a4e14c1eb459d30cf104fdf", - "is_verified": false, - "line_number": 460, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "da7592fd328658e5e783f4d16c62d1d6f9d3acd4", - "is_verified": false, - "line_number": 466, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "23ce66526235ae0035cd8da3920a63c12c1c137a", - "is_verified": false, - "line_number": 478, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a75703e0eb9d3a13d977bf04fa3cc42e9d3c94a2", - "is_verified": false, - "line_number": 508, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "2efc38920659af83e871e71004839171d3eaeba4", - "is_verified": false, - "line_number": 526, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "4f514a159d49488561a2efe8585871ce25141548", - "is_verified": false, - "line_number": 532, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "adb1d675969fb13f1d752232026b9872475aca4b", - "is_verified": false, - "line_number": 562, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "99b6e13d3c63e4f323776aec40dda0551bc0aa56", - "is_verified": false, - "line_number": 568, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "914bd29a063d63f5cda65b9193612041bf1b04e9", - "is_verified": false, - "line_number": 592, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "dca20b45dc15f99f985e0f87aacf5569b014ede8", - "is_verified": false, - "line_number": 598, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "9d48b00c8700d1dcab9108609465af7112840243", - "is_verified": false, - "line_number": 604, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "e72cb4e0e589831cbbd71514f5b6db7f0d09fd37", - "is_verified": false, - "line_number": 628, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "03546202d2aee0b0998d1518625a6b271c345de1", - "is_verified": false, - "line_number": 634, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "753c0fdfc1e518b8c44cd464fb28080f3f94a9f4", - "is_verified": false, - "line_number": 640, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "ab9b46808af9e1164b7a21d946a2cefcbfa9b769", - "is_verified": false, - "line_number": 652, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f4a6791157ee757125b9f46c2cf72ea19cdfb50e", - "is_verified": false, - "line_number": 664, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "23a1f3524f7b992e6a225072ec63fc780f21da34", - "is_verified": false, - "line_number": 676, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "85080cbcb6a89304476c8a35d0c1e522afc56c47", - "is_verified": false, - "line_number": 694, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "3179ea06ef24aee254dce7a4a3d7a02bcc6cb77f", - "is_verified": false, - "line_number": 700, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "6ea8490b9c5872990ccc69e5d54fe850c28796b0", - "is_verified": false, - "line_number": 706, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "9a96eb0a8598688b358bdb4b37cdd0019f9934c7", - "is_verified": false, - "line_number": 712, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f846d79058594083280ddae8a1dbce083aaf6427", - "is_verified": false, - "line_number": 724, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "fb0e32db4013340e8e096da4d7cba00c099d9542", - "is_verified": false, - "line_number": 730, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "cc008700c5e02d5c9a7ca24219677922a3f82f17", - "is_verified": false, - "line_number": 748, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "7863a3a0eb2ed4e19329374549df3cef1ab7ed16", - "is_verified": false, - "line_number": 760, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "41da17b522aa582bfb292d52e8dd307bada14400", - "is_verified": false, - "line_number": 766, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "3632913dea26578a835e7c77ab7f4293d6ec1fe6", - "is_verified": false, - "line_number": 772, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "d33546b1bd9d0542435f0f0946a6231edc175701", - "is_verified": false, - "line_number": 796, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "0321ad34ab13e2dee03faa30b7645b932f24c4d6", - "is_verified": false, - "line_number": 820, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "cb2623c527dbce4b4e4ac56407979cad7149ea9a", - "is_verified": false, - "line_number": 826, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f9ca36cde6942f27b76eac83290189854ff3acd5", - "is_verified": false, - "line_number": 832, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "cf2179b851fcddc8328e4f40e46bec14a56747f8", - "is_verified": false, - "line_number": 838, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "427a8b3d029b9d8020cf1648330b5b0a01eb7e65", - "is_verified": false, - "line_number": 862, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a0e9cb28c049bc9f6680cd51dbef7f227f556e50", - "is_verified": false, - "line_number": 868, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "b5c86792f89b5c8eb61c92e9940a014475247b23", - "is_verified": false, - "line_number": 886, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "5bc62a0f48f3bd1f4c9aa548fba2a0b0234fbbd8", - "is_verified": false, - "line_number": 904, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "af246ca4758a5700d172533c40ff71522ae42d99", - "is_verified": false, - "line_number": 910, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "8c21d79a6f6a5080d3521470b90b316c89080f83", - "is_verified": false, - "line_number": 922, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "baacde28e4cf5095a02fd332813556fb52842d7b", - "is_verified": false, - "line_number": 933, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "53d87de97f77c9ea8b7795228a6ce24ed3dc0781", - "is_verified": false, - "line_number": 938, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "70fb06614f8b86a3daac0c88f0409b40d689689c", - "is_verified": false, - "line_number": 956, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "21c64dba6f59dad4f7f4934d4416f2805cefbd5a", - "is_verified": false, - "line_number": 962, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "1d3051aec8271f45991f72a68fc9be099d3e92c1", - "is_verified": false, - "line_number": 968, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "9f99b00169e0298e86716cdca88d9e546f9de36c", - "is_verified": false, - "line_number": 998, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "d377ef5b36367a118f28c20eb126e6ec376e02ea", - "is_verified": false, - "line_number": 1010, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "b521ee08d1454bfeda09d831eaae591d8c12404c", - "is_verified": false, - "line_number": 1022, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "face7337620d002b928dc0088e5617aafb67b966", - "is_verified": false, - "line_number": 1034, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "19b7d99d9b41aa84e4779f676bd2b22ce574906f", - "is_verified": false, - "line_number": 1040, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f0b2022fc412b5599ddcb48c6f8f87c5a53c26af", - "is_verified": false, - "line_number": 1046, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "446fa65c4cc6c235fabac8cb7d9241fb018514b8", - "is_verified": false, - "line_number": 1088, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "9cc81943eb951dbf87e0fbb52da90903304b8db9", - "is_verified": false, - "line_number": 1100, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "c69107ff29daaa4b30788f9cecd01d67bfc29b71", - "is_verified": false, - "line_number": 1106, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "60f948a394e2811370ba0bb6849777f217ab5274", - "is_verified": false, - "line_number": 1112, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "54be28b91891ca9ef7b85502a59b32a2a03a5cb9", - "is_verified": false, - "line_number": 1118, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "b6df3a01285e2f59424c8ded9d38ecf39c0af1b7", - "is_verified": false, - "line_number": 1130, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "54ed260e3bc31bc77ee06754dff850981d39a66c", - "is_verified": false, - "line_number": 1142, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "35be14614e83fe56d9b2ca1c0e2c2a74890b6889", - "is_verified": false, - "line_number": 1148, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "303d5144ff32301287cc201ecc9243e2d73850bf", - "is_verified": false, - "line_number": 1166, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "8b7be7f7fae86960989b939578d36ce617b498c6", - "is_verified": false, - "line_number": 1172, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a6c79dfeb177d34d195c2be48cc62800e629f115", - "is_verified": false, - "line_number": 1190, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "ef417aa1e71aee527bd6fa12f4490f7d960ec54f", - "is_verified": false, - "line_number": 1196, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a356ce34c2d87126e0170adbec7077e4421af5a5", - "is_verified": false, - "line_number": 1220, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "d3acb69a725a514fb55033e2920abcc24e0162cc", - "is_verified": false, - "line_number": 1262, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "797b61cd33f73538a622541ccdb8eee79c4b51c2", - "is_verified": false, - "line_number": 1286, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "871ca8e6c9f88aba0a0e921f9d2f47120b55bdfc", - "is_verified": false, - "line_number": 1292, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f746c3a4610d3b777453c50c95dc93598c8ad694", - "is_verified": false, - "line_number": 1310, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "1f7c6ecf67ba34903861aad770957fdbfa774269", - "is_verified": false, - "line_number": 1322, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "cd37616882a8287de17e49c9f91ecad00e0b0eae", - "is_verified": false, - "line_number": 1328, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "9373f1ccd9980640fbcec9c685d34eac3c4b9867", - "is_verified": false, - "line_number": 1358, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "227af0d6a86c8c8619233794dcb4ea5ed1195be3", - "is_verified": false, - "line_number": 1364, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "e907fb1ce9090d3555f18d6b2f2ea364d94c6217", - "is_verified": false, - "line_number": 1370, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "bd773713e294c76ec00f052b4aa03f8501b74ee7", - "is_verified": false, - "line_number": 1400, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "436a12a91365f61ddddfeb89b28089218f76b339", - "is_verified": false, - "line_number": 1412, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "7bdcea8d073c580f79a0a1982007a226a2439dbb", - "is_verified": false, - "line_number": 1418, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "2a296c37a4e26df0a86488d15b17ac9d8ec0dfcd", - "is_verified": false, - "line_number": 1424, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "3b991cdd2510d7fd1de8b025f0c7cbb9ac84b931", - "is_verified": false, - "line_number": 1430, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "573b6322edd45ab8e47491791f0909764e4a2f37", - "is_verified": false, - "line_number": 1442, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "ad51c22552ff8cf9c6399db508ceed9dfca2c3c8", - "is_verified": false, - "line_number": 1472, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "327c06fdd2c0f5c179499c1702ab323443093c42", - "is_verified": false, - "line_number": 1478, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "47ce443fa2c6d2894c896af5bf215e058b9211a7", - "is_verified": false, - "line_number": 1502, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "4676cd86733e19676c0704d55f548833f5273643", - "is_verified": false, - "line_number": 1508, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f16b56e2e46c4df6bf412a7a9b90c86957016575", - "is_verified": false, - "line_number": 1514, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a60fc256aaca59a332b08d58bd88404348a8bcb9", - "is_verified": false, - "line_number": 1520, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "04d0a3a2f4c5f2e29f293507958a27b53728c4e8", - "is_verified": false, - "line_number": 1532, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "dede8930d7418d092a12d114de08e444bf0dd82e", - "is_verified": false, - "line_number": 1550, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "2a6863fb102cdb7c5f83b6afd00a794efb701566", - "is_verified": false, - "line_number": 1562, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "4048123bacfc4d262ce85016a54ae55c8063edeb", - "is_verified": false, - "line_number": 1574, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "3de7722ca43ab9676c384eb479950083fb2385bb", - "is_verified": false, - "line_number": 1580, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "5ab5903f6c15a46a71c8db55e70119352304cc15", - "is_verified": false, - "line_number": 1598, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "4311a7e1eaf728d4f31467084f690eff7493a9e4", - "is_verified": false, - "line_number": 1610, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "c6654393d0b0f14057873630031d040e3dea115d", - "is_verified": false, - "line_number": 1616, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a229317aa176166d90f06d566b71932cff018638", - "is_verified": false, - "line_number": 1622, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "25118f28f0772791b1febea557df6f8eb10d0dd8", - "is_verified": false, - "line_number": 1628, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "08e984ad7dce0d92490e6fc8fe01910c8951109e", - "is_verified": false, - "line_number": 1646, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "3d442b2ea6e64698db1e44f7bd5ecb36daebc8a9", - "is_verified": false, - "line_number": 1652, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "244a01453acc60cca4380edd62539519c250d395", - "is_verified": false, - "line_number": 1658, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "ef4b28ff7563e530637c74c37555b1fb5a6966f0", - "is_verified": false, - "line_number": 1676, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "6516fc2579d674314a52e49462a84159df8479d9", - "is_verified": false, - "line_number": 1688, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "8ab07507a1c24711ad94bb37308e838447d4a5ca", - "is_verified": false, - "line_number": 1694, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "c89fdd11b805574e2ba8910cf63c4273044b887c", - "is_verified": false, - "line_number": 1706, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f2dd454db702c939d54193f0be69d772368ac676", - "is_verified": false, - "line_number": 1718, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "898a6c0a313f6e776b073bbc1b1e6010381c5d2b", - "is_verified": false, - "line_number": 1730, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "30ddcbfccd38de28196e92b6fcf77e65d122294d", - "is_verified": false, - "line_number": 1742, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "c2dc8a1d72a39ee9da360d47dcadfd7a5560ee7f", - "is_verified": false, - "line_number": 1748, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "efa90513d8e6348d4005c33485f2981bb2cc3411", - "is_verified": false, - "line_number": 1754, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "eb2f1f46999a581c6a1b8a2279963002e4effd2d", - "is_verified": false, - "line_number": 1760, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "240cd2b6629abde66f97f1955dd87fab8e045258", - "is_verified": false, - "line_number": 1766, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "cd50293b35634a61add9cbfeb9e48fbd44e78bc3", - "is_verified": false, - "line_number": 1790, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "b016c72dac43dd6eec034d8b49aa1ded1cc0c6fa", - "is_verified": false, - "line_number": 1796, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f59912210d43c78fe803463f6bfb35688508a2bf", - "is_verified": false, - "line_number": 1808, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "b0e82a9a7bedac4135f97637be0c11faa2122599", - "is_verified": false, - "line_number": 1814, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "5bf984f56eac13589ac2369cb0bae2f61869810a", - "is_verified": false, - "line_number": 1820, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "9f29336453dfa317f190f570b08116937a529f0b", - "is_verified": false, - "line_number": 1838, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "1579aca9caa27162a684e977c56693b37243d1b4", - "is_verified": false, - "line_number": 1844, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "2acd680fbb8b14e98aea68cfef28ce81eba86c71", - "is_verified": false, - "line_number": 1850, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "2dd96ae1cb8802018fb2f6a27926bb5f78957fb0", - "is_verified": false, - "line_number": 1856, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "3b61d62768cfb3c63d994d7988306f1ebd2acd6b", - "is_verified": false, - "line_number": 1862, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "d17b2d823c9310229ad18c83ffe543f49406ff9b", - "is_verified": false, - "line_number": 1874, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "e7d0065af9edfc8b2de193bbe26faf5a636e0e9f", - "is_verified": false, - "line_number": 1886, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "1bfed9fbd700374425b35a35ddf0f49a1e2469c2", - "is_verified": false, - "line_number": 1892, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "28ab1b1b9c8f05c055b6741bcaeab7337f5b5dc7", - "is_verified": false, - "line_number": 1898, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "76377d63ef7d864c0cefc5b38c762e16d3ab39b5", - "is_verified": false, - "line_number": 1904, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "562c0bc758bca6446fabf1aacf71f63d47bc62ed", - "is_verified": false, - "line_number": 1910, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "0113110e3d49f7b3a48e00192d478584449800e7", - "is_verified": false, - "line_number": 1916, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "8e201f749e20ab2d51d0de3da73effa5f616448d", - "is_verified": false, - "line_number": 1928, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "4ffc5d8cd514be957c9b87ac84c66205ab6d08d3", - "is_verified": false, - "line_number": 1970, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "236783f531bb4cc03a0f4a3e892b5c89e9f45881", - "is_verified": false, - "line_number": 1976, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "c0576697d180e97695dd29883a4e1ccb01b2f653", - "is_verified": false, - "line_number": 1982, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "497af5dcf573db44fc30ac071ebb008e7ac37669", - "is_verified": false, - "line_number": 2000, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "7d770d0728208206c486b536b06077c9953d21f2", - "is_verified": false, - "line_number": 2018, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "6a5f46048b547457e72572c2d38fb1046591ca71", - "is_verified": false, - "line_number": 2024, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "270c9abba84329e1be2fa7130b44134c23891f1f", - "is_verified": false, - "line_number": 2030, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "6fb5a96582d72c338a3f3a7d8144190630d64133", - "is_verified": false, - "line_number": 2036, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a781a6064ef5e2cb085282bb1912e65232fb55d1", - "is_verified": false, - "line_number": 2042, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "ef3435e29e3a2c5dcbbb633856c85561848cd995", - "is_verified": false, - "line_number": 2090, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "be1df677c309419f4efa0ac48afb2a573beeb95d", - "is_verified": false, - "line_number": 2108, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "5d65cf087adec89fb18354508030304fc3809586", - "is_verified": false, - "line_number": 2114, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "76913f65d6da6c5660de587c8a3e807aafa039dd", - "is_verified": false, - "line_number": 2126, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a71266907512ba33211f8ee38accedd3b84bf81a", - "is_verified": false, - "line_number": 2132, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "f261488408e7c6c4f5e9721426e652052ff36092", - "is_verified": false, - "line_number": 2144, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "e47929f0dc35b0d4eea6b4c80fa8fcdedd506d23", - "is_verified": false, - "line_number": 2150, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "a1d4fff4042a2dcb8c40293e53611f28a8721d8d", - "is_verified": false, - "line_number": 2156, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "1f01a7c11bde62eaf153d74394c282aa11574f2a", - "is_verified": false, - "line_number": 2162, - "is_secret": false - }, - { - "type": "Hex High Entropy String", - "filename": "src/lfx/src/lfx/_assets/stable_hash_history.json", - "hashed_secret": "697ccfba2c15c7cd8cf6307fd83a491b5c2c9e3e", - "is_verified": false, - "line_number": 2173, - "is_secret": false - } - ], "src/lfx/src/lfx/base/models/unified_models.py": [ { "type": "Secret Keyword", @@ -7092,5 +5674,5 @@ } ] }, - "generated_at": "2026-03-13T17:17:05Z" -} + "generated_at": "2026-03-12T02:41:29Z" +} \ No newline at end of file diff --git a/Makefile b/Makefile index 9a25080a0b1b..a3e48609613a 100644 --- a/Makefile +++ b/Makefile @@ -449,7 +449,6 @@ build_component_index: ## build the component index with dynamic loading @make install_backend @echo 'Building component index' LFX_DEV=1 uv run python scripts/build_component_index.py - LFX_DEV=1 uv run python scripts/build_hash_history.py lfx_build: ## build the LFX package @echo 'Building LFX package' diff --git a/scripts/build_hash_history.py b/scripts/build_hash_history.py deleted file mode 100755 index fd9afe507e44..000000000000 --- a/scripts/build_hash_history.py +++ /dev/null @@ -1,200 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import asyncio -import copy -from pathlib import Path - -import orjson -from packaging.version import Version - -STABLE_HISTORY_FILE = "src/lfx/src/lfx/_assets/stable_hash_history.json" -NIGHTLY_HISTORY_FILE = "src/lfx/src/lfx/_assets/nightly_hash_history.json" - - -def get_lfx_version(): - """Get the installed lfx version.""" - from importlib.metadata import PackageNotFoundError, version - - # Try lfx-nightly first (for nightly builds), then fall back to lfx - try: - return version("lfx-nightly") - except PackageNotFoundError: - return version("lfx") - - -def load_hash_history(file_path: Path) -> dict: - """Loads a hash history file.""" - if not file_path.exists(): - return {} - return orjson.loads(file_path.read_bytes()) - - -def save_hash_history(file_path: Path, history: dict): - """Saves a hash history file.""" - file_path.write_text(orjson.dumps(history, option=orjson.OPT_INDENT_2).decode("utf-8"), encoding="utf-8") - - -def _import_components() -> tuple[dict, int]: - """Import all lfx components using the async import function. - - Returns: - Tuple of (modules_dict, components_count) - - Raises: - RuntimeError: If component import fails - """ - from lfx.interface.components import import_langflow_components - - try: - components_result = asyncio.run(import_langflow_components()) - modules_dict = components_result.get("components", {}) - components_count = sum(len(v) for v in modules_dict.values()) - print(f"Discovered {components_count} components across {len(modules_dict)} categories") - except Exception as e: - msg = f"Failed to import components: {e}" - raise RuntimeError(msg) from e - else: - return modules_dict, components_count - - -def update_history(history: dict, component_name: str, code_hash: str, current_version: str) -> dict: - """Updates the hash history for a single component with the new simple schema. - - IMPORTANT: Note that the component_name acts as the unique identifier for the component, and must not be changed. - """ - current_version_parsed = Version(current_version) - # Use the string representation of the version as the key - # For dev versions (nightly), this includes the full version with dev suffix (e.g., "0.8.0.dev13") - # For stable versions, this is just major.minor.micro (e.g., "0.8.0") - version_key = str(current_version_parsed) - - if component_name not in history: - print(f"Component {component_name} not found in history. Adding...") - warning_msg = ( - f"WARNING - Ensure that Component {component_name} is a NEW Component. " - "If not, this is an error and will lose hash history for this component." - ) - print(warning_msg) - history[component_name] = {} - history[component_name]["versions"] = {version_key: code_hash} - else: - # Ensure that we aren't ovewriting a previous version - for v in history[component_name]["versions"]: - parsed_version = Version(v) - if parsed_version > current_version_parsed: - # If this happens, we are overwriting a previous version. - msg = ( - f"ERROR - Component {component_name} already has a version {v} that is greater than the current " - f"version {current_version}." - ) - raise ValueError(msg) - history[component_name]["versions"][version_key] = code_hash - - return history - - -def validate_append_only(old_history: dict, new_history: dict) -> None: - """Validate that the new history only adds data, never removes it. - - Args: - old_history: The previous hash history - new_history: The updated hash history - - Raises: - ValueError: If components or versions were removed - """ - # Check that no components were removed - old_components = set(old_history.keys()) - new_components = set(new_history.keys()) - removed_components = old_components - new_components - - if removed_components: - msg = ( - f"ERROR: Components were removed: {removed_components}\n" - "Hash history must be append-only. Components cannot be deleted." - ) - raise ValueError(msg) - - # Check that no version keys were removed from existing components - for component in old_components: - if component in new_history: - old_versions = set(old_history[component].get("versions", {}).keys()) - new_versions = set(new_history[component].get("versions", {}).keys()) - removed_versions = old_versions - new_versions - - if removed_versions: - msg = ( - f"ERROR: Versions removed from component '{component}': {removed_versions}\n" - "Hash history must be append-only. Version keys cannot be deleted." - ) - raise ValueError(msg) - - print("✓ Append-only validation passed - no components or versions were removed") - - -def main(argv=None): - """Main entry point for the script.""" - parser = argparse.ArgumentParser(description="Build and update component hash history.") - parser.add_argument("--nightly", action="store_true", help="Update the nightly hash history.") - args = parser.parse_args(argv) - - current_version = get_lfx_version() - print(f"Current LFX version: {current_version}") - - if args.nightly: - if "dev" not in str(current_version): - err = ( - f"Cannot update nightly hash history for a non-dev version.\n" - f"Expected version format: X.Y.Z.devN (e.g., 0.3.0.dev13)\n" - f"Got: {current_version}\n" - f"This indicates the LFX package was not properly updated to a nightly version." - ) - raise ValueError(err) - history_file = NIGHTLY_HISTORY_FILE - print(f"✓ Version check passed: {current_version} is a dev version") - print("Updating nightly hash history...") - else: - if "dev" in str(current_version): - err = ( - f"Cannot update stable hash history for a dev version.\n" - f"Expected version format: X.Y.Z (e.g., 0.3.0)\n" - f"Got: {current_version}\n" - f"This indicates the LFX package is a development version, not a stable release." - ) - raise ValueError(err) - history_file = STABLE_HISTORY_FILE - print(f"✓ Version check passed: {current_version} is a stable version") - print("Updating stable hash history...") - - modules_dict, components_count = _import_components() - print(f"Found {components_count} components.") - if not components_count: - print("No components found. Exiting.") - return - - old_history = load_hash_history(Path(history_file)) - new_history = copy.deepcopy(old_history) - - for category_name, components_dict in modules_dict.items(): - for comp_name, comp_details in components_dict.items(): - if "metadata" not in comp_details: - print(f"Warning: Component {comp_name} in category {category_name} is missing metadata. Skipping.") - continue - - code_hash = comp_details["metadata"].get("code_hash") - - if not code_hash: - print(f"Warning: Component {comp_name} in category {category_name} is missing code_hash. Skipping.") - continue - - new_history = update_history(new_history, comp_name, code_hash, current_version) - - # Validate append-only constraint before saving - validate_append_only(old_history, new_history) - - save_hash_history(Path(history_file), new_history) - print(f"Successfully updated {history_file}") - - -if __name__ == "__main__": - main() diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index f696dce9e53d..1d91ea3c5bca 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -65,11 +65,6 @@ def update_projects_components_with_latest_component_versions(project_data, all_ all_types_dict_flat = {} for category in all_types_dict.values(): for key, component in category.items(): - # Strip hash_history from component metadata before using in flows - # hash_history is internal metadata for tracking component evolution - # and should only exist in component_index.json, not in saved flows - if "metadata" in component and "hash_history" in component["metadata"]: - del component["metadata"]["hash_history"] all_types_dict_flat[key] = component node_changes_log = defaultdict(list) diff --git a/src/backend/tests/unit/test_build_hash_history.py b/src/backend/tests/unit/test_build_hash_history.py deleted file mode 100644 index a2e51e6632ba..000000000000 --- a/src/backend/tests/unit/test_build_hash_history.py +++ /dev/null @@ -1,119 +0,0 @@ -import sys -from pathlib import Path -from unittest.mock import patch - -import pytest - -# Add the scripts directory to the Python path -sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent.parent.parent / "scripts")) - -# Now we can import the script -from build_hash_history import _import_components, main, update_history - - -@pytest.fixture -def mock_modules_dict(): - """Create a mock modules_dict with a nested structure.""" - return { - "category1": { - "MyComponent": { - "metadata": { - "component_id": "1234-5678-9012-3456", - "code_hash": "hash_v1", - }, - "display_name": "MyComponent", - }, - "AnotherComponent": { - "metadata": { - "component_id": "2345-6789-0123-4567", - "code_hash": "hash_v2", - }, - "display_name": "AnotherComponent", - }, - }, - "category2": { - "ThirdComponent": { - "metadata": { - "component_id": "3456-7890-1234-5678", - "code_hash": "hash_v3", - }, - "display_name": "ThirdComponent", - }, - }, - } - - -def test_update_history_scenarios(): - """Test various scenarios for the update_history function.""" - history = {} - component_name = "MyComponent" - code_hash_v1 = "hash_v1" - code_hash_v2 = "hash_v2" - - # Scenario 1: Initial version - history = update_history(history, component_name, code_hash_v1, "0.3.0") - assert history[component_name]["versions"]["0.3.0"] == code_hash_v1 - - # Scenario 2: New patch version, same hash - history = update_history(history, component_name, code_hash_v1, "0.3.1") - assert history[component_name]["versions"]["0.3.1"] == code_hash_v1 - - # Scenario 3: New patch version, new hash - history = update_history(history, component_name, code_hash_v2, "0.3.2") - assert history[component_name]["versions"]["0.3.2"] == code_hash_v2 - - # Scenario 4: New minor version, same hash as an old version - history = update_history(history, component_name, code_hash_v1, "0.4.0") - assert history[component_name]["versions"]["0.4.0"] == code_hash_v1 - - # Scenario 5: Update hash for the same version - history = update_history(history, component_name, code_hash_v2, "0.5.0") - assert history[component_name]["versions"]["0.5.0"] == code_hash_v2 - history = update_history(history, component_name, code_hash_v1, "0.5.0") - assert history[component_name]["versions"]["0.5.0"] == code_hash_v1 - - # Scenario 6: Overwriting a newer version with an older one should raise an error - with pytest.raises(ValueError, match="already has a version"): - update_history(history, component_name, code_hash_v1, "0.4.0") - - -def test_main_function(tmp_path, mock_modules_dict): - """Test the main function with mock data.""" - history_file = tmp_path / "history.json" - - with ( - patch("build_hash_history._import_components") as mock_import, - patch("build_hash_history.load_hash_history") as mock_load, - patch("build_hash_history.save_hash_history") as mock_save, - patch("build_hash_history.get_lfx_version") as mock_get_version, - patch("build_hash_history.Path") as mock_path, - ): - mock_import.return_value = (mock_modules_dict, 3) - mock_load.return_value = {} - mock_get_version.return_value = "0.1.0" - mock_path.return_value = history_file - - # Run main with mocked functions - main([]) - - mock_save.assert_called_once() - saved_history = mock_save.call_args[0][1] - - assert len(saved_history) == 3 - assert "MyComponent" in saved_history - assert saved_history["MyComponent"]["versions"]["0.1.0"] == "hash_v1" - assert "AnotherComponent" in saved_history - assert saved_history["AnotherComponent"]["versions"]["0.1.0"] == "hash_v2" - assert "ThirdComponent" in saved_history - assert saved_history["ThirdComponent"]["versions"]["0.1.0"] == "hash_v3" - - -def test_all_real_component_names_are_unique(): - """Test that all real component names loaded via _import_components are unique.""" - modules_dict, _ = _import_components() # Load real components - - component_names = [ - component_name for components_dict in modules_dict.values() for component_name in components_dict - ] - - assert len(component_names) == len(set(component_names)) diff --git a/src/backend/tests/unit/test_initial_setup.py b/src/backend/tests/unit/test_initial_setup.py index e99adf2071bb..e7db2631e6cd 100644 --- a/src/backend/tests/unit/test_initial_setup.py +++ b/src/backend/tests/unit/test_initial_setup.py @@ -515,187 +515,3 @@ async def test_copy_profile_pictures_handles_missing_config_dir(): with pytest.raises(ValueError, match="Config dir is not set"): await copy_profile_pictures() - - -# ==================== Hash History Tests ==================== - - -def test_update_projects_strips_hash_history_from_components(): - """Test that hash_history is stripped from components when updating projects. - - This ensures that internal component metadata (hash_history) used for tracking - component evolution in the component index does not leak into saved flows. - """ - # Create a mock all_types_dict with hash_history in component metadata - all_types_dict = { - "agents": { - "Agent": { - "template": { - "code": {"value": "test code"}, - "_type": "Component", - }, - "display_name": "Agent", - "metadata": { - "code_hash": "abc123", - "hash_history": [ # This should be stripped - {"hash": "abc123", "v_from": "1.0.0", "version_last": "1.0.1"} - ], - }, - } - } - } - - # Create a mock project with a node using this component - project_data = { - "nodes": [ - { - "data": { - "type": "Agent", - "node": { - "template": { - "code": {"value": "old code"}, - "_type": "Component", - }, - "outputs": [], - }, - } - } - ] - } - - # Update the project - updated_project = update_projects_components_with_latest_component_versions(project_data, all_types_dict) - - # Verify the component was updated - updated_node = updated_project["nodes"][0]["data"]["node"] - assert updated_node["template"]["code"]["value"] == "test code" - - # CRITICAL: Verify hash_history was NOT copied into the flow - # Hash_history should only exist in component_index.json, never in saved flows - node_metadata = updated_node.get("metadata", {}) - assert "hash_history" not in node_metadata, ( - "hash_history should not be present in flow nodes. " - "It is internal metadata for component evolution tracking and should only exist in component_index.json" - ) - - -def test_update_projects_preserves_other_metadata(): - """Test that other metadata fields are preserved when stripping hash_history.""" - all_types_dict = { - "agents": { - "Agent": { - "template": { - "code": {"value": "test code"}, - "_type": "Component", - }, - "display_name": "Agent", - "metadata": { - "code_hash": "abc123", - "module": "test.module", - "hash_history": [{"hash": "abc123", "v_from": "1.0.0", "v_to": "1.0.1"}], - }, - } - } - } - - project_data = { - "nodes": [ - { - "data": { - "type": "Agent", - "node": { - "template": { - "code": {"value": "old code"}, - "_type": "Component", - }, - "outputs": [], - }, - } - } - ] - } - - update_projects_components_with_latest_component_versions(project_data, all_types_dict) - - # Verify hash_history is stripped but other metadata is preserved - # Note: The function doesn't copy metadata to nodes, it only updates template - # This test verifies the internal flattened dict doesn't have hash_history - # The actual metadata preservation happens in the template update logic - - -def test_update_projects_handles_components_without_metadata(): - """Test that components without metadata are handled gracefully.""" - all_types_dict = { - "agents": { - "Agent": { - "template": { - "code": {"value": "test code"}, - "_type": "Component", - }, - "display_name": "Agent", - # No metadata field at all - } - } - } - - project_data = { - "nodes": [ - { - "data": { - "type": "Agent", - "node": { - "template": { - "code": {"value": "old code"}, - "_type": "Component", - }, - "outputs": [], - }, - } - } - ] - } - - # Should not raise an error - updated_project = update_projects_components_with_latest_component_versions(project_data, all_types_dict) - assert updated_project["nodes"][0]["data"]["node"]["template"]["code"]["value"] == "test code" - - -def test_update_projects_handles_components_without_hash_history(): - """Test that components with metadata but no hash_history are handled gracefully.""" - all_types_dict = { - "agents": { - "Agent": { - "template": { - "code": {"value": "test code"}, - "_type": "Component", - }, - "display_name": "Agent", - "metadata": { - "code_hash": "abc123", - "module": "test.module", - # No hash_history field - }, - } - } - } - - project_data = { - "nodes": [ - { - "data": { - "type": "Agent", - "node": { - "template": { - "code": {"value": "old code"}, - "_type": "Component", - }, - "outputs": [], - }, - } - } - ] - } - - # Should not raise an error - updated_project = update_projects_components_with_latest_component_versions(project_data, all_types_dict) - assert updated_project["nodes"][0]["data"]["node"]["template"]["code"]["value"] == "test code" diff --git a/src/backend/tests/unit/test_starter_projects_no_hash_history.py b/src/backend/tests/unit/test_starter_projects_no_hash_history.py deleted file mode 100644 index d3faadfa074d..000000000000 --- a/src/backend/tests/unit/test_starter_projects_no_hash_history.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Test that starter projects do not contain hash_history in their JSON files. - -This test ensures that internal component metadata (hash_history) used for tracking -component evolution in the component index does not leak into saved flow templates. -""" - -import json -from pathlib import Path - -import pytest - - -def find_hash_history_in_dict(data, path=""): - """Recursively search for hash_history keys in nested dictionaries. - - Args: - data: Dictionary or list to search - path: Current path in the data structure (for error reporting) - - Returns: - List of paths where hash_history was found - """ - found_paths = [] - - if isinstance(data, dict): - for key, value in data.items(): - current_path = f"{path}.{key}" if path else key - - if key == "hash_history": - found_paths.append(current_path) - - # Recursively search nested structures - found_paths.extend(find_hash_history_in_dict(value, current_path)) - - elif isinstance(data, list): - for i, item in enumerate(data): - current_path = f"{path}[{i}]" - found_paths.extend(find_hash_history_in_dict(item, current_path)) - - return found_paths - - -def get_starter_project_files(): - """Get all starter project JSON files.""" - starter_projects_dir = ( - Path(__file__).parent.parent.parent / "base" / "langflow" / "initial_setup" / "starter_projects" - ) - - if not starter_projects_dir.exists(): - pytest.skip(f"Starter projects directory not found: {starter_projects_dir}") - - json_files = list(starter_projects_dir.glob("*.json")) - - if not json_files: - pytest.skip(f"No JSON files found in {starter_projects_dir}") - - return json_files - - -@pytest.mark.parametrize("project_file", get_starter_project_files()) -def test_starter_project_has_no_hash_history(project_file): - """Test that a starter project file does not contain hash_history. - - Hash_history is internal metadata for tracking component code evolution - and should only exist in component_index.json, never in saved flows. - """ - with project_file.open(encoding="utf-8") as f: - project_data = json.load(f) - - # Search for any hash_history keys in the entire project structure - hash_history_paths = find_hash_history_in_dict(project_data) - - assert not hash_history_paths, ( - f"Found hash_history in {project_file.name} at paths: {hash_history_paths}\n" - "hash_history is internal component metadata and should not be in saved flows. " - "It should only exist in component_index.json for tracking component evolution." - ) - - -def test_all_starter_projects_loaded(): - """Sanity check that we're actually testing starter projects.""" - project_files = get_starter_project_files() - - # We should have multiple starter projects - assert len(project_files) > 0, "No starter project files found to test" - - # Print count for visibility diff --git a/src/lfx/src/lfx/base/models/unified_models.py b/src/lfx/src/lfx/base/models/unified_models.py index f4dfb6b8ddf1..ca4c8ad73d5d 100644 --- a/src/lfx/src/lfx/base/models/unified_models.py +++ b/src/lfx/src/lfx/base/models/unified_models.py @@ -18,10 +18,19 @@ GOOGLE_GENERATIVE_AI_EMBEDDING_MODELS_DETAILED, GOOGLE_GENERATIVE_AI_MODELS_DETAILED, ) -from lfx.base.models.model_metadata import MODEL_PROVIDER_METADATA, get_provider_param_mapping +from lfx.base.models.model_metadata import ( + MODEL_PROVIDER_METADATA, + get_provider_param_mapping, +) from lfx.base.models.model_utils import _to_str, replace_with_live_models -from lfx.base.models.ollama_constants import OLLAMA_EMBEDDING_MODELS_DETAILED, OLLAMA_MODELS_DETAILED -from lfx.base.models.openai_constants import OPENAI_EMBEDDING_MODELS_DETAILED, OPENAI_MODELS_DETAILED +from lfx.base.models.ollama_constants import ( + OLLAMA_EMBEDDING_MODELS_DETAILED, + OLLAMA_MODELS_DETAILED, +) +from lfx.base.models.openai_constants import ( + OPENAI_EMBEDDING_MODELS_DETAILED, + OPENAI_MODELS_DETAILED, +) from lfx.base.models.watsonx_constants import WATSONX_MODELS_DETAILED from lfx.log.logger import logger from lfx.services.deps import get_variable_service, session_scope @@ -44,7 +53,11 @@ _EMBEDDING_CLASS_IMPORTS: dict[str, tuple[str, str, str | None]] = { "OpenAIEmbeddings": ("langchain_openai", "OpenAIEmbeddings", None), - "GoogleGenerativeAIEmbeddings": ("langchain_google_genai", "GoogleGenerativeAIEmbeddings", None), + "GoogleGenerativeAIEmbeddings": ( + "langchain_google_genai", + "GoogleGenerativeAIEmbeddings", + None, + ), "OllamaEmbeddings": ("langchain_ollama", "OllamaEmbeddings", None), "WatsonxEmbeddings": ("langchain_ibm", "WatsonxEmbeddings", None), } @@ -454,7 +467,7 @@ async def _get_by_var_name(): return None try: return await variable_service.get_variable( - user_id=UUID(user_id) if isinstance(user_id, str) else user_id, + user_id=(UUID(user_id) if isinstance(user_id, str) else user_id), name=var_name, field="", session=session, @@ -716,7 +729,12 @@ def validate_model_provider_key(provider: str, variables: dict[str, str], model_ logger.error(f"Error getting unified models for provider {provider}: {e}") # For providers that need a model to test credentials - if not first_model and provider in ["OpenAI", "Anthropic", "Google Generative AI", "IBM WatsonX"]: + if not first_model and provider in [ + "OpenAI", + "Anthropic", + "Google Generative AI", + "IBM WatsonX", + ]: return try: @@ -756,7 +774,11 @@ def validate_model_provider_key(provider: str, variables: dict[str, str], model_ if not api_key or not project_id: return llm = ChatWatsonx( - apikey=api_key, url=url, model_id=first_model, project_id=project_id, params={"max_new_tokens": 1} + apikey=api_key, + url=url, + model_id=first_model, + project_id=project_id, + params={"max_new_tokens": 1}, ) llm.invoke("test") @@ -856,7 +878,9 @@ async def _get_model_status(): variable_service = get_variable_service() if variable_service is None: return set(), set() - from langflow.services.variable.service import DatabaseVariableService + from langflow.services.variable.service import ( + DatabaseVariableService, + ) if not isinstance(variable_service, DatabaseVariableService): return set(), set() @@ -893,7 +917,9 @@ async def _get_enabled_providers(): if variable_service is None: return set() - from langflow.services.variable.service import DatabaseVariableService + from langflow.services.variable.service import ( + DatabaseVariableService, + ) if not isinstance(variable_service, DatabaseVariableService): return set() @@ -934,7 +960,9 @@ def __init__(self, value): try: # Get the raw Variable object to access the actual value variable_obj = await variable_service.get_variable_object( - user_id=user_id_uuid, name=var_name, session=session + user_id=user_id_uuid, + name=var_name, + session=session, ) if variable_obj and variable_obj.value: all_provider_variables[var_name] = VarWithValue(variable_obj.value) @@ -1050,7 +1078,9 @@ def __init__(self, value): return options -def get_embedding_model_options(user_id: UUID | str | None = None) -> list[dict[str, Any]]: +def get_embedding_model_options( + user_id: UUID | str | None = None, +) -> list[dict[str, Any]]: """Return a list of available embedding model providers with their configuration. This function uses get_unified_models_detailed() which respects the enabled/disabled @@ -1077,7 +1107,9 @@ async def _get_model_status(): variable_service = get_variable_service() if variable_service is None: return set(), set() - from langflow.services.variable.service import DatabaseVariableService + from langflow.services.variable.service import ( + DatabaseVariableService, + ) if not isinstance(variable_service, DatabaseVariableService): return set(), set() @@ -1114,7 +1146,9 @@ async def _get_enabled_providers(): if variable_service is None: return set() - from langflow.services.variable.service import DatabaseVariableService + from langflow.services.variable.service import ( + DatabaseVariableService, + ) if not isinstance(variable_service, DatabaseVariableService): return set() @@ -1155,7 +1189,9 @@ def __init__(self, value): try: # Get the raw Variable object to access the actual value variable_obj = await variable_service.get_variable_object( - user_id=user_id_uuid, name=var_name, session=session + user_id=user_id_uuid, + name=var_name, + session=session, ) if variable_obj and variable_obj.value: all_provider_variables[var_name] = VarWithValue(variable_obj.value) @@ -1174,7 +1210,13 @@ def __init__(self, value): # Replace static defaults with actual available models from configured instances if enabled_providers: - replace_with_live_models(all_models, user_id, enabled_providers, "embeddings", model_provider_metadata) + replace_with_live_models( + all_models, + user_id, + enabled_providers, + "embeddings", + model_provider_metadata, + ) options = [] @@ -1283,7 +1325,9 @@ def __init__(self, value): return options -def normalize_model_names_to_dicts(model_names: list[str] | str) -> list[dict[str, Any]]: +def normalize_model_names_to_dicts( + model_names: list[str] | str, +) -> list[dict[str, Any]]: """Convert simple model name(s) to list of dicts format. Args: @@ -1293,9 +1337,6 @@ def normalize_model_names_to_dicts(model_names: list[str] | str) -> list[dict[st A list of dicts with full model metadata including runtime info Examples: - >>> normalize_model_names_to_dicts('gpt-4o') - [{'name': 'gpt-4o', 'provider': 'OpenAI', 'metadata': {'model_class': 'ChatOpenAI', ...}}] - >>> normalize_model_names_to_dicts(['gpt-4o', 'claude-3']) [{'name': 'gpt-4o', ...}, {'name': 'claude-3', ...}] """ @@ -1667,7 +1708,9 @@ async def _get_default_model(): variable_service = get_variable_service() if variable_service is None: return None, None - from langflow.services.variable.service import DatabaseVariableService + from langflow.services.variable.service import ( + DatabaseVariableService, + ) if not isinstance(variable_service, DatabaseVariableService): return None, None @@ -1681,9 +1724,9 @@ async def _get_default_model(): try: var = await variable_service.get_variable_object( - user_id=UUID(component.user_id) - if isinstance(component.user_id, str) - else component.user_id, + user_id=( + UUID(component.user_id) if isinstance(component.user_id, str) else component.user_id + ), name=var_name, session=session, ) From 8dbcbb0928e8ff48f2e04d6495abbe7d92d3429a Mon Sep 17 00:00:00 2001 From: Adam-Aghili <149833988+Adam-Aghili@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:21:46 -0400 Subject: [PATCH 21/29] chore: release nightlies from of release branch (#12181) * chore: release nightlies from of release branch in preperation for the new release cycle strategy we will move the nightly to be run off the latest release branch. One caveat worth documenting: When we create the release branch we need to bump the verions for base and main immediately or else the ngihtly will run off the branch but will use a preveious release tag I also do not know how to deal with `merge-hash-history-to-main` in this case * chore: address valid code rabbit comments add clear error when branch does not exist make sure valid inputs is respected in the rest of the jobs/steps release_nightly.yml now uses nightly_tag_release instead of the old nightly_tag_main --- .github/workflows/nightly_build.yml | 62 +++++++++++++++++++-------- .github/workflows/release_nightly.yml | 28 ++++++------ .secrets.baseline | 6 +-- scripts/ci/pypi_nightly_tag.py | 10 ++--- 4 files changed, 65 insertions(+), 41 deletions(-) diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index adba3027b100..569f980901ec 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -49,9 +49,30 @@ jobs: run: | echo "Cannot skip tests while push_to_registry is true." exit 1 + resolve-release-branch: + runs-on: ubuntu-latest + outputs: + branch: ${{ steps.get_branch.outputs.branch }} + steps: + - name: Find latest release branch + id: get_branch + run: | + git ls-remote --heads https://github.com/${{ github.repository }} 'refs/heads/release-*' \ + | awk '{print $2}' \ + | sed 's|refs/heads/||' \ + | sort -V \ + | tail -n 1 > branch.txt + BRANCH=$(cat branch.txt) + if [ -z "$BRANCH" ]; then + echo "No release-* branch found in ${{ github.repository }}" + exit 1 + fi + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "Using release branch: $BRANCH" create-nightly-tag: if: github.repository == 'langflow-ai/langflow' + needs: [validate-inputs, resolve-release-branch] runs-on: ubuntu-latest defaults: run: @@ -60,13 +81,14 @@ jobs: # Required to create tag contents: write outputs: - main_tag: ${{ steps.generate_main_tag.outputs.main_tag }} + release_tag: ${{ steps.generate_release_tag.outputs.release_tag }} base_tag: ${{ steps.set_base_tag.outputs.base_tag }} lfx_tag: ${{ steps.generate_lfx_tag.outputs.lfx_tag }} steps: - name: Checkout code uses: actions/checkout@v6 with: + ref: ${{ needs.resolve-release-branch.outputs.branch }} persist-credentials: true - name: "Setup Environment" uses: astral-sh/setup-uv@v6 @@ -79,20 +101,20 @@ jobs: run: uv sync - name: Generate main nightly tag - id: generate_main_tag + id: generate_release_tag run: | # NOTE: This outputs the tag with the `v` prefix. - MAIN_TAG="$(uv run ./scripts/ci/pypi_nightly_tag.py main)" - echo "main_tag=$MAIN_TAG" >> $GITHUB_OUTPUT - echo "main_tag=$MAIN_TAG" + RELEASE_TAG="$(uv run ./scripts/ci/pypi_nightly_tag.py main)" + echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT + echo "release_tag=$RELEASE_TAG" - name: Delete existing tag if it exists - id: check_main_tag + id: check_release_tag run: | git fetch --tags - git tag -d ${{ steps.generate_main_tag.outputs.main_tag }} || true - git push --delete origin ${{ steps.generate_main_tag.outputs.main_tag }} || true - echo "main_tag_exists=false" >> $GITHUB_OUTPUT + git tag -d ${{ steps.generate_release_tag.outputs.release_tag }} || true + git push --delete origin ${{ steps.generate_release_tag.outputs.release_tag }} || true + echo "release_tag_exists=false" >> $GITHUB_OUTPUT - name: Generate base nightly tag id: generate_base_tag @@ -118,13 +140,13 @@ jobs: git config --global user.email "bot-nightly-builds@langflow.org" git config --global user.name "Langflow Bot" - MAIN_TAG="${{ steps.generate_main_tag.outputs.main_tag }}" + RELEASE_TAG="${{ steps.generate_release_tag.outputs.release_tag }}" BASE_TAG="${{ steps.generate_base_tag.outputs.base_tag }}" LFX_TAG="${{ steps.generate_lfx_tag.outputs.lfx_tag }}" echo "Updating LFX project version to $LFX_TAG" uv run ./scripts/ci/update_lfx_version.py $LFX_TAG - echo "Updating base project version to $BASE_TAG and updating main project version to $MAIN_TAG" - uv run --no-sync ./scripts/ci/update_pyproject_combined.py main $MAIN_TAG $BASE_TAG $LFX_TAG + echo "Updating base project version to $BASE_TAG and updating main project version to $RELEASE_TAG" + uv run --no-sync ./scripts/ci/update_pyproject_combined.py main $RELEASE_TAG $BASE_TAG $LFX_TAG uv lock cd src/backend/base && uv lock && cd ../../.. @@ -133,14 +155,14 @@ jobs: git add pyproject.toml src/backend/base/pyproject.toml src/lfx/pyproject.toml uv.lock src/backend/base/uv.lock git commit -m "Update version and project name" - echo "Tagging main with $MAIN_TAG" - if ! git tag -a $MAIN_TAG -m "Langflow nightly $MAIN_TAG"; then + echo "Tagging main with $RELEASE_TAG" + if ! git tag -a $RELEASE_TAG -m "Langflow nightly $RELEASE_TAG"; then echo "Tag creation failed. Exiting the workflow." exit 1 fi - echo "Pushing main tag $MAIN_TAG" - if ! git push origin $MAIN_TAG; then + echo "Pushing main tag $RELEASE_TAG" + if ! git push origin $RELEASE_TAG; then echo "Tag push failed. Check if the tag already exists. Exiting the workflow." exit 1 fi @@ -149,7 +171,8 @@ jobs: - name: Checkout main nightly tag uses: actions/checkout@v6 with: - ref: ${{ steps.generate_main_tag.outputs.main_tag }} + ref: ${{ steps.generate_release_tag.outputs.release_tag }} + persist-credentials: true - name: Retrieve Base Tag id: retrieve_base_tag @@ -217,14 +240,15 @@ jobs: release-nightly-build: if: github.repository == 'langflow-ai/langflow' && always() && needs.frontend-tests.result != 'failure' && needs.backend-unit-tests.result != 'failure' name: Run Nightly Langflow Build - needs: [create-nightly-tag, frontend-tests, backend-unit-tests] + needs: + [validate-inputs, create-nightly-tag, frontend-tests, backend-unit-tests] uses: ./.github/workflows/release_nightly.yml with: build_docker_base: true build_docker_main: true build_docker_ep: true build_lfx: true - nightly_tag_main: ${{ needs.create-nightly-tag.outputs.main_tag }} + nightly_tag_release: ${{ needs.create-nightly-tag.outputs.release_tag }} nightly_tag_base: ${{ needs.create-nightly-tag.outputs.base_tag }} nightly_tag_lfx: ${{ needs.create-nightly-tag.outputs.lfx_tag }} # When triggered by schedule, inputs.push_to_registry is not set, so default to true diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 72ac8f93b371..43dfa3c4606f 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -24,7 +24,7 @@ on: required: false type: boolean default: false - nightly_tag_main: + nightly_tag_release: description: "Tag for the nightly main build" required: true type: string @@ -60,7 +60,7 @@ jobs: - name: Check out the code at a specific ref uses: actions/checkout@v6 with: - ref: ${{ inputs.nightly_tag_main }} + ref: ${{ inputs.nightly_tag_release }} persist-credentials: true - name: "Setup Environment" uses: astral-sh/setup-uv@v6 @@ -126,7 +126,7 @@ jobs: - name: Check out the code at a specific ref uses: actions/checkout@v6 with: - ref: ${{ inputs.nightly_tag_main }} + ref: ${{ inputs.nightly_tag_release }} persist-credentials: true - name: "Setup Environment" uses: astral-sh/setup-uv@v6 @@ -223,7 +223,7 @@ jobs: - name: Check out the code at a specific ref uses: actions/checkout@v6 with: - ref: ${{ inputs.nightly_tag_main}} + ref: ${{ inputs.nightly_tag_release}} persist-credentials: true - name: "Setup Environment" uses: astral-sh/setup-uv@v6 @@ -269,8 +269,8 @@ jobs: echo "Name $name does not match langflow-nightly. Exiting the workflow." exit 1 fi - if [ "$version" != "${{ inputs.nightly_tag_main }}" ]; then - echo "Version $version does not match nightly tag ${{ inputs.nightly_tag_main }}. Exiting the workflow." + if [ "$version" != "${{ inputs.nightly_tag_release }}" ]; then + echo "Version $version does not match nightly tag ${{ inputs.nightly_tag_release }}. Exiting the workflow." exit 1 fi # Strip the leading `v` from the version @@ -326,7 +326,7 @@ jobs: - name: Check out the code uses: actions/checkout@v6 with: - ref: ${{ inputs.nightly_tag_main }} + ref: ${{ inputs.nightly_tag_release }} persist-credentials: true - name: Download LFX artifact uses: actions/download-artifact@v7 @@ -354,7 +354,7 @@ jobs: - name: Checkout code uses: actions/checkout@v6 with: - ref: ${{ inputs.nightly_tag_main }} + ref: ${{ inputs.nightly_tag_release }} persist-credentials: true - name: Download base artifact uses: actions/download-artifact@v7 @@ -382,7 +382,7 @@ jobs: - name: Checkout code uses: actions/checkout@v6 with: - ref: ${{ inputs.nightly_tag_main }} + ref: ${{ inputs.nightly_tag_release }} persist-credentials: true - name: Download main artifact uses: actions/download-artifact@v7 @@ -407,7 +407,7 @@ jobs: needs: [build-nightly-base, build-nightly-main] uses: ./.github/workflows/docker-nightly-build.yml with: - ref: ${{ inputs.nightly_tag_main }} + ref: ${{ inputs.nightly_tag_release }} release_type: nightly-base push_to_registry: ${{ inputs.push_to_registry }} secrets: inherit @@ -418,7 +418,7 @@ jobs: needs: [build-nightly-main, call_docker_build_base] uses: ./.github/workflows/docker-nightly-build.yml with: - ref: ${{ inputs.nightly_tag_main }} + ref: ${{ inputs.nightly_tag_release }} release_type: nightly-main push_to_registry: ${{ inputs.push_to_registry }} secrets: inherit @@ -430,9 +430,9 @@ jobs: # needs: [build-nightly-main] # uses: ./.github/workflows/docker-nightly-build.yml # with: - # ref: ${{ inputs.nightly_tag_main }} + # ref: ${{ inputs.nightly_tag_release }} # release_type: nightly-main-all - # main_version: ${{ inputs.nightly_tag_main }} + # main_version: ${{ inputs.nightly_tag_release }} # secrets: inherit # call_docker_build_main_ep: @@ -441,7 +441,7 @@ jobs: # needs: [build-nightly-main, call_docker_build_main] # uses: ./.github/workflows/docker-build-v2.yml # with: - # ref: ${{ inputs.nightly_tag_main }} + # ref: ${{ inputs.nightly_tag_release }} # release_type: main-ep # push_to_registry: ${{ inputs.push_to_registry }} # secrets: inherit diff --git a/.secrets.baseline b/.secrets.baseline index 16a945791484..5fdc03d32d6d 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -153,7 +153,7 @@ "filename": ".github/workflows/nightly_build.yml", "hashed_secret": "3e26d6750975d678acb8fa35a0f69237881576b0", "is_verified": false, - "line_number": 322, + "line_number": 257, "is_secret": false } ], @@ -5674,5 +5674,5 @@ } ] }, - "generated_at": "2026-03-12T02:41:29Z" -} \ No newline at end of file + "generated_at": "2026-03-16T13:08:35Z" +} diff --git a/scripts/ci/pypi_nightly_tag.py b/scripts/ci/pypi_nightly_tag.py index 38d9c3450bc5..3bbc4bd9c65f 100755 --- a/scripts/ci/pypi_nightly_tag.py +++ b/scripts/ci/pypi_nightly_tag.py @@ -27,6 +27,7 @@ def get_latest_published_version(build_type: str, *, is_nightly: bool) -> Versio raise ValueError(msg) res = requests.get(url, timeout=10) + res.raise_for_status() try: version_str = res.json()["info"]["version"] except Exception as e: @@ -49,19 +50,18 @@ def create_tag(build_type: str): try: current_nightly_version = get_latest_published_version(build_type, is_nightly=True) - nightly_base_version = current_nightly_version.base_version except (requests.RequestException, KeyError, ValueError): - # If MAIN_TAG nightly doesn't exist on PyPI yet, this is the first nightly + # If nightly doesn't exist yet current_nightly_version = None - nightly_base_version = None build_number = "0" latest_base_version = current_version.base_version - nightly_base_version = current_nightly_version.base_version + nightly_base_version = current_nightly_version.base_version if current_nightly_version else None if latest_base_version == nightly_base_version: # If the latest version is the same as the nightly version, increment the build number - build_number = str(current_nightly_version.dev + 1) + dev_number = (current_nightly_version.dev or 0) if current_nightly_version else 0 + build_number = str(dev_number + 1) new_nightly_version = latest_base_version + ".dev" + build_number From 37cf0a14ed380180cf52ba10a2d910ef1c80b2ec Mon Sep 17 00:00:00 2001 From: Jordan Frazier <122494242+jordanrfrazier@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:55:51 -0400 Subject: [PATCH 22/29] test: add upgrade migration check to ci (#12061) * Add upgrade migration check to ci * [autofix.ci] apply automated fixes * Add fetch step * ruff * Add merge migration * Revert "Add merge migration" This reverts commit fd32424739a758646e77c5967420198ff21b5970. backups * coderabbit suggestions 1. Shell hardening in workflow - set -euo pipefail, full path grep, quoted variables 2. _WORKSPACE_ROOT extracted as module constant (also addresses Cristhianzl's review comment about parents[5] duplication) 3. git missing returns None instead of raising FileNotFoundError 4. # noqa: S603 added to subprocess.run (fixes the Ruff CI failure) 5. FK noise filtering now also compares target table/column, not just ondelete/onupdate 6. Removed redundant git fetch origin main step (fetch-depth: 0 already fetches all branches) 7. Deduplicated Alembic config creation in _get_main_branch_head (moved before the if branch) 8. Simplified dict type hints (removed unnecessary dict[tuple, object]) * test: improve migration tests from PR review feedback - Narrow broad except clause to only wrap subprocess.run call - Add specific error messages for multi-head and unresolvable revisions - Remove redundant hardcoded schema test (covered by compare_metadata) - Fix SQLite FK noise filter to skip ondelete/onupdate comparison - Add downgrade verification to test_upgrade_from_main_branch - Add test file and workflow to CI trigger paths - Add prompt for follow-up PostgreSQL migration test PR Co-Authored-By: Claude Opus 4.6 (1M context) * add engine check on downgrade * [autofix.ci] apply automated fixes * fix: harden CI error handling and test robustness - Set validationPassed=false when validator crashes so CI fails instead of passing silently - Wrap GitHub API calls in try-catch so comment-posting failures don't mask validation results - Preserve git stderr in warnings for better CI debugging - Add defensive handling for unexpected FK constraint shapes in SQLite noise filter - Clean up SQLite WAL/SHM/journal companion files in test teardown * Add explicit fetch to main * ruff * [autofix.ci] apply automated fixes * Add sqlite filter tests and remove redundant fetch --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/migration-validation.yml | 120 +++--- .../base/langflow/initial_setup/setup.py | 7 +- .../unit/alembic/test_migration_execution.py | 358 ++++++++++++++---- src/backend/tests/unit/api/v1/test_mcp.py | 22 +- 4 files changed, 387 insertions(+), 120 deletions(-) diff --git a/.github/workflows/migration-validation.yml b/.github/workflows/migration-validation.yml index db7215f82988..a129baf7dfa0 100644 --- a/.github/workflows/migration-validation.yml +++ b/.github/workflows/migration-validation.yml @@ -4,10 +4,13 @@ on: pull_request: paths: - 'src/backend/base/langflow/alembic/versions/*.py' - - 'alembic/versions/*.py' + - 'src/backend/base/langflow/services/database/models/**/*.py' + - 'src/backend/tests/unit/alembic/test_migration_execution.py' + - '.github/workflows/migration-validation.yml' jobs: - validate-migration: + model-migration-consistency: + name: Model/Migration Consistency runs-on: ubuntu-latest steps: @@ -16,50 +19,72 @@ jobs: with: fetch-depth: 0 + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Setup Python uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.12' - name: Install dependencies run: | - pip install sqlalchemy alembic + uv sync - - name: Get changed migration files - id: changed-files + - name: Check model/migration consistency + env: + MIGRATION_VALIDATION_CI: "true" run: | - # Get all changed Python files in alembic/versions directories + uv run pytest src/backend/tests/unit/alembic/test_migration_execution.py -x -v - # CHANGED_FILES=$(git diff --name-only origin/main...HEAD | grep -E '(alembic|migrations)/versions/.*\.py$' || echo "") + validate-migration: + name: Migration Pattern Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 - # Exclude test migrations, as they are not part of the main codebase - CHANGED_FILES=$(git diff --name-only origin/main...HEAD | grep -E '(alembic|migrations)/versions/.*\.py$' | grep -v 'test_migrations/' || echo "") + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Get changed migration files + id: changed-files + run: | + set -euo pipefail + CHANGED_FILES=$(git diff --name-only origin/main...HEAD | grep -E 'src/backend/base/langflow/alembic/versions/.*\.py$' | grep -v 'test_migrations/' || echo "") if [ -z "$CHANGED_FILES" ]; then echo "No migration files changed" - echo "files=" >> $GITHUB_OUTPUT + echo "files=" >> "$GITHUB_OUTPUT" else echo "Changed migration files:" echo "$CHANGED_FILES" - # Convert newlines to spaces for passing as arguments - echo "files=$(echo $CHANGED_FILES | tr '\n' ' ')" >> $GITHUB_OUTPUT + echo "files=$(printf '%s' "$CHANGED_FILES" | tr '\n' ' ')" >> "$GITHUB_OUTPUT" fi - - name: Validate migrations + - name: Validate migration patterns if: steps.changed-files.outputs.files != '' + env: + MIGRATION_FILES: ${{ steps.changed-files.outputs.files }} run: | - python src/backend/base/langflow/alembic/migration_validator.py ${{ steps.changed-files.outputs.files }} - -# - name: Check migration phase sequence -# if: steps.changed-files.outputs.files != '' -# run: | -# python scripts/check_phase_sequence.py ${{ steps.changed-files.outputs.files }} + python src/backend/base/langflow/alembic/migration_validator.py $MIGRATION_FILES - name: Generate validation report if: always() && steps.changed-files.outputs.files != '' + env: + MIGRATION_FILES: ${{ steps.changed-files.outputs.files }} run: | python src/backend/base/langflow/alembic/migration_validator.py \ - --json ${{ steps.changed-files.outputs.files }} > validation-report.json || true + --json $MIGRATION_FILES > validation-report.json 2> validation-stderr.txt || true + if [ ! -s validation-report.json ]; then + echo "::error::Validator produced no output. Stderr:" + cat validation-stderr.txt + fi - name: Post PR comment with results if: always() && steps.changed-files.outputs.files != '' @@ -110,7 +135,7 @@ jobs: } } - message += `### 📚 Resources\n`; + message += `### Resources\n`; message += `- Review the [DB Migration Guide](./src/backend/base/langflow/alembic/DB-MIGRATION-GUIDE.MD)\n`; message += `- Use \`python scripts/generate_migration.py --help\` to generate compliant migrations\n\n`; @@ -123,37 +148,42 @@ jobs: } catch (error) { message = `⚠️ **Migration validation check failed to run properly**\n`; message += `Error: ${error.message}\n`; + validationPassed = false; } - // Post or update comment - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('Migration Validation') - ); - - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: message - }); - } else { - await github.rest.issues.createComment({ + // Post or update comment (non-critical — don't let API errors mask validation results) + try { + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, - body: message }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Migration Validation') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: message + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: message + }); + } + } catch (apiError) { + core.warning(`Failed to post PR comment: ${apiError.message}`); } // Fail the workflow if validation didn't pass if (!validationPassed) { core.setFailed('Migration validation failed'); - } \ No newline at end of file + } diff --git a/src/backend/base/langflow/initial_setup/setup.py b/src/backend/base/langflow/initial_setup/setup.py index 1d91ea3c5bca..d3dcbc6e5d7b 100644 --- a/src/backend/base/langflow/initial_setup/setup.py +++ b/src/backend/base/langflow/initial_setup/setup.py @@ -62,10 +62,9 @@ def update_projects_components_with_latest_component_versions(project_data, all_types_dict): # Flatten the all_types_dict for easy access - all_types_dict_flat = {} - for category in all_types_dict.values(): - for key, component in category.items(): - all_types_dict_flat[key] = component + all_types_dict_flat = { + key: component for category in all_types_dict.values() for key, component in category.items() + } node_changes_log = defaultdict(list) project_data_copy = deepcopy(project_data) diff --git a/src/backend/tests/unit/alembic/test_migration_execution.py b/src/backend/tests/unit/alembic/test_migration_execution.py index e09c4f2e1161..3a9396a0527e 100644 --- a/src/backend/tests/unit/alembic/test_migration_execution.py +++ b/src/backend/tests/unit/alembic/test_migration_execution.py @@ -1,3 +1,7 @@ +import errno +import os +import shutil +import subprocess import tempfile from pathlib import Path @@ -7,14 +11,15 @@ from alembic.config import Config from alembic.migration import MigrationContext from langflow.services.database.service import SQLModel -from sqlalchemy import create_engine, inspect +from sqlalchemy import create_engine + +_WORKSPACE_ROOT = Path(__file__).resolve().parents[5] def _get_alembic_cfg(db_path: str) -> Config: """Create an Alembic Config pointing at the project's migration scripts.""" alembic_cfg = Config() - workspace_root = Path(__file__).resolve().parents[5] - script_location = workspace_root / "src/backend/base/langflow/alembic" + script_location = _WORKSPACE_ROOT / "src/backend/base/langflow/alembic" if not script_location.exists(): pytest.fail(f"Alembic script location not found at {script_location}") @@ -24,65 +29,229 @@ def _get_alembic_cfg(db_path: str) -> Config: return alembic_cfg -EXPECTED_TABLES = { - "user", - "flow", - "folder", - "apikey", - "variable", - "file", - "message", - "transaction", - "vertex_build", - "job", - "trace", -} - -EXPECTED_COLUMNS = { - "user": {"id", "username", "password"}, - "flow": {"id", "name", "user_id", "folder_id", "data"}, - "folder": {"id", "name"}, - "message": {"id", "text", "flow_id", "session_id"}, -} - -EXPECTED_FOREIGN_KEYS = { - "flow": {"user.id", "folder.id"}, -} - - -def test_migrated_schema_has_expected_tables(): - """Migrate a fresh DB to head and verify the schema is correct.""" - with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: - db_path = tmp.name +def _get_main_branch_head() -> str | None: + """Get the alembic head revision that origin/main is at. + + Finds migration files new on this branch (not on origin/main), then looks up + their down_revision to determine where main's DB would be. Uses the current + branch's alembic ScriptDirectory since it already contains all migrations. + + Returns None if git operations fail (e.g. shallow clone without origin/main). + """ + from alembic.script import ScriptDirectory + git = shutil.which("git") + if git is None: + return None + + # Find migration files that are new on this branch vs origin/main try: - alembic_cfg = _get_alembic_cfg(db_path) - command.upgrade(alembic_cfg, "head") + result = subprocess.run( # noqa: S603 + [ + git, + "diff", + "--name-only", + "--diff-filter=A", + "origin/main...HEAD", + "--", + "src/backend/base/langflow/alembic/versions/*.py", + ], + capture_output=True, + text=True, + check=True, + cwd=_WORKSPACE_ROOT, + ) + except subprocess.CalledProcessError as exc: + import warnings - engine = create_engine(f"sqlite:///{db_path}") - try: - insp = inspect(engine) - actual_tables = set(insp.get_table_names()) - - missing_tables = EXPECTED_TABLES - actual_tables - assert not missing_tables, f"Missing tables after migration: {missing_tables}" - - for table, expected_cols in EXPECTED_COLUMNS.items(): - actual_cols = {col["name"] for col in insp.get_columns(table)} - missing_cols = expected_cols - actual_cols - assert not missing_cols, f"Table '{table}' missing columns: {missing_cols}" - - for table, expected_fk_targets in EXPECTED_FOREIGN_KEYS.items(): - fks = insp.get_foreign_keys(table) - actual_fk_targets = { - f"{fk['referred_table']}.{fk['referred_columns'][0]}" for fk in fks if fk["referred_columns"] - } - missing_fks = expected_fk_targets - actual_fk_targets - assert not missing_fks, f"Table '{table}' missing foreign keys to: {missing_fks}" - finally: - engine.dispose() - finally: - Path(db_path).unlink(missing_ok=True) + warnings.warn(f"git diff failed (rc={exc.returncode}): {exc.stderr.strip()}", stacklevel=2) + return None + except OSError as exc: + if exc.errno == errno.ENOENT: + return None # git binary not found at resolved path + raise # unexpected OS error (disk full, permissions, etc.) + + new_files = [f for f in result.stdout.strip().splitlines() if f.endswith(".py")] + + alembic_cfg = Config() + script_location = _WORKSPACE_ROOT / "src/backend/base/langflow/alembic" + alembic_cfg.set_main_option("script_location", str(script_location)) + script = ScriptDirectory.from_config(alembic_cfg) + + if not new_files: + # No new migrations on this branch — head is same as main + heads = script.get_heads() + if len(heads) == 1: + return heads[0] + if len(heads) > 1: + pytest.fail(f"Alembic has {len(heads)} head revisions — migration branches need merging: {heads}") + return None + + # Collect revision IDs of all new migrations + new_rev_ids = set() + for fpath in new_files: + filename = Path(fpath).name + new_rev_ids.add(filename.split("_", 1)[0]) + + # Find down_revisions that point outside the new migrations (i.e. into main) + main_revisions = set() + for rev_id in new_rev_ids: + rev_script = script.get_revision(rev_id) + if rev_script is None: + msg = ( + f"New migration file matched revision ID '{rev_id}' " + f"but Alembic has no such revision — check filename convention" + ) + raise ValueError(msg) + if rev_script.down_revision is None: + msg = f"New migration {rev_id} has down_revision=None — it must chain from an existing migration" + raise ValueError(msg) + down = rev_script.down_revision + downs = set(down) if isinstance(down, (tuple, list)) else {down} + # Only keep down_revisions that are NOT themselves new migrations + main_revisions.update(downs - new_rev_ids) + + if len(main_revisions) > 1: + pytest.fail( + f"New migrations descend from {len(main_revisions)} different base revisions — " + f"they must share a single parent on main: {main_revisions}" + ) + if not main_revisions: + pytest.fail( + f"New migrations {new_rev_ids} form a disconnected chain — " + f"none of their down_revisions point to an existing migration on main" + ) + return main_revisions.pop() + + +def _filter_sqlite_noise(diffs: list) -> list: + """Filter out diffs that are known SQLite limitations. + + - modify_nullable: SQLite doesn't support ALTER COLUMN + - remove_fk/add_fk: SQLite doesn't track FK constraint names or actions + (ondelete/onupdate), so autogenerate sees phantom FK diffs. Paired + remove/add on the same (table, columns) with identical referenced targets + are suppressed; unpaired or re-targeted FKs are preserved. + """ + significant_diffs = [] + fk_removes: dict = {} # (table, col_tuple) -> ForeignKeyConstraint + fk_adds: dict = {} + + for d in diffs: + if not (isinstance(d, tuple) and len(d) >= 2): + significant_diffs.append(d) + continue + + op_type = d[0] + if op_type == "modify_nullable": + continue + if op_type in ("remove_fk", "add_fk"): + fk = d[1] + try: + key = (fk.parent.name, tuple(sorted(c.name for c in fk.columns))) + except (AttributeError, TypeError): + significant_diffs.append(d) + continue + if op_type == "remove_fk": + fk_removes[key] = fk + else: + fk_adds[key] = fk + continue + + significant_diffs.append(d) + + # Compare FK remove/add pairs: suppress when referenced targets match. + # We intentionally skip ondelete/onupdate comparison because SQLite's + # PRAGMA foreign_key_list does not reliably report these actions. + all_fk_keys = set(fk_removes) | set(fk_adds) + for key in all_fk_keys: + rm = fk_removes.get(key) + add = fk_adds.get(key) + if rm and add: + rm_targets = sorted( + (elem.column.table.name, elem.column.name) for elem in rm.elements if elem.column is not None + ) + add_targets = sorted( + (elem.column.table.name, elem.column.name) for elem in add.elements if elem.column is not None + ) + if rm_targets and rm_targets == add_targets: + continue # Same target — name-only or action-only diff is SQLite noise + if rm: + significant_diffs.append(("remove_fk", rm)) + if add: + significant_diffs.append(("add_fk", add)) + + return significant_diffs + + +class _FakeColumn: + """Minimal stand-in for sqlalchemy Column used by FK constraint diffs.""" + + def __init__(self, name, table_name): + self.name = name + self.table = type("T", (), {"name": table_name})() + + +class _FakeElement: + """Minimal stand-in for FK constraint element with a .column attribute.""" + + def __init__(self, column): + self.column = column + + +class _FakeFK: + """Minimal stand-in for ForeignKeyConstraint used by autogenerate diffs.""" + + def __init__(self, parent_table, col_names, ref_table, ref_col_names): + self.parent = type("T", (), {"name": parent_table})() + self.columns = [_FakeColumn(c, parent_table) for c in col_names] + self.elements = [_FakeElement(_FakeColumn(rc, ref_table)) for rc in ref_col_names] + + +class TestFilterSqliteNoise: + """Tests for _filter_sqlite_noise FK pair suppression logic.""" + + def test_paired_fk_same_target_suppressed(self): + """Paired remove_fk/add_fk on same columns with same target is noise.""" + fk_rm = _FakeFK("flow", ["user_id"], "user", ["id"]) + fk_add = _FakeFK("flow", ["user_id"], "user", ["id"]) + diffs = [("remove_fk", fk_rm), ("add_fk", fk_add)] + assert _filter_sqlite_noise(diffs) == [] + + def test_unpaired_remove_fk_preserved(self): + """A remove_fk without matching add_fk is kept.""" + fk_rm = _FakeFK("flow", ["user_id"], "user", ["id"]) + diffs = [("remove_fk", fk_rm)] + result = _filter_sqlite_noise(diffs) + assert len(result) == 1 + assert result[0][0] == "remove_fk" + + def test_unpaired_add_fk_preserved(self): + """An add_fk without matching remove_fk is kept.""" + fk_add = _FakeFK("flow", ["user_id"], "user", ["id"]) + diffs = [("add_fk", fk_add)] + result = _filter_sqlite_noise(diffs) + assert len(result) == 1 + assert result[0][0] == "add_fk" + + def test_paired_fk_different_target_preserved(self): + """Paired remove_fk/add_fk pointing to different tables is a real change.""" + fk_rm = _FakeFK("flow", ["user_id"], "user", ["id"]) + fk_add = _FakeFK("flow", ["user_id"], "account", ["id"]) + diffs = [("remove_fk", fk_rm), ("add_fk", fk_add)] + result = _filter_sqlite_noise(diffs) + assert len(result) == 2 + + def test_modify_nullable_always_suppressed(self): + """modify_nullable diffs are always SQLite noise.""" + diffs = [("modify_nullable", "some_table", "some_col", {}, None, True)] + assert _filter_sqlite_noise(diffs) == [] + + def test_non_fk_diffs_preserved(self): + """Non-FK diffs like add_column pass through unchanged.""" + diffs = [("add_column", "table", "col")] + result = _filter_sqlite_noise(diffs) + assert result == diffs def test_no_phantom_migrations(): @@ -108,12 +277,7 @@ def test_no_phantom_migrations(): finally: engine.dispose() - # Filter out diffs that are known SQLite limitations: - # - modify_nullable: SQLite doesn't support ALTER COLUMN - # - remove_fk/add_fk: SQLite doesn't track FK names or ondelete, so - # autogenerate sees phantom FK changes - _sqlite_noise = {"modify_nullable", "remove_fk", "add_fk"} - significant_diffs = [d for d in diffs if not (isinstance(d, tuple) and len(d) >= 2 and d[0] in _sqlite_noise)] + significant_diffs = _filter_sqlite_noise(diffs) if significant_diffs: diff_descriptions = "\n".join(str(d) for d in significant_diffs) @@ -123,4 +287,66 @@ def test_no_phantom_migrations(): f"how column metadata is generated.\n\nDiffs:\n{diff_descriptions}" ) finally: - Path(db_path).unlink(missing_ok=True) + for suffix in ("", "-wal", "-shm", "-journal"): + Path(db_path + suffix).unlink(missing_ok=True) + + +def test_upgrade_from_main_branch(): + """Verify that a DB at main's head can upgrade to current head and downgrade back. + + This catches the real-world scenario: a user running on main (or the latest release) + upgrades to a branch with new migrations. The upgrade must succeed, the resulting + schema must match the models, and downgrade back to main must also succeed. + """ + main_head = _get_main_branch_head() + if main_head is None: + if os.environ.get("MIGRATION_VALIDATION_CI"): + pytest.fail("Could not determine main branch head revision — ensure fetch-depth: 0 and origin/main exists") + pytest.skip("Could not determine main branch head revision (shallow clone or no origin/main)") + + with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: + db_path = tmp.name + + try: + alembic_cfg = _get_alembic_cfg(db_path) + + # Step 1: Create DB at main's head revision (simulates existing user DB) + command.upgrade(alembic_cfg, main_head) + + # Step 2: Upgrade to the current branch head + command.upgrade(alembic_cfg, "head") + + # Step 3: Verify models match the migrated DB + engine = create_engine(f"sqlite:///{db_path}") + try: + with engine.connect() as connection: + migration_context = MigrationContext.configure(connection) + diffs = compare_metadata(migration_context, SQLModel.metadata) + finally: + engine.dispose() + + significant_diffs = _filter_sqlite_noise(diffs) + + if significant_diffs: + diff_descriptions = "\n".join(str(d) for d in significant_diffs) + pytest.fail( + f"After upgrading from main ({main_head}) to head, " + f"autogenerate detected {len(significant_diffs)} schema mismatch(es).\n\n" + f"Diffs:\n{diff_descriptions}" + ) + + # Step 4: Downgrade back to main's head to verify rollback works + command.downgrade(alembic_cfg, main_head) + + # Step 5: Verify the DB is actually at main's revision after downgrade + engine = create_engine(f"sqlite:///{db_path}") + try: + with engine.connect() as connection: + ctx = MigrationContext.configure(connection) + current_rev = ctx.get_current_revision() + assert current_rev == main_head, f"After downgrade, expected revision {main_head} but got {current_rev}" + finally: + engine.dispose() + finally: + for suffix in ("", "-wal", "-shm", "-journal"): + Path(db_path + suffix).unlink(missing_ok=True) diff --git a/src/backend/tests/unit/api/v1/test_mcp.py b/src/backend/tests/unit/api/v1/test_mcp.py index eb7663d44c20..6fd84d89ecc6 100644 --- a/src/backend/tests/unit/api/v1/test_mcp.py +++ b/src/backend/tests/unit/api/v1/test_mcp.py @@ -5,7 +5,6 @@ import pytest from fastapi import HTTPException, status from httpx import AsyncClient -from langflow.services.auth.utils import get_password_hash from langflow.services.database.models.user import User # Mark all tests in this module as asyncio @@ -15,7 +14,11 @@ @pytest.fixture def mock_user(): return User( - id=uuid4(), username="testuser", password=get_password_hash("testpassword"), is_active=True, is_superuser=False + id=uuid4(), + username="testuser", + password="fake-hashed-password", # noqa: S106 + is_active=True, + is_superuser=False, ) @@ -285,7 +288,10 @@ async def test_streamable_http_start_stop_lifecycle(): manager_instance.run.return_value = _DummyRunContext(entered, exited) with ( - patch("langflow.api.v1.mcp.StreamableHTTPSessionManager", return_value=manager_instance), + patch( + "langflow.api.v1.mcp.StreamableHTTPSessionManager", + return_value=manager_instance, + ), patch("langflow.api.v1.mcp.logger.adebug", new_callable=AsyncMock), ): streamable_http = StreamableHTTP() @@ -306,7 +312,10 @@ async def test_streamable_http_start_failure_keeps_manager_unavailable(): manager_instance.run.return_value = _FailingRunContext(failure) with ( - patch("langflow.api.v1.mcp.StreamableHTTPSessionManager", return_value=manager_instance), + patch( + "langflow.api.v1.mcp.StreamableHTTPSessionManager", + return_value=manager_instance, + ), patch("langflow.api.v1.mcp.logger.adebug", new_callable=AsyncMock), patch("langflow.api.v1.mcp.logger.aexception", new_callable=AsyncMock), ): @@ -328,7 +337,10 @@ async def test_streamable_http_start_failure_surfaces_exception_once(): async_logger = AsyncMock() with ( - patch("langflow.api.v1.mcp.StreamableHTTPSessionManager", return_value=manager_instance), + patch( + "langflow.api.v1.mcp.StreamableHTTPSessionManager", + return_value=manager_instance, + ), patch("langflow.api.v1.mcp.logger.aexception", new=async_logger), ): streamable_http = StreamableHTTP() From 3811db4d24bac9fbd1700dcebb0798ab30925fba Mon Sep 17 00:00:00 2001 From: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:55:38 -0400 Subject: [PATCH 23/29] feat(deployments): unify payload passthrough from api to adapter (#12190) * feat(deployments): unify dynamic payload passthrough across api and adapter * use datatime.timezone for python3.10 compatibility * use appropriate type vars in slots and sanitize error message * tweaks to schemas * use policy to avoid dump churn --- .secrets.baseline | 4 +- .../base/langflow/api/v1/mappers/__init__.py | 1 + .../api/v1/mappers/deployments/__init__.py | 12 + .../api/v1/mappers/deployments/base.py | 121 +++++++++ .../api/v1/test_deployment_mapper_base.py | 209 +++++++++++++++ .../unit/api/v1/test_deployment_schemas.py | 27 ++ src/lfx/PLUGGABLE_SERVICES.md | 22 ++ src/lfx/src/lfx/services/adapters/__init__.py | 16 ++ .../services/adapters/deployment/__init__.py | 42 +++ .../lfx/services/adapters/deployment/base.py | 6 +- .../services/adapters/deployment/payloads.py | 108 ++++++++ .../services/adapters/deployment/schema.py | 103 +++++--- .../services/adapters/deployment/service.py | 1 - src/lfx/src/lfx/services/adapters/payload.py | 91 +++++++ src/lfx/src/lfx/services/interfaces.py | 6 +- .../deployment/test_deployment_schema.py | 47 +++- .../deployment/test_payload_formalization.py | 239 ++++++++++++++++++ 17 files changed, 1012 insertions(+), 43 deletions(-) create mode 100644 src/backend/base/langflow/api/v1/mappers/__init__.py create mode 100644 src/backend/base/langflow/api/v1/mappers/deployments/__init__.py create mode 100644 src/backend/base/langflow/api/v1/mappers/deployments/base.py create mode 100644 src/backend/tests/unit/api/v1/test_deployment_mapper_base.py create mode 100644 src/lfx/src/lfx/services/adapters/deployment/payloads.py create mode 100644 src/lfx/src/lfx/services/adapters/payload.py create mode 100644 src/lfx/tests/unit/services/deployment/test_payload_formalization.py diff --git a/.secrets.baseline b/.secrets.baseline index 5fdc03d32d6d..a01cbe251607 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -2517,7 +2517,7 @@ "filename": "src/backend/tests/unit/api/v1/test_deployment_schemas.py", "hashed_secret": "99091d046a81493ef2545d8c3cd8e881e8702893", "is_verified": false, - "line_number": 49, + "line_number": 51, "is_secret": false }, { @@ -2525,7 +2525,7 @@ "filename": "src/backend/tests/unit/api/v1/test_deployment_schemas.py", "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", "is_verified": false, - "line_number": 71, + "line_number": 73, "is_secret": false } ], diff --git a/src/backend/base/langflow/api/v1/mappers/__init__.py b/src/backend/base/langflow/api/v1/mappers/__init__.py new file mode 100644 index 000000000000..016a215c2a5f --- /dev/null +++ b/src/backend/base/langflow/api/v1/mappers/__init__.py @@ -0,0 +1 @@ +"""API payload mappers.""" diff --git a/src/backend/base/langflow/api/v1/mappers/deployments/__init__.py b/src/backend/base/langflow/api/v1/mappers/deployments/__init__.py new file mode 100644 index 000000000000..8fadcfa95512 --- /dev/null +++ b/src/backend/base/langflow/api/v1/mappers/deployments/__init__.py @@ -0,0 +1,12 @@ +"""Deployment API mapper registry and base contracts.""" + +from .base import BaseDeploymentMapper, DeploymentApiPayloads, DeploymentMapperRegistry + +deployment_mapper_registry = DeploymentMapperRegistry() + +__all__ = [ + "BaseDeploymentMapper", + "DeploymentApiPayloads", + "DeploymentMapperRegistry", + "deployment_mapper_registry", +] diff --git a/src/backend/base/langflow/api/v1/mappers/deployments/base.py b/src/backend/base/langflow/api/v1/mappers/deployments/base.py new file mode 100644 index 000000000000..88f81377b0c7 --- /dev/null +++ b/src/backend/base/langflow/api/v1/mappers/deployments/base.py @@ -0,0 +1,121 @@ +# ruff: noqa: ARG002 +"""Base deployment payload mapper contracts for API <-> adapter transforms.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, ClassVar + +from lfx.services.adapters.deployment.payloads import DeploymentPayloadFields +from lfx.services.adapters.payload import PayloadSlot + +if TYPE_CHECKING: + from sqlalchemy.ext.asyncio import AsyncSession + + +@dataclass(frozen=True) +class DeploymentApiPayloads(DeploymentPayloadFields): + """API-side payload schema registry for deployment providers. + + Ownership boundary: + Langflow owns API slot population here because API payloads may include + Langflow-specific references and reshaping requirements. Adapter-side + slot population is defined separately via ``DeploymentPayloadSchemas``. + """ + + +class BaseDeploymentMapper: + """Per-provider mapper for deployment API payloads. + + The base implementation is intentionally passthrough-first: + inbound ``resolve_*`` methods return the original dict unless an + API payload slot is configured for that field, in which case the + slot ``apply`` policy is used. + Outbound ``shape_*`` methods return provider payloads unchanged, including + the operation-specific result shapers: + ``shape_deployment_create_result``, ``shape_deployment_operation_result``, + ``shape_deployment_list_result``, ``shape_config_list_result``, and + ``shape_snapshot_list_result``. + + Provider-specific mappers override only the methods that need + Langflow-aware resolution or payload reshaping. + """ + + api_payloads: ClassVar[DeploymentApiPayloads] = DeploymentApiPayloads() + + async def resolve_deployment_spec(self, raw: dict[str, Any] | None, db: AsyncSession) -> dict[str, Any] | None: + return self._validate_slot(self.api_payloads.deployment_spec, raw) + + async def resolve_deployment_config(self, raw: dict[str, Any] | None, db: AsyncSession) -> dict[str, Any] | None: + return self._validate_slot(self.api_payloads.deployment_config, raw) + + async def resolve_deployment_update(self, raw: dict[str, Any] | None, db: AsyncSession) -> dict[str, Any] | None: + return self._validate_slot(self.api_payloads.deployment_update, raw) + + async def resolve_execution_input(self, raw: dict[str, Any] | None, db: AsyncSession) -> dict[str, Any] | None: + return self._validate_slot(self.api_payloads.execution_input, raw) + + async def resolve_deployment_list_params( + self, raw: dict[str, Any] | None, db: AsyncSession + ) -> dict[str, Any] | None: + return self._validate_slot(self.api_payloads.deployment_list_params, raw) + + async def resolve_config_list_params(self, raw: dict[str, Any] | None, db: AsyncSession) -> dict[str, Any] | None: + return self._validate_slot(self.api_payloads.config_list_params, raw) + + async def resolve_snapshot_list_params(self, raw: dict[str, Any] | None, db: AsyncSession) -> dict[str, Any] | None: + return self._validate_slot(self.api_payloads.snapshot_list_params, raw) + + def shape_deployment_create_result(self, provider_result: dict[str, Any] | None) -> dict[str, Any] | None: + return provider_result + + def shape_deployment_operation_result(self, provider_result: dict[str, Any] | None) -> dict[str, Any] | None: + return provider_result + + def shape_deployment_list_result(self, provider_result: dict[str, Any] | None) -> dict[str, Any] | None: + return provider_result + + def shape_config_list_result(self, provider_result: dict[str, Any] | None) -> dict[str, Any] | None: + return provider_result + + def shape_snapshot_list_result(self, provider_result: dict[str, Any] | None) -> dict[str, Any] | None: + return provider_result + + def shape_execution_result(self, provider_result: dict[str, Any] | None) -> dict[str, Any] | None: + return provider_result + + def shape_deployment_item_data(self, provider_data: dict[str, Any] | None) -> dict[str, Any] | None: + return provider_data + + def shape_deployment_status_data(self, provider_data: dict[str, Any] | None) -> dict[str, Any] | None: + return provider_data + + @staticmethod + def _validate_slot( + slot: PayloadSlot[Any] | None, + raw: dict[str, Any] | None, + ) -> dict[str, Any] | None: + """Validate a payload dict against a configured API slot.""" + if raw is None or slot is None: + return raw + return slot.apply(raw) + + +class DeploymentMapperRegistry: + """Registry of per-provider deployment mappers.""" + + _default: BaseDeploymentMapper + _mappers: dict[str, BaseDeploymentMapper] + + def __init__(self) -> None: + self._default = BaseDeploymentMapper() + self._mappers = {} + + def register(self, provider_key: str, mapper: BaseDeploymentMapper) -> None: + if not isinstance(mapper, BaseDeploymentMapper): + msg = "mapper must be an instance of BaseDeploymentMapper" + raise TypeError(msg) + self._mappers[provider_key] = mapper + + def get(self, provider_key: str) -> BaseDeploymentMapper: + return self._mappers.get(provider_key, self._default) diff --git a/src/backend/tests/unit/api/v1/test_deployment_mapper_base.py b/src/backend/tests/unit/api/v1/test_deployment_mapper_base.py new file mode 100644 index 000000000000..2ca819f9dea9 --- /dev/null +++ b/src/backend/tests/unit/api/v1/test_deployment_mapper_base.py @@ -0,0 +1,209 @@ +"""Tests for deployment API mapper base contracts.""" + +from __future__ import annotations + +from dataclasses import fields + +import pytest +from langflow.api.v1.mappers.deployments.base import ( + BaseDeploymentMapper, + DeploymentApiPayloads, + DeploymentMapperRegistry, +) +from lfx.services.adapters.deployment.payloads import DeploymentPayloadSchemas +from lfx.services.adapters.payload import AdapterPayloadValidationError, PayloadSlot, PayloadSlotPolicy +from pydantic import BaseModel + + +class _ApiSpec(BaseModel): + region: str + + +class _ApiConfig(BaseModel): + retries: int + + +class _ApiUpdate(BaseModel): + patch: str + + +class _ApiExecutionInput(BaseModel): + invocation_id: str + + +class _ApiDeploymentListParams(BaseModel): + env: str + + +class _ApiConfigListParams(BaseModel): + config_tag: str + + +class _ApiSnapshotListParams(BaseModel): + snapshot_tag: str + + +class _TypedMapper(BaseDeploymentMapper): + api_payloads = DeploymentApiPayloads( + deployment_spec=PayloadSlot(_ApiSpec, policy=PayloadSlotPolicy.VALIDATE_ONLY), + deployment_config=PayloadSlot(_ApiConfig, policy=PayloadSlotPolicy.VALIDATE_ONLY), + deployment_update=PayloadSlot(_ApiUpdate, policy=PayloadSlotPolicy.VALIDATE_ONLY), + execution_input=PayloadSlot(_ApiExecutionInput, policy=PayloadSlotPolicy.VALIDATE_ONLY), + deployment_list_params=PayloadSlot(_ApiDeploymentListParams, policy=PayloadSlotPolicy.VALIDATE_ONLY), + config_list_params=PayloadSlot(_ApiConfigListParams, policy=PayloadSlotPolicy.VALIDATE_ONLY), + snapshot_list_params=PayloadSlot(_ApiSnapshotListParams, policy=PayloadSlotPolicy.VALIDATE_ONLY), + ) + + +class _NormalizingMapper(BaseDeploymentMapper): + api_payloads = DeploymentApiPayloads( + deployment_config=PayloadSlot(_ApiConfig, policy=PayloadSlotPolicy.VALIDATE_AND_DUMP), + ) + + +INBOUND_METHOD_CASES = [ + ("resolve_deployment_spec", {"region": "us-east-1"}), + ("resolve_deployment_config", {"retries": 3}), + ("resolve_deployment_update", {"patch": "replace"}), + ("resolve_execution_input", {"invocation_id": "inv-1"}), + ("resolve_deployment_list_params", {"env": "prod"}), + ("resolve_config_list_params", {"config_tag": "release"}), + ("resolve_snapshot_list_params", {"snapshot_tag": "nightly"}), +] + +INBOUND_SLOT_NAMES = [ + "deployment_spec", + "deployment_config", + "deployment_update", + "execution_input", + "deployment_list_params", + "config_list_params", + "snapshot_list_params", +] + +OUTBOUND_SLOT_NAMES = [ + "deployment_create_result", + "deployment_operation_result", + "deployment_list_result", + "config_list_result", + "snapshot_list_result", + "execution_result", + "deployment_item_data", + "deployment_status_data", +] + + +def test_api_payload_field_names_match_adapter_registry() -> None: + api_fields = [field.name for field in fields(DeploymentApiPayloads)] + adapter_fields = [field.name for field in fields(DeploymentPayloadSchemas)] + assert api_fields == adapter_fields + + +@pytest.mark.asyncio +@pytest.mark.parametrize(("method_name", "payload"), INBOUND_METHOD_CASES) +async def test_base_mapper_resolvers_passthrough_when_slot_not_configured( + method_name: str, payload: dict[str, str | int] +) -> None: + mapper = BaseDeploymentMapper() + resolver = getattr(mapper, method_name) + resolved = await resolver(payload, db=None) # type: ignore[arg-type] + assert resolved == payload + + +@pytest.mark.asyncio +@pytest.mark.parametrize(("method_name", "payload"), INBOUND_METHOD_CASES) +async def test_base_mapper_resolvers_validate_configured_slots_without_re_serializing( + method_name: str, payload: dict[str, str | int] +) -> None: + mapper = _TypedMapper() + resolver = getattr(mapper, method_name) + resolved = await resolver(payload, db=None) # type: ignore[arg-type] + assert resolved == payload + assert resolved is payload + + +@pytest.mark.asyncio +async def test_base_mapper_resolvers_apply_normalize_policy_when_slot_configured() -> None: + mapper = _NormalizingMapper() + payload: dict[str, str | int] = {"retries": "3"} + + resolved = await mapper.resolve_deployment_config(payload, db=None) # type: ignore[arg-type] + + assert resolved == {"retries": 3} + assert resolved is not payload + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method_name", [name for name, _ in INBOUND_METHOD_CASES]) +async def test_base_mapper_resolvers_reject_invalid_payload_for_configured_slots(method_name: str) -> None: + mapper = _TypedMapper() + resolver = getattr(mapper, method_name) + with pytest.raises(AdapterPayloadValidationError, match="Invalid payload"): + await resolver({"unknown": "field"}, db=None) # type: ignore[arg-type] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("method_name", [name for name, _ in INBOUND_METHOD_CASES]) +async def test_base_mapper_resolvers_passthrough_none_payload_when_slot_configured(method_name: str) -> None: + mapper = _TypedMapper() + resolver = getattr(mapper, method_name) + resolved = await resolver(None, db=None) # type: ignore[arg-type] + assert resolved is None + + +def test_mapper_has_resolve_method_for_all_inbound_slots() -> None: + for slot_name in INBOUND_SLOT_NAMES: + assert hasattr(BaseDeploymentMapper, f"resolve_{slot_name}") + + +def test_mapper_has_shape_method_for_all_outbound_slots() -> None: + for slot_name in OUTBOUND_SLOT_NAMES: + assert hasattr(BaseDeploymentMapper, f"shape_{slot_name}") + + +@pytest.mark.parametrize( + "method_name", + [ + "shape_deployment_create_result", + "shape_deployment_operation_result", + "shape_deployment_list_result", + "shape_config_list_result", + "shape_snapshot_list_result", + "shape_execution_result", + "shape_deployment_item_data", + "shape_deployment_status_data", + ], +) +def test_base_mapper_shapers_passthrough_provider_payload(method_name: str) -> None: + mapper = BaseDeploymentMapper() + payload = {"ok": True} + shaper = getattr(mapper, method_name) + shaped = shaper(payload) + assert shaped is payload + + +def test_mapper_registry_returns_default_when_unregistered() -> None: + registry = DeploymentMapperRegistry() + assert isinstance(registry.get("missing-provider"), BaseDeploymentMapper) + + +def test_mapper_registry_returns_registered_mapper() -> None: + registry = DeploymentMapperRegistry() + mapper = _TypedMapper() + registry.register("acme", mapper) + assert registry.get("acme") is mapper + + +def test_mapper_registry_re_register_same_key_replaces_mapper() -> None: + registry = DeploymentMapperRegistry() + mapper1 = _TypedMapper() + mapper2 = BaseDeploymentMapper() + registry.register("acme", mapper1) + registry.register("acme", mapper2) + assert registry.get("acme") is mapper2 + + +def test_mapper_registry_rejects_non_mapper_instances() -> None: + registry = DeploymentMapperRegistry() + with pytest.raises(TypeError, match="BaseDeploymentMapper"): + registry.register("bad", object()) # type: ignore[arg-type] diff --git a/src/backend/tests/unit/api/v1/test_deployment_schemas.py b/src/backend/tests/unit/api/v1/test_deployment_schemas.py index b77351677255..72ecb26cb204 100644 --- a/src/backend/tests/unit/api/v1/test_deployment_schemas.py +++ b/src/backend/tests/unit/api/v1/test_deployment_schemas.py @@ -8,8 +8,10 @@ import pytest from langflow.api.v1.schemas.deployments import ( DeploymentConfigBindingUpdate, + DeploymentConfigCreate, DeploymentConfigListItem, DeploymentConfigListResponse, + DeploymentCreateRequest, DeploymentProviderAccountCreateRequest, DeploymentProviderAccountGetResponse, DeploymentProviderAccountUpdateRequest, @@ -194,6 +196,31 @@ def test_rejects_explicit_null_only_payload(self): DeploymentUpdateRequest(spec=None) +class TestSharedKernelProviderPayloadCompatibility: + def test_create_request_accepts_provider_spec_dict_through_strict_wrapper(self): + request = DeploymentCreateRequest( + provider_id=uuid4(), + spec={ + "name": "deployment", + "description": "", + "type": "agent", + "provider_spec": {"region": "us-east-1", "size": "small"}, + }, + ) + assert request.spec.provider_spec == {"region": "us-east-1", "size": "small"} + + def test_config_create_accepts_provider_config_dict_through_strict_wrapper(self): + payload = DeploymentConfigCreate( + raw_payload={ + "name": "cfg", + "description": "cfg-desc", + "provider_config": {"timeout_s": 30, "flags": {"dry_run": True}}, + } + ) + assert payload.raw_payload is not None + assert payload.raw_payload.provider_config == {"timeout_s": 30, "flags": {"dry_run": True}} + + # --------------------------------------------------------------------------- # DeploymentConfigListItem / DeploymentConfigListResponse # --------------------------------------------------------------------------- diff --git a/src/lfx/PLUGGABLE_SERVICES.md b/src/lfx/PLUGGABLE_SERVICES.md index e73fa3053afe..3a2f26ca2fd8 100644 --- a/src/lfx/PLUGGABLE_SERVICES.md +++ b/src/lfx/PLUGGABLE_SERVICES.md @@ -134,6 +134,28 @@ lfx/services/ This keeps top-level services (DI-managed singletons) separate from adapter implementations (keyed registries with multiple implementations per type). +### Payload Contract Ownership (Adapters vs API Layer) + +Payload contracts intentionally split ownership across layers: + +- **LFX owns shared slot primitives and slot taxonomy**: + - `lfx.services.adapters.payload` defines shared primitives (`PayloadSlot`, `ProviderPayloadSchemas`) + - each adapter domain in lfx defines canonical slot names once (for example deployment slots in + `lfx.services.adapters.deployment.payloads`) +- **Adapters (LFX side) own adapter-facing payload models**: + - adapters populate their `*PayloadSchemas` registry with adapter-side models +- **API hosts (for example Langflow) own API-facing payload models**: + - API mapper layers populate their own API payload registries + - API slots may differ from adapter slots when API-specific references or reshaping are required + +This boundary allows both layers to share one slot taxonomy while keeping API exceptions and +transformation logic outside adapter implementations. + +Deployment is the concrete example in this repository: + +- LFX defines deployment slot taxonomy and adapter registry (`DeploymentPayloadFields` / `DeploymentPayloadSchemas`) +- Langflow defines deployment API registry (`DeploymentApiPayloads`) and mapper behavior + ### Error Handling Behavior - Invalid import paths (missing `module:ClassName`) are ignored with warning logs diff --git a/src/lfx/src/lfx/services/adapters/__init__.py b/src/lfx/src/lfx/services/adapters/__init__.py index 32f4582daaae..8469021cd524 100644 --- a/src/lfx/src/lfx/services/adapters/__init__.py +++ b/src/lfx/src/lfx/services/adapters/__init__.py @@ -1 +1,17 @@ """Adapter namespaces for service-scoped plugin registries.""" + +from .payload import ( + AdapterPayload, + AdapterPayloadValidationError, + PayloadSlot, + PayloadSlotPolicy, + ProviderPayloadSchemas, +) + +__all__ = [ + "AdapterPayload", + "AdapterPayloadValidationError", + "PayloadSlot", + "PayloadSlotPolicy", + "ProviderPayloadSchemas", +] diff --git a/src/lfx/src/lfx/services/adapters/deployment/__init__.py b/src/lfx/src/lfx/services/adapters/deployment/__init__.py index 5df096c16648..c1ae98fdca19 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/__init__.py +++ b/src/lfx/src/lfx/services/adapters/deployment/__init__.py @@ -2,12 +2,54 @@ from .base import BaseDeploymentService from .exceptions import DeploymentError, DeploymentNotConfiguredError, DeploymentServiceError +from .payloads import ( + DeploymentPayloadFields, + DeploymentPayloadSchemas, + T_ConfigListParams, + T_ConfigListResult, + T_DeploymentConfig, + T_DeploymentCreateResult, + T_DeploymentItemData, + T_DeploymentListParams, + T_DeploymentListResult, + T_DeploymentOperationResult, + T_DeploymentSpec, + T_DeploymentStatusData, + T_DeploymentUpdate, + T_ExecutionInput, + T_ExecutionResult, + T_ListParamsPayload, + T_ProviderData, + T_ProviderResult, + T_SnapshotListParams, + T_SnapshotListResult, +) from .service import DeploymentService __all__ = [ "BaseDeploymentService", "DeploymentError", "DeploymentNotConfiguredError", + "DeploymentPayloadFields", + "DeploymentPayloadSchemas", "DeploymentService", "DeploymentServiceError", + "T_ConfigListParams", + "T_ConfigListResult", + "T_DeploymentConfig", + "T_DeploymentCreateResult", + "T_DeploymentItemData", + "T_DeploymentListParams", + "T_DeploymentListResult", + "T_DeploymentOperationResult", + "T_DeploymentSpec", + "T_DeploymentStatusData", + "T_DeploymentUpdate", + "T_ExecutionInput", + "T_ExecutionResult", + "T_ListParamsPayload", + "T_ProviderData", + "T_ProviderResult", + "T_SnapshotListParams", + "T_SnapshotListResult", ] diff --git a/src/lfx/src/lfx/services/adapters/deployment/base.py b/src/lfx/src/lfx/services/adapters/deployment/base.py index 5b69419ccb64..5338c3b2480b 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/base.py +++ b/src/lfx/src/lfx/services/adapters/deployment/base.py @@ -3,13 +3,14 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from lfx.services.base import Service if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession + from lfx.services.adapters.deployment.payloads import DeploymentPayloadSchemas from lfx.services.adapters.deployment.schema import ( ConfigListParams, ConfigListResult, @@ -47,6 +48,8 @@ class BaseDeploymentService(Service, ABC): LFX dependency injection and service protocols. """ + payload_schemas: ClassVar[DeploymentPayloadSchemas | None] = None + @abstractmethod async def create( self, @@ -148,7 +151,6 @@ async def create_execution( self, *, user_id: IdLike, - deployment_type: DeploymentType | None = None, payload: ExecutionCreate, db: AsyncSession, ) -> ExecutionCreateResult: diff --git a/src/lfx/src/lfx/services/adapters/deployment/payloads.py b/src/lfx/src/lfx/services/adapters/deployment/payloads.py new file mode 100644 index 000000000000..9547b50f3fd0 --- /dev/null +++ b/src/lfx/src/lfx/services/adapters/deployment/payloads.py @@ -0,0 +1,108 @@ +"""Deployment payload slot taxonomy shared across adapter and API layers.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from pydantic import BaseModel +from typing_extensions import TypeVar + +from lfx.services.adapters.payload import AdapterPayload, PayloadSlot, ProviderPayloadSchemas + +# This pairing is an explicit boundary design: +# - payload value typevars default to dict for open provider extension points +# - slot model typevars are BaseModel-bound for strict parse/dump contracts +# Together this preserves a stable pluggable surface while keeping validation +# strict at slot boundaries. +# Inbound payload pairs +T_DeploymentSpec = TypeVar("T_DeploymentSpec", default=AdapterPayload) +T_DeploymentSpecModel = TypeVar("T_DeploymentSpecModel", bound=BaseModel, default=BaseModel) + +T_DeploymentConfig = TypeVar("T_DeploymentConfig", default=AdapterPayload) +T_DeploymentConfigModel = TypeVar("T_DeploymentConfigModel", bound=BaseModel, default=BaseModel) + +T_DeploymentUpdate = TypeVar("T_DeploymentUpdate", default=AdapterPayload) +T_DeploymentUpdateModel = TypeVar("T_DeploymentUpdateModel", bound=BaseModel, default=BaseModel) + +T_ExecutionInput = TypeVar("T_ExecutionInput", default=AdapterPayload) +T_ExecutionInputModel = TypeVar("T_ExecutionInputModel", bound=BaseModel, default=BaseModel) + +T_DeploymentListParams = TypeVar("T_DeploymentListParams", default=AdapterPayload) +T_DeploymentListParamsModel = TypeVar("T_DeploymentListParamsModel", bound=BaseModel, default=BaseModel) + +T_ConfigListParams = TypeVar("T_ConfigListParams", default=AdapterPayload) +T_ConfigListParamsModel = TypeVar("T_ConfigListParamsModel", bound=BaseModel, default=BaseModel) + +T_SnapshotListParams = TypeVar("T_SnapshotListParams", default=AdapterPayload) +T_SnapshotListParamsModel = TypeVar("T_SnapshotListParamsModel", bound=BaseModel, default=BaseModel) + +# Outbound payload pairs +T_DeploymentCreateResult = TypeVar("T_DeploymentCreateResult", default=AdapterPayload) +T_DeploymentCreateResultModel = TypeVar("T_DeploymentCreateResultModel", bound=BaseModel, default=BaseModel) + +T_DeploymentOperationResult = TypeVar("T_DeploymentOperationResult", default=AdapterPayload) +T_DeploymentOperationResultModel = TypeVar("T_DeploymentOperationResultModel", bound=BaseModel, default=BaseModel) + +T_DeploymentListResult = TypeVar("T_DeploymentListResult", default=AdapterPayload) +T_DeploymentListResultModel = TypeVar("T_DeploymentListResultModel", bound=BaseModel, default=BaseModel) + +T_ConfigListResult = TypeVar("T_ConfigListResult", default=AdapterPayload) +T_ConfigListResultModel = TypeVar("T_ConfigListResultModel", bound=BaseModel, default=BaseModel) + +T_SnapshotListResult = TypeVar("T_SnapshotListResult", default=AdapterPayload) +T_SnapshotListResultModel = TypeVar("T_SnapshotListResultModel", bound=BaseModel, default=BaseModel) + +T_ExecutionResult = TypeVar("T_ExecutionResult", default=AdapterPayload) +T_ExecutionResultModel = TypeVar("T_ExecutionResultModel", bound=BaseModel, default=BaseModel) + +T_DeploymentItemData = TypeVar("T_DeploymentItemData", default=AdapterPayload) +T_DeploymentItemDataModel = TypeVar("T_DeploymentItemDataModel", bound=BaseModel, default=BaseModel) + +T_DeploymentStatusData = TypeVar("T_DeploymentStatusData", default=AdapterPayload) +T_DeploymentStatusDataModel = TypeVar("T_DeploymentStatusDataModel", bound=BaseModel, default=BaseModel) + +# Shared payload-only typevars +T_ProviderData = TypeVar("T_ProviderData", default=AdapterPayload) +T_ProviderResult = TypeVar("T_ProviderResult", default=AdapterPayload) +T_ListParamsPayload = TypeVar("T_ListParamsPayload", default=AdapterPayload) + + +@dataclass(frozen=True) +class DeploymentPayloadFields(ProviderPayloadSchemas): + """Canonical deployment payload slot names for all providers. + + Outbound slots are intentionally operation-specific (create, operation, + deployment list, config list, snapshot list, execution) so providers can + expose distinct payload contracts per operation without sharing one + umbrella ``deployment_result`` shape. + + Ownership boundary: + this module defines *slot names* (shared structure) for both layers. + Slot population is layer-specific: + - adapters populate ``DeploymentPayloadSchemas`` (adapter-side contracts) + - Langflow mappers populate ``DeploymentApiPayloads`` (API-side contracts) + """ + + # Inbound (request -> adapter) + deployment_spec: PayloadSlot[T_DeploymentSpecModel] | None = None + deployment_config: PayloadSlot[T_DeploymentConfigModel] | None = None + deployment_update: PayloadSlot[T_DeploymentUpdateModel] | None = None + execution_input: PayloadSlot[T_ExecutionInputModel] | None = None + deployment_list_params: PayloadSlot[T_DeploymentListParamsModel] | None = None + config_list_params: PayloadSlot[T_ConfigListParamsModel] | None = None + snapshot_list_params: PayloadSlot[T_SnapshotListParamsModel] | None = None + + # Outbound (adapter -> response) + deployment_create_result: PayloadSlot[T_DeploymentCreateResultModel] | None = None + deployment_operation_result: PayloadSlot[T_DeploymentOperationResultModel] | None = None + deployment_list_result: PayloadSlot[T_DeploymentListResultModel] | None = None + config_list_result: PayloadSlot[T_ConfigListResultModel] | None = None + snapshot_list_result: PayloadSlot[T_SnapshotListResultModel] | None = None + execution_result: PayloadSlot[T_ExecutionResultModel] | None = None + deployment_item_data: PayloadSlot[T_DeploymentItemDataModel] | None = None + deployment_status_data: PayloadSlot[T_DeploymentStatusDataModel] | None = None + + +@dataclass(frozen=True) +class DeploymentPayloadSchemas(DeploymentPayloadFields): + """Adapter-side payload schema registry for deployment providers.""" diff --git a/src/lfx/src/lfx/services/adapters/deployment/schema.py b/src/lfx/src/lfx/services/adapters/deployment/schema.py index e0bd38898109..30e27678bd48 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/schema.py +++ b/src/lfx/src/lfx/services/adapters/deployment/schema.py @@ -2,11 +2,33 @@ import json from enum import Enum from functools import lru_cache -from typing import Annotated, Any +from typing import Annotated, Generic from uuid import UUID from pydantic import BaseModel, ConfigDict, Field, StringConstraints, field_validator, model_validator +from lfx.services.adapters.deployment.payloads import ( + T_ConfigListParams, + T_ConfigListResult, + T_DeploymentConfig, + T_DeploymentCreateResult, + T_DeploymentItemData, + T_DeploymentListParams, + T_DeploymentListResult, + T_DeploymentOperationResult, + T_DeploymentSpec, + T_DeploymentStatusData, + T_DeploymentUpdate, + T_ExecutionInput, + T_ExecutionResult, + T_ListParamsPayload, + T_ProviderData, + T_ProviderResult, + T_SnapshotListParams, + T_SnapshotListResult, +) +from lfx.services.adapters.payload import AdapterPayload + DeploymentProviderName = Annotated[ str, StringConstraints(strip_whitespace=True, min_length=1, max_length=128), @@ -14,7 +36,6 @@ NormalizedId = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)] IdLike = UUID | NormalizedId -ProviderPayload = dict[str, Any] class DeploymentType(str, Enum): @@ -83,11 +104,29 @@ class SnapshotItems(BaseModel): model_config = ConfigDict(extra="forbid") - raw_payloads: SnapshotList = Field( - ..., - description="Raw snapshot payloads to create and bind for this deployment.", + raw_payloads: SnapshotList | None = Field( + None, + description="List of raw snapshot payloads to create and bind for this deployment. Omit to leave unchanged.", + ) + ids: list[IdLike] | None = Field( + None, + description="List of existing snapshot ids to bind to the deployment. Omit to leave unchanged.", ) + @field_validator("ids") + @classmethod + def validate_ids(cls, value: list[IdLike] | None) -> list[str] | None: + if value is None: + return None + return _normalize_and_dedupe_id_list(value, field_name="snapshot_id") + + @model_validator(mode="after") + def validate_snapshot_source(self) -> "SnapshotItems": + if not self.raw_payloads and not self.ids: + msg = "At least one of 'raw_payloads' or 'ids' must be provided and non-empty." + raise ValueError(msg) + return self + class SnapshotDeploymentBindingUpdate(BaseModel): """Snapshot deployment binding patch payload. @@ -172,7 +211,7 @@ class DeploymentConfig(BaseModel): description: str | None = Field(None, description="The description of the config") environment_variables: dict[EnvVarKey, EnvVarValueSpec] | None = Field(None, description="Environment variables") # the provider might have additional configuration options that are not covered here - provider_config: ProviderPayload | None = Field(None, description="Provider configuration") + provider_config: T_DeploymentConfig | None = Field(None, description="Provider configuration") class ConfigItem(BaseModel): @@ -263,25 +302,25 @@ class ConfigListItem(BaseModel): provider_data: dict | None = Field(None, description="The data of the config item from the provider") -class ProviderDataModel(BaseModel): +class ProviderDataModel(BaseModel, Generic[T_ProviderData]): """Base model for provider metadata payloads.""" - provider_data: ProviderPayload | None = Field(None, description="The data from the provider") + provider_data: T_ProviderData | None = Field(None, description="The data from the provider") -class ProviderResultModel(BaseModel): +class ProviderResultModel(BaseModel, Generic[T_ProviderResult]): """Base model for provider operation payloads.""" - provider_result: ProviderPayload | None = Field(None, description="The result from the provider") + provider_result: T_ProviderResult | None = Field(None, description="The result from the provider") -class ProviderSpecModel(BaseModel): +class ProviderSpecModel(BaseModel, Generic[T_DeploymentSpec]): """Base model for provider-specific input payloads.""" - provider_spec: ProviderPayload | None = Field(None, description="The data of the deployment from the provider") + provider_spec: T_DeploymentSpec | None = Field(None, description="The data of the deployment from the provider") -class BaseDeploymentData(ProviderSpecModel): +class BaseDeploymentData(ProviderSpecModel[T_DeploymentSpec]): """Model representing a data for a deployment.""" name: str = Field(description="The name of the deployment") @@ -289,7 +328,7 @@ class BaseDeploymentData(ProviderSpecModel): type: DeploymentType = Field(description="The type of the deployment") -class DeploymentCreateResult(BaseDeploymentData, ProviderResultModel): +class DeploymentCreateResult(ProviderResultModel[T_DeploymentCreateResult]): """Model representing a result for a deployment creation operation.""" id: IdLike = Field(description="The id of the created deployment") @@ -299,11 +338,11 @@ class DeploymentCreateResult(BaseDeploymentData, ProviderResultModel): ) snapshot_ids: list[IdLike] = Field( default_factory=list, - description="Snapshot ids produced during deployment creation.", + description="Snapshot ids produced or bound during deployment creation.", ) -class DeploymentOperationResult(ProviderResultModel): +class DeploymentOperationResult(ProviderResultModel[T_DeploymentOperationResult]): """Base model for deployment operation responses by deployment id.""" id: IdLike = Field(description="The id of the deployment") @@ -313,7 +352,7 @@ class DeploymentDeleteResult(DeploymentOperationResult): """Model representing a result for a deployment deletion operation.""" -class ItemResult(ProviderDataModel): +class ItemResult(ProviderDataModel[T_DeploymentItemData]): """Model representing a result for a deployment list item.""" id: IdLike = Field(description="The id of the deployment") @@ -333,28 +372,28 @@ class DeploymentDuplicateResult(ItemResult): """Model representing the result of a deployment duplication operation.""" -class DeploymentListResult(ProviderResultModel): +class DeploymentListResult(ProviderResultModel[T_DeploymentListResult]): """Model representing a result for a deployment list operation.""" deployments: list[ItemResult] = Field(description="The list of deployments") -class ConfigListResult(ProviderResultModel): +class ConfigListResult(ProviderResultModel[T_ConfigListResult]): """Model representing a result for a config list operation.""" configs: list[ConfigListItem] = Field(description="The list of configs") -class SnapshotListResult(ProviderResultModel): +class SnapshotListResult(ProviderResultModel[T_SnapshotListResult]): """Model representing a result for a snapshot list operation.""" snapshots: list[SnapshotItem] = Field(description="The list of snapshots") -class _BaseListParams(BaseModel): +class _BaseListParams(BaseModel, Generic[T_ListParamsPayload]): """Shared list-filter fields.""" - provider_params: ProviderPayload | None = Field( + provider_params: T_ListParamsPayload | None = Field( None, description="Provider-specific query params to filter by.", ) @@ -380,7 +419,7 @@ def validate_filter_ids(cls, value: list[IdLike] | None, info) -> list[str] | No return cls._normalize_filter_id_values(value, field_name=info.field_name) -class DeploymentListParams(_BaseListParams): +class DeploymentListParams(_BaseListParams[T_DeploymentListParams]): """Query params for deployment list operations.""" deployment_types: list[DeploymentType] | None = Field( @@ -410,7 +449,7 @@ def validate_entity_filter_ids(cls, value: list[IdLike] | None, info) -> list[st return cls._normalize_filter_id_values(value, field_name=info.field_name) -class ConfigListParams(_BaseListParams): +class ConfigListParams(_BaseListParams[T_ConfigListParams]): """Query params for config list operations.""" config_ids: list[IdLike] | None = Field( @@ -424,7 +463,7 @@ def validate_config_ids(cls, value: list[IdLike] | None, info) -> list[str] | No return cls._normalize_filter_id_values(value, field_name=info.field_name) -class SnapshotListParams(_BaseListParams): +class SnapshotListParams(_BaseListParams[T_SnapshotListParams]): """Query params for snapshot list operations.""" snapshot_ids: list[IdLike] | None = Field( @@ -472,7 +511,7 @@ class DeploymentUpdate(BaseModel): spec: BaseDeploymentDataUpdate | None = Field(None, description="The metadata of the deployment") snapshot: SnapshotDeploymentBindingUpdate | None = Field(None, description="The snapshot of the deployment") config: ConfigDeploymentBindingUpdate | None = Field(None, description="The config of the deployment") - provider_data: ProviderPayload | None = Field( + provider_data: T_DeploymentUpdate | None = Field( None, description="Provider-specific opaque payload for deployment update operations.", ) @@ -501,7 +540,7 @@ class RedeployResult(DeploymentOperationResult): """Model representing a deployment redeployment operation result.""" -class DeploymentStatusResult(ProviderDataModel): +class DeploymentStatusResult(ProviderDataModel[T_DeploymentStatusData]): """Model representing a deployment status response. Inherits ``provider_data`` from ``ProviderDataModel`` to carry @@ -515,8 +554,12 @@ class ExecutionCreate(BaseModel): """Provider-agnostic deployment execution payload.""" deployment_id: IdLike = Field(description="The id of the deployment to create an execution for.") + deployment_type: DeploymentType | None = Field( + default=None, + description="Optional deployment type routing hint for execution creation.", + ) - provider_data: ProviderPayload | None = Field( + provider_data: T_ExecutionInput | None = Field( None, description="Provider-specific execution data.", ) @@ -529,7 +572,7 @@ def validate_deployment_id(cls, value: IdLike) -> IdLike: return value -class ExecutionResultBase(ProviderResultModel): +class ExecutionResultBase(ProviderResultModel[T_ExecutionResult]): """Base model for deployment execution responses.""" execution_id: str | None = Field( @@ -559,7 +602,7 @@ class ExecutionStatusResult(ExecutionResultBase): """ -class DeploymentListTypesResult(ProviderResultModel): +class DeploymentListTypesResult(ProviderResultModel[AdapterPayload]): """Model representing deployment types listing response.""" deployment_types: list[DeploymentType] = Field(description="Supported deployment types.") diff --git a/src/lfx/src/lfx/services/adapters/deployment/service.py b/src/lfx/src/lfx/services/adapters/deployment/service.py index 7419fb48e356..5ed28a7a8219 100644 --- a/src/lfx/src/lfx/services/adapters/deployment/service.py +++ b/src/lfx/src/lfx/services/adapters/deployment/service.py @@ -150,7 +150,6 @@ async def create_execution( self, *, user_id: IdLike, - deployment_type: DeploymentType | None = None, payload: ExecutionCreate, db: AsyncSession, ) -> ExecutionCreateResult: diff --git a/src/lfx/src/lfx/services/adapters/payload.py b/src/lfx/src/lfx/services/adapters/payload.py new file mode 100644 index 000000000000..76a008d3d57f --- /dev/null +++ b/src/lfx/src/lfx/services/adapters/payload.py @@ -0,0 +1,91 @@ +"""Shared payload contracts and schema registry primitives. + +Ownership boundaries start here: + +- ``PayloadSlot`` is a generic parse/dump contract and is layer-agnostic. + Both adapter-side and API-side registries use the same slot primitive. +- ``ProviderPayloadSchemas`` is a structural base for named slot registries. + It owns introspection helpers only (``slots`` / ``active_slots``), not + any adapter- or API-specific slot names. +- Concrete ``*PayloadFields`` classes (for example deployment payload fields) + define canonical slot names in lfx so both layers share one structure. +- Adapter integrations populate adapter-side registries in lfx + (``*PayloadSchemas`` subclasses). +- Langflow integrations populate API-side registries in Langflow + (for example ``DeploymentApiPayloads``), including Langflow-specific + validation/reshaping decisions. +""" + +from __future__ import annotations + +from dataclasses import dataclass, fields +from enum import Enum +from typing import Any, Generic + +from pydantic import BaseModel, ValidationError +from typing_extensions import TypeVar + +AdapterPayload = dict[str, Any] +T_Model = TypeVar("T_Model", bound=BaseModel) + + +class PayloadSlotPolicy(str, Enum): + """Controls how a slot applies schema validation.""" + + VALIDATE_ONLY = "validate_only" + VALIDATE_AND_DUMP = "validate_and_dump" + + +class AdapterPayloadValidationError(ValueError): + """Raised when a payload fails adapter schema validation.""" + + def __init__(self, *, model_name: str, error: ValidationError) -> None: + self.model_name = model_name + self.error = error + # Keep public exception text sanitized to avoid leaking raw payload + # fragments from ValidationError.__str__() into logs/responses. + super().__init__(f"Invalid payload for '{model_name}'.") + + +@dataclass(frozen=True) +class PayloadSlot(Generic[T_Model]): + """Layer-agnostic contract between raw payload dicts and typed models.""" + + adapter_model: type[T_Model] + policy: PayloadSlotPolicy = PayloadSlotPolicy.VALIDATE_AND_DUMP + + def parse(self, raw: AdapterPayload) -> T_Model: + """Validate and parse raw provider payload into the typed model.""" + try: + return self.adapter_model.model_validate(raw) + except ValidationError as exc: + raise AdapterPayloadValidationError(model_name=self.adapter_model.__name__, error=exc) from exc + + def dump(self, value: T_Model) -> AdapterPayload: + """Serialize typed model back into a plain provider payload dict.""" + return value.model_dump(mode="json") + + def apply(self, raw: AdapterPayload) -> AdapterPayload: + """Apply slot policy to a raw payload dict.""" + validated = self.parse(raw) + if self.policy is PayloadSlotPolicy.VALIDATE_ONLY: + return raw + return self.dump(validated) + + +@dataclass(frozen=True) +class ProviderPayloadSchemas: + """Structural base class for named slot registries. + + This class intentionally avoids defining slot names or ownership policy. + Concrete subclasses declare domain-specific fields and are owned by the + module that defines that domain contract (for example deployment). + """ + + def slots(self) -> dict[str, PayloadSlot[Any] | None]: + """Return all registry slots, keyed by slot name.""" + return {f.name: getattr(self, f.name) for f in fields(self)} + + def active_slots(self) -> dict[str, PayloadSlot[Any]]: + """Return only populated (non-None) slots.""" + return {name: slot for name, slot in self.slots().items() if slot is not None} diff --git a/src/lfx/src/lfx/services/interfaces.py b/src/lfx/src/lfx/services/interfaces.py index cdc08420bc55..2c2211bdee66 100644 --- a/src/lfx/src/lfx/services/interfaces.py +++ b/src/lfx/src/lfx/services/interfaces.py @@ -241,8 +241,9 @@ class DeploymentServiceProtocol(Protocol): Adapter-specific or advanced operations are defined on concrete deployment service classes. - ``deployment_type`` is accepted as an optional routing hint by all - operations that act on a specific deployment (including executions). + ``deployment_type`` is accepted as an optional routing hint by operations + that act on a specific deployment. For execution creation, it is provided + in the ``ExecutionCreate`` payload. """ @abstractmethod @@ -355,7 +356,6 @@ async def create_execution( self, *, user_id: IdLike, - deployment_type: DeploymentType | None = None, payload: ExecutionCreate, db: AsyncSession, ) -> ExecutionCreateResult: diff --git a/src/lfx/tests/unit/services/deployment/test_deployment_schema.py b/src/lfx/tests/unit/services/deployment/test_deployment_schema.py index 04fa86a63548..a6da88dfb17f 100644 --- a/src/lfx/tests/unit/services/deployment/test_deployment_schema.py +++ b/src/lfx/tests/unit/services/deployment/test_deployment_schema.py @@ -38,10 +38,35 @@ from pydantic import ValidationError -def test_snapshot_items_requires_raw_payloads() -> None: - with pytest.raises(ValidationError, match="Field required"): +def test_snapshot_items_requires_at_least_one_source() -> None: + with pytest.raises(ValidationError, match="At least one of 'raw_payloads' or 'ids'"): SnapshotItems() + +def test_snapshot_items_accepts_ids_only() -> None: + snap_uuid = uuid4() + payload = SnapshotItems(ids=[snap_uuid, f" {snap_uuid} ", "snap_1", "snap_1"]) + assert payload.raw_payloads is None + assert payload.ids == [str(snap_uuid), "snap_1"] + + +def test_snapshot_items_accepts_raw_payloads_and_ids() -> None: + snap_uuid = uuid4() + payload = SnapshotItems( + raw_payloads=[ + { + "id": uuid4(), + "name": "Flow", + "data": {"nodes": [], "edges": []}, + } + ], + ids=[snap_uuid], + ) + assert payload.raw_payloads is not None + assert payload.ids == [str(snap_uuid)] + + +def test_snapshot_items_rejects_unknown_fields() -> None: with pytest.raises(ValidationError, match="Extra inputs are not permitted"): SnapshotItems( raw_payloads=[ @@ -278,6 +303,11 @@ def test_snapshot_items_rejects_empty_raw_payload_list() -> None: SnapshotItems(raw_payloads=[]) +def test_snapshot_items_rejects_empty_ids_without_raw_payloads() -> None: + with pytest.raises(ValidationError, match="At least one of 'raw_payloads' or 'ids'"): + SnapshotItems(ids=[]) + + def test_base_flow_artifact_allows_extra_fields() -> None: flow = BaseFlowArtifact( id=uuid4(), @@ -372,6 +402,16 @@ def test_execution_create_accepts_uuid_deployment_id() -> None: assert payload.deployment_id == dep_uuid +def test_execution_create_accepts_deployment_type() -> None: + payload = ExecutionCreate(deployment_id="dep_1", deployment_type=DeploymentType.AGENT) + assert payload.deployment_type == DeploymentType.AGENT + + +def test_execution_create_rejects_invalid_deployment_type() -> None: + with pytest.raises(ValidationError, match="deployment_type"): + ExecutionCreate(deployment_id="dep_1", deployment_type="invalid-type") + + def test_execution_create_rejects_blank_deployment_id() -> None: with pytest.raises(ValidationError): ExecutionCreate(deployment_id=" ") @@ -513,9 +553,6 @@ def test_deployment_create_happy_path_with_snapshot_and_config() -> None: def test_deployment_create_result_defaults() -> None: result = DeploymentCreateResult( id="dep_1", - name="deployment", - description="", - type=DeploymentType.AGENT, ) assert result.snapshot_ids == [] assert result.config_id is None diff --git a/src/lfx/tests/unit/services/deployment/test_payload_formalization.py b/src/lfx/tests/unit/services/deployment/test_payload_formalization.py new file mode 100644 index 000000000000..2fd21355f475 --- /dev/null +++ b/src/lfx/tests/unit/services/deployment/test_payload_formalization.py @@ -0,0 +1,239 @@ +"""Tests for deployment payload slot formalization.""" + +from __future__ import annotations + +from dataclasses import fields +from datetime import datetime, timezone +from enum import Enum +from uuid import UUID + +import pytest +from lfx.services.adapters.deployment.payloads import DeploymentPayloadSchemas +from lfx.services.adapters.deployment.schema import ( + BaseDeploymentData, + ConfigListParams, + ConfigListResult, + DeploymentCreateResult, + DeploymentListParams, + DeploymentListResult, + DeploymentOperationResult, + DeploymentStatusResult, + DeploymentType, + ExecutionResultBase, + ItemResult, + SnapshotListParams, + SnapshotListResult, +) +from lfx.services.adapters.payload import AdapterPayloadValidationError, PayloadSlot, PayloadSlotPolicy +from pydantic import BaseModel + + +class _SpecModel(BaseModel): + region: str + + +class _StatusModel(BaseModel): + healthy: bool + + +class _FilterModel(BaseModel): + env: str + + +class _ResultModel(BaseModel): + external_url: str + + +class _ExecutionResultModel(BaseModel): + status: str + + +class _ConfigFilterModel(BaseModel): + namespace: str + + +class _SnapshotFilterModel(BaseModel): + label: str + + +class _ApiLikeConfigModel(BaseModel): + retries: int + + +class _Color(str, Enum): + BLUE = "blue" + + +class _NestedPayload(BaseModel): + tag: str + + +class _RichPayload(BaseModel): + uid: UUID + created_at: datetime + color: _Color + nested: _NestedPayload + + +def test_payload_slot_parse_and_dump_round_trip() -> None: + slot = PayloadSlot(_SpecModel) + + parsed = slot.parse({"region": "us-east-1"}) + + assert isinstance(parsed, _SpecModel) + assert slot.dump(parsed) == {"region": "us-east-1"} + + +def test_payload_slot_raises_typed_validation_error() -> None: + slot = PayloadSlot(_SpecModel) + + with pytest.raises(AdapterPayloadValidationError, match="Invalid payload") as exc: + slot.parse({"missing": "region"}) + assert exc.value.model_name == "_SpecModel" + assert exc.value.error is not None + + +def test_payload_slot_dump_json_serializes_uuid_datetime_enum_and_nested_model() -> None: + slot = PayloadSlot(_RichPayload) + parsed = slot.parse( + { + "uid": "d779b2b7-302e-4f7b-af66-3fe4fd51b9fe", + "created_at": "2026-01-02T03:04:05Z", + "color": "blue", + "nested": {"tag": "x"}, + } + ) + + dumped = slot.dump(parsed) + + assert dumped == { + "uid": "d779b2b7-302e-4f7b-af66-3fe4fd51b9fe", + "created_at": "2026-01-02T03:04:05Z", + "color": "blue", + "nested": {"tag": "x"}, + } + + +def test_payload_slot_apply_validate_only_policy_returns_original_payload() -> None: + slot = PayloadSlot(_SpecModel, policy=PayloadSlotPolicy.VALIDATE_ONLY) + raw = {"region": "us-east-1"} + + applied = slot.apply(raw) + + assert applied == raw + assert applied is raw + + +def test_payload_slot_apply_validate_and_dump_policy_normalizes_payload() -> None: + slot = PayloadSlot(_ApiLikeConfigModel, policy=PayloadSlotPolicy.VALIDATE_AND_DUMP) + raw = {"retries": "3"} + + applied = slot.apply(raw) + + assert applied == {"retries": 3} + assert applied is not raw + + +def test_deployment_payload_schemas_exposes_slots_and_active_slots() -> None: + schemas = DeploymentPayloadSchemas( + deployment_spec=PayloadSlot(_SpecModel), + deployment_status_data=PayloadSlot(_StatusModel), + ) + + all_slots = schemas.slots() + active_slots = schemas.active_slots() + + assert set(all_slots) == {field.name for field in fields(DeploymentPayloadSchemas)} + assert set(active_slots) == {"deployment_spec", "deployment_status_data"} + assert all_slots["deployment_config"] is None + + +def test_deployment_payload_schemas_defaults_to_no_active_slots() -> None: + assert DeploymentPayloadSchemas().active_slots() == {} + + +def test_generic_parametrization_applies_to_provider_fields() -> None: + typed_spec = BaseDeploymentData[_SpecModel]( + name="dep", + description="", + type=DeploymentType.AGENT, + provider_spec={"region": "us-east-1"}, + ) + typed_status = DeploymentStatusResult[_StatusModel]( + id="dep_1", + provider_data={"healthy": True}, + ) + typed_params = DeploymentListParams[_FilterModel](provider_params={"env": "prod"}) + + assert isinstance(typed_spec.provider_spec, _SpecModel) + assert isinstance(typed_status.provider_data, _StatusModel) + assert isinstance(typed_params.provider_params, _FilterModel) + + +def test_generic_parametrization_applies_to_result_and_list_models() -> None: + typed_create = DeploymentCreateResult[_ResultModel]( + id="dep_1", + provider_result={"external_url": "https://dep.example"}, + ) + typed_operation = DeploymentOperationResult[_ResultModel]( + id="dep_1", + provider_result={"external_url": "https://dep.example"}, + ) + typed_execution = ExecutionResultBase[_ExecutionResultModel]( + execution_id="exec_1", + deployment_id="dep_1", + provider_result={"status": "running"}, + ) + typed_item = ItemResult[_StatusModel]( + id="dep_1", + name="dep", + type=DeploymentType.AGENT, + provider_data={"healthy": True}, + ) + typed_deployment_list = DeploymentListResult[_ResultModel]( + deployments=[], + provider_result={"external_url": "https://dep.example"}, + ) + typed_config_list = ConfigListResult[_ResultModel]( + configs=[], + provider_result={"external_url": "https://dep.example"}, + ) + typed_snapshot_list = SnapshotListResult[_ResultModel]( + snapshots=[], + provider_result={"external_url": "https://dep.example"}, + ) + typed_config_params = ConfigListParams[_ConfigFilterModel](provider_params={"namespace": "prod"}) + typed_snapshot_params = SnapshotListParams[_SnapshotFilterModel](provider_params={"label": "nightly"}) + + assert isinstance(typed_create.provider_result, _ResultModel) + assert isinstance(typed_operation.provider_result, _ResultModel) + assert isinstance(typed_execution.provider_result, _ExecutionResultModel) + assert isinstance(typed_item.provider_data, _StatusModel) + assert isinstance(typed_deployment_list.provider_result, _ResultModel) + assert isinstance(typed_config_list.provider_result, _ResultModel) + assert isinstance(typed_snapshot_list.provider_result, _ResultModel) + assert isinstance(typed_config_params.provider_params, _ConfigFilterModel) + assert isinstance(typed_snapshot_params.provider_params, _SnapshotFilterModel) + + +def test_unparametrized_models_keep_dict_passthrough_behavior() -> None: + payload = {"region": "us-east-1"} + data = BaseDeploymentData( + name="dep", + description="", + type=DeploymentType.AGENT, + provider_spec=payload, + ) + assert data.provider_spec == payload + + now = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + rich_slot = PayloadSlot(_RichPayload) + dumped = rich_slot.dump( + _RichPayload( + uid=UUID("d779b2b7-302e-4f7b-af66-3fe4fd51b9fe"), + created_at=now, + color=_Color.BLUE, + nested=_NestedPayload(tag="x"), + ) + ) + assert dumped["uid"] == "d779b2b7-302e-4f7b-af66-3fe4fd51b9fe" From 2d3be10b6a6d083882640abad8eacb4a805ae2e0 Mon Sep 17 00:00:00 2001 From: Viktor Avelino <64113566+viktoravelino@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:02:49 -0400 Subject: [PATCH 24/29] fix: allow clearing Max Tokens field with Backspace/Delete (#12198) * fix: allow clearing Max Tokens field with Backspace/Delete Empty string input was being converted to 0 via Number(""), which triggered the min-value guard and snapped the field back to 1 before onChange could propagate. Adding an early return for empty input lets the field clear correctly, propagating null (no limit) downstream. * test: add IntComponent tests for handleInputChange clearing behavior Covers the regression where Backspace/Delete was blocked by the min-value guard, and verifies that below-min values still clamp correctly. --- .../__tests__/IntComponent.test.tsx | 136 ++++++++++++++++++ .../components/intComponent/index.tsx | 4 +- 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/intComponent/__tests__/IntComponent.test.tsx diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/intComponent/__tests__/IntComponent.test.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/intComponent/__tests__/IntComponent.test.tsx new file mode 100644 index 000000000000..e9af84f0d052 --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/intComponent/__tests__/IntComponent.test.tsx @@ -0,0 +1,136 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import type { ReactNode } from "react"; +import IntComponent from "../index"; + +type MockProps = { + children?: ReactNode; + onChange?: React.ChangeEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onInput?: React.FormEventHandler; + onMouseDown?: React.MouseEventHandler; + onClickCapture?: React.MouseEventHandler; + disabled?: boolean; + [key: string]: unknown; +}; + +// Mock Chakra UI NumberInput as plain HTML equivalents +jest.mock("@chakra-ui/number-input", () => ({ + NumberInput: ({ + children, + onChange: _onChange, + isDisabled: _isDisabled, + value: _value, + ...props + }: MockProps) => ( +
)}>{children}
+ ), + NumberInputField: ({ + onChange, + onKeyDown, + onInput, + disabled, + children: _children, + ...props + }: MockProps) => ( + )} + /> + ), + NumberInputStepper: ({ children }: MockProps) =>
{children}
, + NumberIncrementStepper: ({ + children, + onClickCapture: _onClickCapture, + ...props + }: MockProps) => ( + + ), + NumberDecrementStepper: ({ + children, + onClickCapture, + ...props + }: MockProps) => ( + + ), +})); + +jest.mock("lucide-react", () => ({ + MinusIcon: () => -, + PlusIcon: () => +, +})); + +jest.mock("@/constants/constants", () => ({ ICON_STROKE_WIDTH: 1.5 })); +jest.mock("@/utils/utils", () => ({ + cn: (...args: string[]) => args.filter(Boolean).join(" "), +})); +jest.mock("@/utils/reactflowUtils", () => ({ handleKeyDown: jest.fn() })); + +const defaultProps = { + value: 100, + handleOnNewValue: jest.fn(), + rangeSpec: { min: 1, max: 131072, step: 1 }, + name: "max_tokens", + disabled: false, + editNode: false, + id: "max-tokens-input", +}; + +describe("IntComponent – handleInputChange (clearing the field)", () => { + beforeEach(() => jest.clearAllMocks()); + + it("does NOT reset the input when the field is cleared (empty string)", () => { + render(); + const input = screen.getByTestId("max-tokens-input") as HTMLInputElement; + + // Simulate the user clearing the field via Backspace/Delete + Object.defineProperty(input, "value", { writable: true, value: "" }); + fireEvent.input(input); + + // The DOM value should remain "" — not snapped back to "1" + expect(input.value).toBe(""); + }); + + it("resets the input to min when value is below min (e.g. 0 for max_tokens)", () => { + render(); + const input = screen.getByTestId("max-tokens-input") as HTMLInputElement; + + Object.defineProperty(input, "value", { writable: true, value: "0" }); + fireEvent.input(input); + + expect(input.value).toBe("1"); + }); + + it("resets the input to min when a negative value is typed", () => { + render(); + const input = screen.getByTestId("max-tokens-input") as HTMLInputElement; + + Object.defineProperty(input, "value", { writable: true, value: "-5" }); + fireEvent.input(input); + + expect(input.value).toBe("1"); + }); + + it("does not reset the input when value is at or above min", () => { + render(); + const input = screen.getByTestId("max-tokens-input") as HTMLInputElement; + + Object.defineProperty(input, "value", { writable: true, value: "50" }); + fireEvent.input(input); + + expect(input.value).toBe("50"); + }); +}); diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/intComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/intComponent/index.tsx index 577980b52bf1..75d5353091ba 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/intComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/intComponent/index.tsx @@ -124,7 +124,9 @@ export default function IntComponent({ }; const handleInputChange = (event: React.FormEvent) => { - const inputValue = Number((event.target as HTMLInputElement).value); + const raw = (event.target as HTMLInputElement).value; + if (raw === "") return; // Allow clearing the field (empty = no limit) + const inputValue = Number(raw); if (Number.isFinite(inputValue) && inputValue < getMinValue()) { (event.target as HTMLInputElement).value = getMinValue().toString(); } From 94c94ad62dd55ddad21cd7aa3f7a4a8f5cc2c10c Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Mon, 16 Mar 2026 17:10:40 -0300 Subject: [PATCH 25/29] fix: Resolve CodeQL false positives for path injection and URL substring sanitization (#12201) --- .../agentic/services/helpers/flow_loader.py | 5 +++++ .../agentic/flows/test_langflow_assistant.py | 4 +++- .../services/helpers/test_flow_loader.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/agentic/services/helpers/flow_loader.py b/src/backend/base/langflow/agentic/services/helpers/flow_loader.py index 44270dd9f55a..356f4c92f575 100644 --- a/src/backend/base/langflow/agentic/services/helpers/flow_loader.py +++ b/src/backend/base/langflow/agentic/services/helpers/flow_loader.py @@ -77,6 +77,11 @@ def resolve_flow_path(flow_filename: str) -> tuple[Path, str]: Raises: HTTPException: If flow file not found or path traversal detected. """ + # Early rejection of path traversal sequences before any path construction. + # This complements _validate_path_within_base as defense-in-depth. + if ".." in flow_filename or "\\" in flow_filename: + raise HTTPException(status_code=400, detail=f"Invalid flow filename: '{flow_filename}'") + if flow_filename.endswith(".json"): flow_path = _validate_path_within_base(FLOWS_BASE_PATH / flow_filename, flow_filename) if flow_path.exists(): diff --git a/src/backend/tests/unit/agentic/flows/test_langflow_assistant.py b/src/backend/tests/unit/agentic/flows/test_langflow_assistant.py index 06dddd58b6b7..222ea2626c7f 100644 --- a/src/backend/tests/unit/agentic/flows/test_langflow_assistant.py +++ b/src/backend/tests/unit/agentic/flows/test_langflow_assistant.py @@ -259,7 +259,9 @@ def test_should_contain_component_generation_instructions(self): def test_should_contain_langflow_references(self): """Should contain Langflow documentation references.""" assert "langflow" in ASSISTANT_PROMPT.lower() - assert "docs.langflow.org" in ASSISTANT_PROMPT + # Use full URL with scheme to avoid CodeQL py/incomplete-url-substring-sanitization + # This is not URL validation — it verifies the static prompt contains doc references + assert "https://docs.langflow.org/" in ASSISTANT_PROMPT def test_should_contain_code_requirements(self): """Should contain code requirements for components.""" diff --git a/src/backend/tests/unit/agentic/services/helpers/test_flow_loader.py b/src/backend/tests/unit/agentic/services/helpers/test_flow_loader.py index 784f591d79b0..9bdaa2cddfa7 100644 --- a/src/backend/tests/unit/agentic/services/helpers/test_flow_loader.py +++ b/src/backend/tests/unit/agentic/services/helpers/test_flow_loader.py @@ -149,6 +149,24 @@ def test_should_fallback_to_json_when_python_not_found(self, tmp_path): assert result_type == "json" assert result_path == json_file.resolve() + def test_should_reject_filename_with_path_traversal_sequences(self, tmp_path): + """Should reject filenames containing '..' before any path construction.""" + with patch("langflow.agentic.services.helpers.flow_loader.FLOWS_BASE_PATH", tmp_path): + with pytest.raises(HTTPException) as exc_info: + resolve_flow_path("../../etc/passwd") + + assert exc_info.value.status_code == 400 + assert "Invalid flow filename" in exc_info.value.detail + + def test_should_reject_filename_with_backslash_traversal(self, tmp_path): + """Should reject filenames containing backslash path separators.""" + with patch("langflow.agentic.services.helpers.flow_loader.FLOWS_BASE_PATH", tmp_path): + with pytest.raises(HTTPException) as exc_info: + resolve_flow_path("..\\..\\etc\\passwd") + + assert exc_info.value.status_code == 400 + assert "Invalid flow filename" in exc_info.value.detail + def test_should_raise_404_when_flow_not_found(self, tmp_path): """Should raise HTTPException 404 when flow file doesn't exist.""" with patch("langflow.agentic.services.helpers.flow_loader.FLOWS_BASE_PATH", tmp_path): From 68edb4ad81d232841652216c30daa65eb42e8de1 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Mon, 16 Mar 2026 17:31:08 -0300 Subject: [PATCH 26/29] fix: Add explicit left/right DataFrame inputs for merge operations (#12177) * add explicit left/right DataFrame inputs for merge operations * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * [autofix.ci] apply automated fixes * ruff style and checker fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- ...add_ondelete_cascade_to_file_user_id_fk.py | 24 +-- .../processing/test_dataframe_operations.py | 190 +++++++++++++++--- src/lfx/src/lfx/_assets/component_index.json | 58 +++++- .../processing/dataframe_operations.py | 81 +++++--- 4 files changed, 272 insertions(+), 81 deletions(-) diff --git a/src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py b/src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py index a9f99d5d83f3..8f1c01d5e01e 100644 --- a/src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py +++ b/src/backend/base/langflow/alembic/versions/0e6138e7a0c2_add_ondelete_cascade_to_file_user_id_fk.py @@ -6,13 +6,13 @@ Phase: EXPAND """ + from collections.abc import Sequence import sqlalchemy as sa from alembic import op from langflow.utils import migration - # revision identifiers, used by Alembic. revision: str = "0e6138e7a0c2" # pragma: allowlist secret down_revision: str | None = "fc7f696a57bf" # pragma: allowlist secret @@ -38,16 +38,12 @@ def upgrade() -> None: fk_name = _get_fk_constraint_name(conn, "file", "user_id") if fk_name is None: - with op.batch_alter_table('file', schema=None) as batch_op: - batch_op.create_foreign_key( - "fk_file_user_id_user", 'user', ['user_id'], ['id'], ondelete='CASCADE' - ) + with op.batch_alter_table("file", schema=None) as batch_op: + batch_op.create_foreign_key("fk_file_user_id_user", "user", ["user_id"], ["id"], ondelete="CASCADE") else: - with op.batch_alter_table('file', schema=None) as batch_op: - batch_op.drop_constraint(fk_name, type_='foreignkey') - batch_op.create_foreign_key( - "fk_file_user_id_user", 'user', ['user_id'], ['id'], ondelete='CASCADE' - ) + with op.batch_alter_table("file", schema=None) as batch_op: + batch_op.drop_constraint(fk_name, type_="foreignkey") + batch_op.create_foreign_key("fk_file_user_id_user", "user", ["user_id"], ["id"], ondelete="CASCADE") def downgrade() -> None: @@ -61,8 +57,6 @@ def downgrade() -> None: if fk_name is None: return - with op.batch_alter_table('file', schema=None) as batch_op: - batch_op.drop_constraint(fk_name, type_='foreignkey') - batch_op.create_foreign_key( - "fk_file_user_id_user", 'user', ['user_id'], ['id'] - ) + with op.batch_alter_table("file", schema=None) as batch_op: + batch_op.drop_constraint(fk_name, type_="foreignkey") + batch_op.create_foreign_key("fk_file_user_id_user", "user", ["user_id"], ["id"]) diff --git a/src/backend/tests/unit/components/processing/test_dataframe_operations.py b/src/backend/tests/unit/components/processing/test_dataframe_operations.py index 4ea3dde5c566..49a964533a7d 100644 --- a/src/backend/tests/unit/components/processing/test_dataframe_operations.py +++ b/src/backend/tests/unit/components/processing/test_dataframe_operations.py @@ -280,6 +280,8 @@ def test_filter_fields_show(self, component): "replacement_value": {"show": False}, "merge_on_column": {"show": False}, "merge_how": {"show": False}, + "left_dataframe": {"show": False}, + "right_dataframe": {"show": False}, } # Select Filter operation @@ -307,6 +309,8 @@ def test_sort_fields_show(self, component): "replacement_value": {"show": False}, "merge_on_column": {"show": False}, "merge_how": {"show": False}, + "left_dataframe": {"show": False}, + "right_dataframe": {"show": False}, } # Select Sort operation @@ -334,6 +338,8 @@ def test_empty_selection_hides_fields(self, component): "replacement_value": {"show": True}, "merge_on_column": {"show": True}, "merge_how": {"show": True}, + "left_dataframe": {"show": True}, + "right_dataframe": {"show": True}, } # Deselect operation (empty list) @@ -356,6 +362,8 @@ def test_empty_selection_hides_fields(self, component): assert updated_config["replacement_value"]["show"] is False assert updated_config["merge_on_column"]["show"] is False assert updated_config["merge_how"]["show"] is False + assert updated_config["left_dataframe"]["show"] is False + assert updated_config["right_dataframe"]["show"] is False class TestDataTypes: @@ -441,7 +449,8 @@ def test_merge_inner_join(self, component): df1 = DataFrame(pd.DataFrame({"id": [1, 2, 3], "name": ["Alice", "Bob", "Charlie"]})) df2 = DataFrame(pd.DataFrame({"id": [2, 3, 4], "city": ["NYC", "LA", "Chicago"]})) - component.df = [df1, df2] + component.left_dataframe = df1 + component.right_dataframe = df2 component.operation = [{"name": "Merge", "icon": "merge"}] component.merge_on_column = "id" component.merge_how = "inner" @@ -457,7 +466,8 @@ def test_merge_outer_join(self, component): df1 = DataFrame(pd.DataFrame({"id": [1, 2], "name": ["Alice", "Bob"]})) df2 = DataFrame(pd.DataFrame({"id": [2, 3], "city": ["NYC", "LA"]})) - component.df = [df1, df2] + component.left_dataframe = df1 + component.right_dataframe = df2 component.operation = [{"name": "Merge", "icon": "merge"}] component.merge_on_column = "id" component.merge_how = "outer" @@ -467,38 +477,156 @@ def test_merge_outer_join(self, component): assert len(result) == 3 # ids 1, 2, 3 def test_merge_left_join(self, component): - """Test left merge keeps all records from first DataFrame.""" + """Test left merge keeps all records from left DataFrame.""" df1 = DataFrame(pd.DataFrame({"id": [1, 2, 3], "name": ["Alice", "Bob", "Charlie"]})) df2 = DataFrame(pd.DataFrame({"id": [2, 4], "city": ["NYC", "Chicago"]})) - component.df = [df1, df2] + component.left_dataframe = df1 + component.right_dataframe = df2 component.operation = [{"name": "Merge", "icon": "merge"}] component.merge_on_column = "id" component.merge_how = "left" result = component.perform_operation() - assert len(result) == 3 # All from df1 + assert len(result) == 3 # All from left (df1) def test_merge_right_join(self, component): - """Test right merge keeps all records from second DataFrame.""" + """Test right merge keeps all records from right DataFrame.""" df1 = DataFrame(pd.DataFrame({"id": [1, 2], "name": ["Alice", "Bob"]})) df2 = DataFrame(pd.DataFrame({"id": [2, 3, 4], "city": ["NYC", "LA", "Chicago"]})) - component.df = [df1, df2] + component.left_dataframe = df1 + component.right_dataframe = df2 component.operation = [{"name": "Merge", "icon": "merge"}] component.merge_on_column = "id" component.merge_how = "right" result = component.perform_operation() - assert len(result) == 3 # All from df2 + assert len(result) == 3 # All from right (df2) + + def test_should_preserve_left_rows_when_left_merge_with_explicit_inputs(self, component): + """Test that left merge deterministically preserves all rows from the explicit left DataFrame. + + This is the core bug fix test: with overlapping but distinct customer_ids, + a left merge must always keep all rows from left_dataframe regardless of + connection order. + """ + # Arrange — exact scenario from bug report + df_a = DataFrame( + pd.DataFrame( + { + "customer_id": ["CUST-001", "CUST-002", "CUST-003", "CUST-004"], + "name": ["Alice", "Bob", "Carol", "David"], + } + ) + ) + df_b = DataFrame( + pd.DataFrame( + { + "customer_id": ["CUST-001", "CUST-002", "CUST-005", "CUST-006"], + "product": ["Notebook", "Mouse", "Keyboard", "Monitor"], + } + ) + ) + + # Act — df_a is explicitly set as left, df_b as right + component.left_dataframe = df_a + component.right_dataframe = df_b + component.operation = [{"name": "Merge", "icon": "merge"}] + component.merge_on_column = "customer_id" + component.merge_how = "left" + + result = component.perform_operation() + + # Assert — all 4 rows from left (df_a) must be preserved + result_ids = sorted(result["customer_id"].tolist()) + assert result_ids == ["CUST-001", "CUST-002", "CUST-003", "CUST-004"] + assert len(result) == 4 + # CUST-003 and CUST-004 should have NaN product (not in df_b) + assert pd.isna(result.loc[result["customer_id"] == "CUST-003", "product"].iloc[0]) + assert pd.isna(result.loc[result["customer_id"] == "CUST-004", "product"].iloc[0]) + + def test_should_preserve_right_rows_when_right_merge_with_explicit_inputs(self, component): + """Test that right merge deterministically preserves all rows from the explicit right DataFrame.""" + # Arrange — same data, but now we want df_b's rows preserved + df_a = DataFrame( + pd.DataFrame( + { + "customer_id": ["CUST-001", "CUST-002", "CUST-003", "CUST-004"], + "name": ["Alice", "Bob", "Carol", "David"], + } + ) + ) + df_b = DataFrame( + pd.DataFrame( + { + "customer_id": ["CUST-001", "CUST-002", "CUST-005", "CUST-006"], + "product": ["Notebook", "Mouse", "Keyboard", "Monitor"], + } + ) + ) + + # Act — df_a is left, df_b is right, merge type is "right" + component.left_dataframe = df_a + component.right_dataframe = df_b + component.operation = [{"name": "Merge", "icon": "merge"}] + component.merge_on_column = "customer_id" + component.merge_how = "right" + + result = component.perform_operation() + + # Assert — all 4 rows from right (df_b) must be preserved + result_ids = sorted(result["customer_id"].tolist()) + assert result_ids == ["CUST-001", "CUST-002", "CUST-005", "CUST-006"] + assert len(result) == 4 + # CUST-005 and CUST-006 should have NaN name (not in df_a) + assert pd.isna(result.loc[result["customer_id"] == "CUST-005", "name"].iloc[0]) + assert pd.isna(result.loc[result["customer_id"] == "CUST-006", "name"].iloc[0]) + + def test_should_swap_results_when_left_right_inputs_are_swapped(self, component): + """Test that swapping left/right inputs produces different, deterministic results.""" + df_a = DataFrame( + pd.DataFrame( + { + "customer_id": ["CUST-001", "CUST-002", "CUST-003"], + "name": ["Alice", "Bob", "Carol"], + } + ) + ) + df_b = DataFrame( + pd.DataFrame( + { + "customer_id": ["CUST-002", "CUST-004"], + "city": ["NYC", "Chicago"], + } + ) + ) + + # Left merge with df_a as left + component.left_dataframe = df_a + component.right_dataframe = df_b + component.operation = [{"name": "Merge", "icon": "merge"}] + component.merge_on_column = "customer_id" + component.merge_how = "left" + result_a_left = component.perform_operation() + + # Left merge with df_b as left (swapped) + component.left_dataframe = df_b + component.right_dataframe = df_a + result_b_left = component.perform_operation() + + # Results must be different — df_a has 3 rows, df_b has 2 + assert len(result_a_left) == 3 # All from df_a + assert len(result_b_left) == 2 # All from df_b def test_merge_single_dataframe_returns_original(self, component): - """Test merge with single DataFrame returns it unchanged.""" + """Test merge with single left DataFrame and no right returns it unchanged.""" df1 = DataFrame(pd.DataFrame({"id": [1, 2], "name": ["Alice", "Bob"]})) - component.df = [df1] + component.left_dataframe = df1 + component.right_dataframe = None component.operation = [{"name": "Merge", "icon": "merge"}] component.merge_on_column = "id" component.merge_how = "inner" @@ -512,20 +640,22 @@ def test_merge_invalid_column_raises_error(self, component): df1 = DataFrame(pd.DataFrame({"id": [1, 2], "name": ["Alice", "Bob"]})) df2 = DataFrame(pd.DataFrame({"id": [2, 3], "city": ["NYC", "LA"]})) - component.df = [df1, df2] + component.left_dataframe = df1 + component.right_dataframe = df2 component.operation = [{"name": "Merge", "icon": "merge"}] component.merge_on_column = "non_existent" component.merge_how = "inner" - with pytest.raises(ValueError, match="not found in first DataFrame"): + with pytest.raises(ValueError, match="not found in left DataFrame"): component.perform_operation() def test_merge_same_columns_coalesces_values(self, component): - """Test merge with same columns uses coalesce (df1 value or df2 value).""" + """Test merge with same columns uses coalesce (left value or right value).""" df1 = DataFrame(pd.DataFrame({"id": [1, 2], "value": ["a", "b"]})) df2 = DataFrame(pd.DataFrame({"id": [2, 3], "value": ["x", "y"]})) - component.df = [df1, df2] + component.left_dataframe = df1 + component.right_dataframe = df2 component.operation = [{"name": "Merge", "icon": "merge"}] component.merge_on_column = "id" component.merge_how = "outer" @@ -533,26 +663,12 @@ def test_merge_same_columns_coalesces_values(self, component): result = component.perform_operation() assert len(result) == 3 - # Check no duplicate columns with _df2 suffix - assert "value_df2" not in result.columns + # Check no duplicate columns with _right suffix + assert "value_right" not in result.columns # Verify coalesced values - assert result.loc[result["id"] == 1, "value"].iloc[0] == "a" # from df1 - assert result.loc[result["id"] == 2, "value"].iloc[0] == "b" # from df1 (coalesced) - assert result.loc[result["id"] == 3, "value"].iloc[0] == "y" # from df2 - - def test_merge_more_than_two_dataframes_raises_error(self, component): - """Test merge with more than 2 DataFrames raises ValueError.""" - df1 = DataFrame(pd.DataFrame({"id": [1], "name": ["A"]})) - df2 = DataFrame(pd.DataFrame({"id": [2], "name": ["B"]})) - df3 = DataFrame(pd.DataFrame({"id": [3], "name": ["C"]})) - - component.df = [df1, df2, df3] - component.operation = [{"name": "Merge", "icon": "merge"}] - component.merge_on_column = "id" - component.merge_how = "inner" - - with pytest.raises(ValueError, match="Merge requires exactly"): - component.perform_operation() + assert result.loc[result["id"] == 1, "value"].iloc[0] == "a" # from left + assert result.loc[result["id"] == 2, "value"].iloc[0] == "b" # from left (coalesced) + assert result.loc[result["id"] == 3, "value"].iloc[0] == "y" # from right class TestListInputHandling: @@ -591,12 +707,16 @@ def test_merge_fields_show(self, component): "replacement_value": {"show": False}, "merge_on_column": {"show": False}, "merge_how": {"show": False}, + "left_dataframe": {"show": False}, + "right_dataframe": {"show": False}, } updated_config = component.update_build_config(build_config, [{"name": "Merge", "icon": "merge"}], "operation") assert updated_config["merge_on_column"]["show"] is True assert updated_config["merge_how"]["show"] is True + assert updated_config["left_dataframe"]["show"] is True + assert updated_config["right_dataframe"]["show"] is True assert updated_config["column_name"]["show"] is False def test_concatenate_hides_all_extra_fields(self, component): @@ -614,6 +734,8 @@ def test_concatenate_hides_all_extra_fields(self, component): "replacement_value": {"show": True}, "merge_on_column": {"show": True}, "merge_how": {"show": True}, + "left_dataframe": {"show": True}, + "right_dataframe": {"show": True}, } updated_config = component.update_build_config( @@ -624,6 +746,8 @@ def test_concatenate_hides_all_extra_fields(self, component): assert updated_config["column_name"]["show"] is False assert updated_config["merge_on_column"]["show"] is False assert updated_config["merge_how"]["show"] is False + assert updated_config["left_dataframe"]["show"] is False + assert updated_config["right_dataframe"]["show"] is False # Integration test to verify all operators work together diff --git a/src/lfx/src/lfx/_assets/component_index.json b/src/lfx/src/lfx/_assets/component_index.json index f7b6e163cade..2388627d45e1 100644 --- a/src/lfx/src/lfx/_assets/component_index.json +++ b/src/lfx/src/lfx/_assets/component_index.json @@ -100228,6 +100228,8 @@ "num_rows", "replace_value", "replacement_value", + "left_dataframe", + "right_dataframe", "merge_on_column", "merge_how" ], @@ -100235,7 +100237,7 @@ "icon": "table", "legacy": false, "metadata": { - "code_hash": "3a3aca2d9d1f", + "code_hash": "4df73846bdd3", "dependencies": { "dependencies": [ { @@ -100324,7 +100326,7 @@ "show": true, "title_case": false, "type": "code", - "value": "import pandas as pd\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs import SortableListInput\nfrom lfx.io import BoolInput, DataFrameInput, DropdownInput, IntInput, MessageTextInput, Output, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass DataFrameOperationsComponent(Component):\n display_name = \"Table Operations\"\n description = \"Perform various operations on a Table.\"\n documentation: str = \"https://docs.langflow.org/dataframe-operations\"\n icon = \"table\"\n name = \"DataFrameOperations\"\n metadata = {\n \"keywords\": [\n \"dataframe\",\n \"dataframe operations\",\n \"table\",\n \"table operations\",\n \"filter\",\n \"sort\",\n \"merge\",\n \"concatenate\",\n \"drop column\",\n \"rename column\",\n \"add column\",\n \"select columns\",\n \"replace value\",\n \"drop duplicates\",\n ],\n }\n\n OPERATION_CHOICES = [\n \"Add Column\",\n \"Concatenate\",\n \"Drop Column\",\n \"Filter\",\n \"Head\",\n \"Merge\",\n \"Rename Column\",\n \"Replace Value\",\n \"Select Columns\",\n \"Sort\",\n \"Tail\",\n \"Drop Duplicates\",\n ]\n\n inputs = [\n DataFrameInput(\n name=\"df\",\n display_name=\"Table\",\n info=\"The input DataFrame to operate on. Connect multiple DataFrames for merge or concatenate operations.\",\n required=True,\n is_list=True,\n ),\n SortableListInput(\n name=\"operation\",\n display_name=\"Operation\",\n placeholder=\"Select Operation\",\n info=\"Select the DataFrame operation to perform.\",\n options=[\n {\"name\": \"Add Column\", \"icon\": \"plus\"},\n {\"name\": \"Concatenate\", \"icon\": \"combine\"},\n {\"name\": \"Drop Column\", \"icon\": \"minus\"},\n {\"name\": \"Filter\", \"icon\": \"filter\"},\n {\"name\": \"Head\", \"icon\": \"arrow-up\"},\n {\"name\": \"Merge\", \"icon\": \"merge\"},\n {\"name\": \"Rename Column\", \"icon\": \"pencil\"},\n {\"name\": \"Replace Value\", \"icon\": \"replace\"},\n {\"name\": \"Select Columns\", \"icon\": \"columns\"},\n {\"name\": \"Sort\", \"icon\": \"arrow-up-down\"},\n {\"name\": \"Tail\", \"icon\": \"arrow-down\"},\n {\"name\": \"Drop Duplicates\", \"icon\": \"copy-x\"},\n ],\n real_time_refresh=True,\n limit=1,\n ),\n StrInput(\n name=\"column_name\",\n display_name=\"Column Name\",\n info=\"The column name to use for the operation.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"filter_value\",\n display_name=\"Filter Value\",\n info=\"The value to filter rows by.\",\n dynamic=True,\n show=False,\n ),\n DropdownInput(\n name=\"filter_operator\",\n display_name=\"Filter Operator\",\n options=[\n \"equals\",\n \"not equals\",\n \"contains\",\n \"not contains\",\n \"starts with\",\n \"ends with\",\n \"greater than\",\n \"less than\",\n ],\n value=\"equals\",\n info=\"The operator to apply for filtering rows.\",\n advanced=False,\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"ascending\",\n display_name=\"Sort Ascending\",\n info=\"Whether to sort in ascending order.\",\n dynamic=True,\n show=False,\n value=True,\n ),\n StrInput(\n name=\"new_column_name\",\n display_name=\"New Column Name\",\n info=\"The new column name when renaming or adding a column.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"new_column_value\",\n display_name=\"New Column Value\",\n info=\"The value to populate the new column with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"columns_to_select\",\n display_name=\"Columns to Select\",\n dynamic=True,\n is_list=True,\n show=False,\n ),\n IntInput(\n name=\"num_rows\",\n display_name=\"Number of Rows\",\n info=\"Number of rows to return (for head/tail).\",\n dynamic=True,\n show=False,\n value=5,\n ),\n MessageTextInput(\n name=\"replace_value\",\n display_name=\"Value to Replace\",\n info=\"The value to replace in the column.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"replacement_value\",\n display_name=\"Replacement Value\",\n info=\"The value to replace with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"merge_on_column\",\n display_name=\"Merge On Column\",\n info=\"The column name to merge DataFrames on. Must exist in both DataFrames.\",\n dynamic=True,\n show=False,\n ),\n DropdownInput(\n name=\"merge_how\",\n display_name=\"Merge Type\",\n options=[\"inner\", \"outer\", \"left\", \"right\"],\n value=\"inner\",\n info=\"Type of merge: inner (intersection), outer (union), left, or right.\",\n dynamic=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Table\",\n name=\"output\",\n method=\"perform_operation\",\n info=\"The resulting DataFrame after the operation.\",\n )\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n dynamic_fields = [\n \"column_name\",\n \"filter_value\",\n \"filter_operator\",\n \"ascending\",\n \"new_column_name\",\n \"new_column_value\",\n \"columns_to_select\",\n \"num_rows\",\n \"replace_value\",\n \"replacement_value\",\n \"merge_on_column\",\n \"merge_how\",\n ]\n for field in dynamic_fields:\n build_config[field][\"show\"] = False\n\n if field_name == \"operation\":\n # Handle SortableListInput format\n if isinstance(field_value, list):\n operation_name = field_value[0].get(\"name\", \"\") if field_value else \"\"\n else:\n operation_name = field_value or \"\"\n\n # If no operation selected, all dynamic fields stay hidden (already set to False above)\n if not operation_name:\n return build_config\n\n if operation_name == \"Filter\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"filter_value\"][\"show\"] = True\n build_config[\"filter_operator\"][\"show\"] = True\n elif operation_name == \"Sort\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"ascending\"][\"show\"] = True\n elif operation_name == \"Drop Column\":\n build_config[\"column_name\"][\"show\"] = True\n elif operation_name == \"Rename Column\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"new_column_name\"][\"show\"] = True\n elif operation_name == \"Add Column\":\n build_config[\"new_column_name\"][\"show\"] = True\n build_config[\"new_column_value\"][\"show\"] = True\n elif operation_name == \"Select Columns\":\n build_config[\"columns_to_select\"][\"show\"] = True\n elif operation_name in {\"Head\", \"Tail\"}:\n build_config[\"num_rows\"][\"show\"] = True\n elif operation_name == \"Replace Value\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"replace_value\"][\"show\"] = True\n build_config[\"replacement_value\"][\"show\"] = True\n elif operation_name == \"Drop Duplicates\":\n build_config[\"column_name\"][\"show\"] = True\n elif operation_name == \"Merge\":\n build_config[\"merge_on_column\"][\"show\"] = True\n build_config[\"merge_how\"][\"show\"] = True\n\n return build_config\n\n def _get_primary_dataframe(self) -> DataFrame:\n \"\"\"Get the first DataFrame from input (handles both single and list inputs).\"\"\"\n if isinstance(self.df, list):\n return self.df[0].copy() if self.df else DataFrame()\n return self.df.copy()\n\n def perform_operation(self) -> DataFrame:\n df_copy = self._get_primary_dataframe()\n\n # Handle SortableListInput format for operation (also supports legacy string format)\n operation_input = getattr(self, \"operation\", [])\n if isinstance(operation_input, list):\n op = operation_input[0].get(\"name\", \"\") if operation_input else \"\"\n else:\n op = operation_input or \"\"\n\n # If no operation selected, return original DataFrame\n if not op:\n return df_copy\n\n if op == \"Filter\":\n return self.filter_rows_by_value(df_copy)\n if op == \"Sort\":\n return self.sort_by_column(df_copy)\n if op == \"Drop Column\":\n return self.drop_column(df_copy)\n if op == \"Rename Column\":\n return self.rename_column(df_copy)\n if op == \"Add Column\":\n return self.add_column(df_copy)\n if op == \"Select Columns\":\n return self.select_columns(df_copy)\n if op == \"Head\":\n return self.head(df_copy)\n if op == \"Tail\":\n return self.tail(df_copy)\n if op == \"Replace Value\":\n return self.replace_values(df_copy)\n if op == \"Drop Duplicates\":\n return self.drop_duplicates(df_copy)\n if op == \"Concatenate\":\n return self.concatenate_dataframes()\n if op == \"Merge\":\n return self.merge_dataframes()\n msg = f\"Unsupported operation: {op}\"\n logger.error(msg)\n raise ValueError(msg)\n\n def filter_rows_by_value(self, df: DataFrame) -> DataFrame:\n column = df[self.column_name]\n filter_value = self.filter_value\n\n # Handle regular DropdownInput format (just a string value)\n operator = getattr(self, \"filter_operator\", \"equals\") # Default to equals for backward compatibility\n\n if operator == \"equals\":\n mask = column == filter_value\n elif operator == \"not equals\":\n mask = column != filter_value\n elif operator == \"contains\":\n mask = column.astype(str).str.contains(str(filter_value), na=False)\n elif operator == \"not contains\":\n mask = ~column.astype(str).str.contains(str(filter_value), na=False)\n elif operator == \"starts with\":\n mask = column.astype(str).str.startswith(str(filter_value), na=False)\n elif operator == \"ends with\":\n mask = column.astype(str).str.endswith(str(filter_value), na=False)\n elif operator == \"greater than\":\n try:\n # Try to convert filter_value to numeric for comparison\n numeric_value = pd.to_numeric(filter_value)\n mask = column > numeric_value\n except (ValueError, TypeError):\n # If conversion fails, compare as strings\n mask = column.astype(str) > str(filter_value)\n elif operator == \"less than\":\n try:\n # Try to convert filter_value to numeric for comparison\n numeric_value = pd.to_numeric(filter_value)\n mask = column < numeric_value\n except (ValueError, TypeError):\n # If conversion fails, compare as strings\n mask = column.astype(str) < str(filter_value)\n else:\n mask = column == filter_value # Fallback to equals\n\n return DataFrame(df[mask])\n\n def sort_by_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.sort_values(by=self.column_name, ascending=self.ascending))\n\n def drop_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.drop(columns=[self.column_name]))\n\n def rename_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.rename(columns={self.column_name: self.new_column_name}))\n\n def add_column(self, df: DataFrame) -> DataFrame:\n df[self.new_column_name] = [self.new_column_value] * len(df)\n return DataFrame(df)\n\n def select_columns(self, df: DataFrame) -> DataFrame:\n columns = [col.strip() for col in self.columns_to_select]\n return DataFrame(df[columns])\n\n def head(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.head(self.num_rows))\n\n def tail(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.tail(self.num_rows))\n\n def replace_values(self, df: DataFrame) -> DataFrame:\n df[self.column_name] = df[self.column_name].replace(self.replace_value, self.replacement_value)\n return DataFrame(df)\n\n def drop_duplicates(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.drop_duplicates(subset=self.column_name))\n\n def concatenate_dataframes(self) -> DataFrame:\n \"\"\"Concatenate multiple DataFrames vertically (stack rows).\"\"\"\n if not isinstance(self.df, list) or len(self.df) == 0:\n return self.df.copy() if self.df is not None else DataFrame()\n\n # If only one DataFrame, return it\n if len(self.df) == 1:\n return self.df[0].copy()\n\n # Concatenate all DataFrames vertically\n concatenated = pd.concat(self.df, ignore_index=True)\n return DataFrame(concatenated)\n\n def merge_dataframes(self) -> DataFrame:\n \"\"\"Merge two DataFrames based on a common column (join operation).\"\"\"\n if not isinstance(self.df, list) or len(self.df) == 0:\n return self.df.copy() if self.df is not None else DataFrame()\n\n # If only one DataFrame, return it\n if len(self.df) == 1:\n return self.df[0].copy()\n\n # Merge requires exactly two DataFrames\n max_merge_inputs = 2\n if len(self.df) > max_merge_inputs:\n msg = f\"Merge requires exactly {max_merge_inputs} DataFrames, got {len(self.df)}\"\n raise ValueError(msg)\n\n df1 = self.df[0].copy()\n df2 = self.df[1].copy()\n\n merge_on = getattr(self, \"merge_on_column\", None)\n merge_how = getattr(self, \"merge_how\", \"inner\")\n\n # If merge column specified, validate it exists in both DataFrames\n if merge_on:\n if merge_on not in df1.columns:\n msg = f\"Column '{merge_on}' not found in first DataFrame. Available: {list(df1.columns)}\"\n raise ValueError(msg)\n if merge_on not in df2.columns:\n msg = f\"Column '{merge_on}' not found in second DataFrame. Available: {list(df2.columns)}\"\n raise ValueError(msg)\n\n merged = df1.merge(df2, on=merge_on, how=merge_how, suffixes=(\"\", \"_df2\"))\n else:\n merged = df1.merge(df2, left_index=True, right_index=True, how=merge_how, suffixes=(\"\", \"_df2\"))\n\n # Combine duplicate columns: use df1 value if exists, otherwise df2 value\n cols_to_drop = []\n for col in merged.columns:\n if col.endswith(\"_df2\"):\n original_col = col[:-4] # Remove \"_df2\" suffix\n if original_col in merged.columns:\n # Coalesce: use original if not null, otherwise use _df2\n merged[original_col] = merged[original_col].combine_first(merged[col])\n cols_to_drop.append(col)\n\n if cols_to_drop:\n merged = merged.drop(columns=cols_to_drop)\n\n return DataFrame(merged)\n" + "value": "import pandas as pd\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.inputs import SortableListInput\nfrom lfx.io import BoolInput, DataFrameInput, DropdownInput, IntInput, MessageTextInput, Output, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.dataframe import DataFrame\n\n\nclass DataFrameOperationsComponent(Component):\n display_name = \"Table Operations\"\n description = \"Perform various operations on a Table.\"\n documentation: str = \"https://docs.langflow.org/dataframe-operations\"\n icon = \"table\"\n name = \"DataFrameOperations\"\n metadata = {\n \"keywords\": [\n \"dataframe\",\n \"dataframe operations\",\n \"table\",\n \"table operations\",\n \"filter\",\n \"sort\",\n \"merge\",\n \"concatenate\",\n \"drop column\",\n \"rename column\",\n \"add column\",\n \"select columns\",\n \"replace value\",\n \"drop duplicates\",\n ],\n }\n\n OPERATION_CHOICES = [\n \"Add Column\",\n \"Concatenate\",\n \"Drop Column\",\n \"Filter\",\n \"Head\",\n \"Merge\",\n \"Rename Column\",\n \"Replace Value\",\n \"Select Columns\",\n \"Sort\",\n \"Tail\",\n \"Drop Duplicates\",\n ]\n\n inputs = [\n DataFrameInput(\n name=\"df\",\n display_name=\"Table\",\n info=\"The input DataFrame to operate on. Connect multiple DataFrames for merge or concatenate operations.\",\n required=True,\n is_list=True,\n ),\n SortableListInput(\n name=\"operation\",\n display_name=\"Operation\",\n placeholder=\"Select Operation\",\n info=\"Select the DataFrame operation to perform.\",\n options=[\n {\"name\": \"Add Column\", \"icon\": \"plus\"},\n {\"name\": \"Concatenate\", \"icon\": \"combine\"},\n {\"name\": \"Drop Column\", \"icon\": \"minus\"},\n {\"name\": \"Filter\", \"icon\": \"filter\"},\n {\"name\": \"Head\", \"icon\": \"arrow-up\"},\n {\"name\": \"Merge\", \"icon\": \"merge\"},\n {\"name\": \"Rename Column\", \"icon\": \"pencil\"},\n {\"name\": \"Replace Value\", \"icon\": \"replace\"},\n {\"name\": \"Select Columns\", \"icon\": \"columns\"},\n {\"name\": \"Sort\", \"icon\": \"arrow-up-down\"},\n {\"name\": \"Tail\", \"icon\": \"arrow-down\"},\n {\"name\": \"Drop Duplicates\", \"icon\": \"copy-x\"},\n ],\n real_time_refresh=True,\n limit=1,\n ),\n StrInput(\n name=\"column_name\",\n display_name=\"Column Name\",\n info=\"The column name to use for the operation.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"filter_value\",\n display_name=\"Filter Value\",\n info=\"The value to filter rows by.\",\n dynamic=True,\n show=False,\n ),\n DropdownInput(\n name=\"filter_operator\",\n display_name=\"Filter Operator\",\n options=[\n \"equals\",\n \"not equals\",\n \"contains\",\n \"not contains\",\n \"starts with\",\n \"ends with\",\n \"greater than\",\n \"less than\",\n ],\n value=\"equals\",\n info=\"The operator to apply for filtering rows.\",\n advanced=False,\n dynamic=True,\n show=False,\n ),\n BoolInput(\n name=\"ascending\",\n display_name=\"Sort Ascending\",\n info=\"Whether to sort in ascending order.\",\n dynamic=True,\n show=False,\n value=True,\n ),\n StrInput(\n name=\"new_column_name\",\n display_name=\"New Column Name\",\n info=\"The new column name when renaming or adding a column.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"new_column_value\",\n display_name=\"New Column Value\",\n info=\"The value to populate the new column with.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"columns_to_select\",\n display_name=\"Columns to Select\",\n dynamic=True,\n is_list=True,\n show=False,\n ),\n IntInput(\n name=\"num_rows\",\n display_name=\"Number of Rows\",\n info=\"Number of rows to return (for head/tail).\",\n dynamic=True,\n show=False,\n value=5,\n ),\n MessageTextInput(\n name=\"replace_value\",\n display_name=\"Value to Replace\",\n info=\"The value to replace in the column.\",\n dynamic=True,\n show=False,\n ),\n MessageTextInput(\n name=\"replacement_value\",\n display_name=\"Replacement Value\",\n info=\"The value to replace with.\",\n dynamic=True,\n show=False,\n ),\n DataFrameInput(\n name=\"left_dataframe\",\n display_name=\"Left Table\",\n info=\"The left (primary) DataFrame for merge operations. \"\n \"In a left merge, all rows from this table are preserved.\",\n dynamic=True,\n show=False,\n ),\n DataFrameInput(\n name=\"right_dataframe\",\n display_name=\"Right Table\",\n info=\"The right (secondary) DataFrame for merge operations. \"\n \"In a right merge, all rows from this table are preserved.\",\n dynamic=True,\n show=False,\n ),\n StrInput(\n name=\"merge_on_column\",\n display_name=\"Merge On Column\",\n info=\"The column name to merge DataFrames on. Must exist in both DataFrames.\",\n dynamic=True,\n show=False,\n ),\n DropdownInput(\n name=\"merge_how\",\n display_name=\"Merge Type\",\n options=[\"inner\", \"outer\", \"left\", \"right\"],\n value=\"inner\",\n info=\"Type of merge: inner (intersection), outer (union), left, or right.\",\n dynamic=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(\n display_name=\"Table\",\n name=\"output\",\n method=\"perform_operation\",\n info=\"The resulting DataFrame after the operation.\",\n )\n ]\n\n def update_build_config(self, build_config, field_value, field_name=None):\n dynamic_fields = [\n \"column_name\",\n \"filter_value\",\n \"filter_operator\",\n \"ascending\",\n \"new_column_name\",\n \"new_column_value\",\n \"columns_to_select\",\n \"num_rows\",\n \"replace_value\",\n \"replacement_value\",\n \"left_dataframe\",\n \"right_dataframe\",\n \"merge_on_column\",\n \"merge_how\",\n ]\n for field in dynamic_fields:\n build_config[field][\"show\"] = False\n\n if field_name == \"operation\":\n # Handle SortableListInput format\n if isinstance(field_value, list):\n operation_name = field_value[0].get(\"name\", \"\") if field_value else \"\"\n else:\n operation_name = field_value or \"\"\n\n # If no operation selected, all dynamic fields stay hidden (already set to False above)\n if not operation_name:\n return build_config\n\n if operation_name == \"Filter\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"filter_value\"][\"show\"] = True\n build_config[\"filter_operator\"][\"show\"] = True\n elif operation_name == \"Sort\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"ascending\"][\"show\"] = True\n elif operation_name == \"Drop Column\":\n build_config[\"column_name\"][\"show\"] = True\n elif operation_name == \"Rename Column\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"new_column_name\"][\"show\"] = True\n elif operation_name == \"Add Column\":\n build_config[\"new_column_name\"][\"show\"] = True\n build_config[\"new_column_value\"][\"show\"] = True\n elif operation_name == \"Select Columns\":\n build_config[\"columns_to_select\"][\"show\"] = True\n elif operation_name in {\"Head\", \"Tail\"}:\n build_config[\"num_rows\"][\"show\"] = True\n elif operation_name == \"Replace Value\":\n build_config[\"column_name\"][\"show\"] = True\n build_config[\"replace_value\"][\"show\"] = True\n build_config[\"replacement_value\"][\"show\"] = True\n elif operation_name == \"Drop Duplicates\":\n build_config[\"column_name\"][\"show\"] = True\n elif operation_name == \"Merge\":\n build_config[\"left_dataframe\"][\"show\"] = True\n build_config[\"right_dataframe\"][\"show\"] = True\n build_config[\"merge_on_column\"][\"show\"] = True\n build_config[\"merge_how\"][\"show\"] = True\n\n return build_config\n\n def _get_primary_dataframe(self) -> DataFrame:\n \"\"\"Get the first DataFrame from input (handles both single and list inputs).\"\"\"\n if isinstance(self.df, list):\n return self.df[0].copy() if self.df else DataFrame()\n return self.df.copy()\n\n def perform_operation(self) -> DataFrame:\n # Handle SortableListInput format for operation (also supports legacy string format)\n operation_input = getattr(self, \"operation\", [])\n if isinstance(operation_input, list):\n op = operation_input[0].get(\"name\", \"\") if operation_input else \"\"\n else:\n op = operation_input or \"\"\n\n # Merge and Concatenate use their own inputs, not the primary df\n if op == \"Merge\":\n return self.merge_dataframes()\n if op == \"Concatenate\":\n return self.concatenate_dataframes()\n\n df_copy = self._get_primary_dataframe()\n\n # If no operation selected, return original DataFrame\n if not op:\n return df_copy\n\n if op == \"Filter\":\n return self.filter_rows_by_value(df_copy)\n if op == \"Sort\":\n return self.sort_by_column(df_copy)\n if op == \"Drop Column\":\n return self.drop_column(df_copy)\n if op == \"Rename Column\":\n return self.rename_column(df_copy)\n if op == \"Add Column\":\n return self.add_column(df_copy)\n if op == \"Select Columns\":\n return self.select_columns(df_copy)\n if op == \"Head\":\n return self.head(df_copy)\n if op == \"Tail\":\n return self.tail(df_copy)\n if op == \"Replace Value\":\n return self.replace_values(df_copy)\n if op == \"Drop Duplicates\":\n return self.drop_duplicates(df_copy)\n msg = f\"Unsupported operation: {op}\"\n logger.error(msg)\n raise ValueError(msg)\n\n def filter_rows_by_value(self, df: DataFrame) -> DataFrame:\n column = df[self.column_name]\n filter_value = self.filter_value\n\n # Handle regular DropdownInput format (just a string value)\n operator = getattr(self, \"filter_operator\", \"equals\") # Default to equals for backward compatibility\n\n if operator == \"equals\":\n mask = column == filter_value\n elif operator == \"not equals\":\n mask = column != filter_value\n elif operator == \"contains\":\n mask = column.astype(str).str.contains(str(filter_value), na=False)\n elif operator == \"not contains\":\n mask = ~column.astype(str).str.contains(str(filter_value), na=False)\n elif operator == \"starts with\":\n mask = column.astype(str).str.startswith(str(filter_value), na=False)\n elif operator == \"ends with\":\n mask = column.astype(str).str.endswith(str(filter_value), na=False)\n elif operator == \"greater than\":\n try:\n # Try to convert filter_value to numeric for comparison\n numeric_value = pd.to_numeric(filter_value)\n mask = column > numeric_value\n except (ValueError, TypeError):\n # If conversion fails, compare as strings\n mask = column.astype(str) > str(filter_value)\n elif operator == \"less than\":\n try:\n # Try to convert filter_value to numeric for comparison\n numeric_value = pd.to_numeric(filter_value)\n mask = column < numeric_value\n except (ValueError, TypeError):\n # If conversion fails, compare as strings\n mask = column.astype(str) < str(filter_value)\n else:\n mask = column == filter_value # Fallback to equals\n\n return DataFrame(df[mask])\n\n def sort_by_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.sort_values(by=self.column_name, ascending=self.ascending))\n\n def drop_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.drop(columns=[self.column_name]))\n\n def rename_column(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.rename(columns={self.column_name: self.new_column_name}))\n\n def add_column(self, df: DataFrame) -> DataFrame:\n df[self.new_column_name] = [self.new_column_value] * len(df)\n return DataFrame(df)\n\n def select_columns(self, df: DataFrame) -> DataFrame:\n columns = [col.strip() for col in self.columns_to_select]\n return DataFrame(df[columns])\n\n def head(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.head(self.num_rows))\n\n def tail(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.tail(self.num_rows))\n\n def replace_values(self, df: DataFrame) -> DataFrame:\n df[self.column_name] = df[self.column_name].replace(self.replace_value, self.replacement_value)\n return DataFrame(df)\n\n def drop_duplicates(self, df: DataFrame) -> DataFrame:\n return DataFrame(df.drop_duplicates(subset=self.column_name))\n\n def concatenate_dataframes(self) -> DataFrame:\n \"\"\"Concatenate multiple DataFrames vertically (stack rows).\"\"\"\n if not isinstance(self.df, list) or len(self.df) == 0:\n return self.df.copy() if self.df is not None else DataFrame()\n\n # If only one DataFrame, return it\n if len(self.df) == 1:\n return self.df[0].copy()\n\n # Concatenate all DataFrames vertically\n concatenated = pd.concat(self.df, ignore_index=True)\n return DataFrame(concatenated)\n\n def merge_dataframes(self) -> DataFrame:\n \"\"\"Merge two DataFrames based on a common column (join operation).\n\n Uses explicit left_dataframe and right_dataframe inputs to give the user\n deterministic control over which DataFrame is primary (left) and which\n is secondary (right) in the merge.\n \"\"\"\n left_df = getattr(self, \"left_dataframe\", None)\n right_df = getattr(self, \"right_dataframe\", None)\n\n if left_df is None:\n return DataFrame()\n\n if right_df is None:\n return left_df.copy()\n\n df_left = left_df.copy()\n df_right = right_df.copy()\n\n merge_on = getattr(self, \"merge_on_column\", None)\n merge_how = getattr(self, \"merge_how\", \"inner\")\n\n if merge_on:\n if merge_on not in df_left.columns:\n msg = f\"Column '{merge_on}' not found in left DataFrame. Available: {list(df_left.columns)}\"\n raise ValueError(msg)\n if merge_on not in df_right.columns:\n msg = f\"Column '{merge_on}' not found in right DataFrame. Available: {list(df_right.columns)}\"\n raise ValueError(msg)\n\n merged = df_left.merge(df_right, on=merge_on, how=merge_how, suffixes=(\"\", \"_right\"))\n else:\n merged = df_left.merge(df_right, left_index=True, right_index=True, how=merge_how, suffixes=(\"\", \"_right\"))\n\n # Combine duplicate columns: use left value if exists, otherwise right value\n cols_to_drop = []\n for col in merged.columns:\n if col.endswith(\"_right\"):\n original_col = col[:-6] # Remove \"_right\" suffix\n if original_col in merged.columns:\n merged[original_col] = merged[original_col].combine_first(merged[col])\n cols_to_drop.append(col)\n\n if cols_to_drop:\n merged = merged.drop(columns=cols_to_drop)\n\n return DataFrame(merged)\n" }, "column_name": { "_input_type": "StrInput", @@ -100451,6 +100453,31 @@ "type": "str", "value": "" }, + "left_dataframe": { + "_input_type": "DataFrameInput", + "advanced": false, + "display_name": "Left Table", + "dynamic": true, + "info": "The left (primary) DataFrame for merge operations. In a left merge, all rows from this table are preserved.", + "input_types": [ + "DataFrame", + "Table" + ], + "list": false, + "list_add_label": "Add More", + "name": "left_dataframe", + "override_skip": false, + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" + }, "merge_how": { "_input_type": "DropdownInput", "advanced": false, @@ -100687,6 +100714,31 @@ "track_in_telemetry": false, "type": "str", "value": "" + }, + "right_dataframe": { + "_input_type": "DataFrameInput", + "advanced": false, + "display_name": "Right Table", + "dynamic": true, + "info": "The right (secondary) DataFrame for merge operations. In a right merge, all rows from this table are preserved.", + "input_types": [ + "DataFrame", + "Table" + ], + "list": false, + "list_add_label": "Add More", + "name": "right_dataframe", + "override_skip": false, + "placeholder": "", + "required": false, + "show": false, + "title_case": false, + "tool_mode": false, + "trace_as_input": true, + "trace_as_metadata": true, + "track_in_telemetry": false, + "type": "other", + "value": "" } }, "tool_mode": false @@ -118487,6 +118539,6 @@ "num_components": 359, "num_modules": 97 }, - "sha256": "dcb9a6e44e2405967f6fd4881e5bc5d3748bb507b3a0e57fc90c9b86c2536730", + "sha256": "13f65a29f6c2ff9456219241d124739b669f2d58199107d961e515d1da6a73e4", "version": "0.3.1" } \ No newline at end of file diff --git a/src/lfx/src/lfx/components/processing/dataframe_operations.py b/src/lfx/src/lfx/components/processing/dataframe_operations.py index ffc8c5298b93..2646162117f1 100644 --- a/src/lfx/src/lfx/components/processing/dataframe_operations.py +++ b/src/lfx/src/lfx/components/processing/dataframe_operations.py @@ -161,6 +161,22 @@ class DataFrameOperationsComponent(Component): dynamic=True, show=False, ), + DataFrameInput( + name="left_dataframe", + display_name="Left Table", + info="The left (primary) DataFrame for merge operations. " + "In a left merge, all rows from this table are preserved.", + dynamic=True, + show=False, + ), + DataFrameInput( + name="right_dataframe", + display_name="Right Table", + info="The right (secondary) DataFrame for merge operations. " + "In a right merge, all rows from this table are preserved.", + dynamic=True, + show=False, + ), StrInput( name="merge_on_column", display_name="Merge On Column", @@ -200,6 +216,8 @@ def update_build_config(self, build_config, field_value, field_name=None): "num_rows", "replace_value", "replacement_value", + "left_dataframe", + "right_dataframe", "merge_on_column", "merge_how", ] @@ -243,6 +261,8 @@ def update_build_config(self, build_config, field_value, field_name=None): elif operation_name == "Drop Duplicates": build_config["column_name"]["show"] = True elif operation_name == "Merge": + build_config["left_dataframe"]["show"] = True + build_config["right_dataframe"]["show"] = True build_config["merge_on_column"]["show"] = True build_config["merge_how"]["show"] = True @@ -255,8 +275,6 @@ def _get_primary_dataframe(self) -> DataFrame: return self.df.copy() def perform_operation(self) -> DataFrame: - df_copy = self._get_primary_dataframe() - # Handle SortableListInput format for operation (also supports legacy string format) operation_input = getattr(self, "operation", []) if isinstance(operation_input, list): @@ -264,6 +282,14 @@ def perform_operation(self) -> DataFrame: else: op = operation_input or "" + # Merge and Concatenate use their own inputs, not the primary df + if op == "Merge": + return self.merge_dataframes() + if op == "Concatenate": + return self.concatenate_dataframes() + + df_copy = self._get_primary_dataframe() + # If no operation selected, return original DataFrame if not op: return df_copy @@ -288,10 +314,6 @@ def perform_operation(self) -> DataFrame: return self.replace_values(df_copy) if op == "Drop Duplicates": return self.drop_duplicates(df_copy) - if op == "Concatenate": - return self.concatenate_dataframes() - if op == "Merge": - return self.merge_dataframes() msg = f"Unsupported operation: {op}" logger.error(msg) raise ValueError(msg) @@ -380,46 +402,45 @@ def concatenate_dataframes(self) -> DataFrame: return DataFrame(concatenated) def merge_dataframes(self) -> DataFrame: - """Merge two DataFrames based on a common column (join operation).""" - if not isinstance(self.df, list) or len(self.df) == 0: - return self.df.copy() if self.df is not None else DataFrame() + """Merge two DataFrames based on a common column (join operation). - # If only one DataFrame, return it - if len(self.df) == 1: - return self.df[0].copy() + Uses explicit left_dataframe and right_dataframe inputs to give the user + deterministic control over which DataFrame is primary (left) and which + is secondary (right) in the merge. + """ + left_df = getattr(self, "left_dataframe", None) + right_df = getattr(self, "right_dataframe", None) + + if left_df is None: + return DataFrame() - # Merge requires exactly two DataFrames - max_merge_inputs = 2 - if len(self.df) > max_merge_inputs: - msg = f"Merge requires exactly {max_merge_inputs} DataFrames, got {len(self.df)}" - raise ValueError(msg) + if right_df is None: + return left_df.copy() - df1 = self.df[0].copy() - df2 = self.df[1].copy() + df_left = left_df.copy() + df_right = right_df.copy() merge_on = getattr(self, "merge_on_column", None) merge_how = getattr(self, "merge_how", "inner") - # If merge column specified, validate it exists in both DataFrames if merge_on: - if merge_on not in df1.columns: - msg = f"Column '{merge_on}' not found in first DataFrame. Available: {list(df1.columns)}" + if merge_on not in df_left.columns: + msg = f"Column '{merge_on}' not found in left DataFrame. Available: {list(df_left.columns)}" raise ValueError(msg) - if merge_on not in df2.columns: - msg = f"Column '{merge_on}' not found in second DataFrame. Available: {list(df2.columns)}" + if merge_on not in df_right.columns: + msg = f"Column '{merge_on}' not found in right DataFrame. Available: {list(df_right.columns)}" raise ValueError(msg) - merged = df1.merge(df2, on=merge_on, how=merge_how, suffixes=("", "_df2")) + merged = df_left.merge(df_right, on=merge_on, how=merge_how, suffixes=("", "_right")) else: - merged = df1.merge(df2, left_index=True, right_index=True, how=merge_how, suffixes=("", "_df2")) + merged = df_left.merge(df_right, left_index=True, right_index=True, how=merge_how, suffixes=("", "_right")) - # Combine duplicate columns: use df1 value if exists, otherwise df2 value + # Combine duplicate columns: use left value if exists, otherwise right value cols_to_drop = [] for col in merged.columns: - if col.endswith("_df2"): - original_col = col[:-4] # Remove "_df2" suffix + if col.endswith("_right"): + original_col = col[:-6] # Remove "_right" suffix if original_col in merged.columns: - # Coalesce: use original if not null, otherwise use _df2 merged[original_col] = merged[original_col].combine_first(merged[col]) cols_to_drop.append(col) From 7c5bda40b8543bcb365f6d745066c80a71d9ea32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Alexandre=20Borges=20Lima?= <104531655+AntonioABLima@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:38:31 -0300 Subject: [PATCH 27/29] fix: add dict to allowlist preventing TableInput data loss (#12074) * fix: add dict to allowlist preventing TableInput data loss Co-Authored-By: DeyLak * test: add regression test for TableInput list[dict] preservation Regression test for: https://github.com/langflow-ai/langflow/issues/12062 Co-Authored-By: DeyLak --------- Co-authored-by: DeyLak Co-authored-by: Cristhian Zanforlin Lousa --- .../unit/base/tools/test_toolmodemixin.py | 21 +++++++++++++++++++ .../lfx/custom/custom_component/component.py | 3 ++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/backend/tests/unit/base/tools/test_toolmodemixin.py b/src/backend/tests/unit/base/tools/test_toolmodemixin.py index f9e9453a7081..562bdafeadce 100644 --- a/src/backend/tests/unit/base/tools/test_toolmodemixin.py +++ b/src/backend/tests/unit/base/tools/test_toolmodemixin.py @@ -153,3 +153,24 @@ def test_component_inputs_toolkit(): assert properties[input_name]["description"] == expected["description"], ( f"Description mismatch for {input_name}." ) + + +def test_table_input_preserves_list_dict(): + """Test that TableInput with tool_mode=True preserves all dicts in a list. + + Regression test for: https://github.com/langflow-ai/langflow/issues/12062 + """ + component = AllInputsComponent() + + # Simulate passing a list of dicts + test_data = [ + {"field": "age", "value": "5"}, + {"field": "status", "value": "active"}, + {"field": "weight", "value": "500"}, + ] + + component.set(table_input=test_data) + + # Verify all items are preserved (not just the last one) + assert component.table_input == test_data + assert len(component.table_input) == 3 diff --git a/src/lfx/src/lfx/custom/custom_component/component.py b/src/lfx/src/lfx/custom/custom_component/component.py index 8003e360ba3b..c7a4a12fb709 100644 --- a/src/lfx/src/lfx/custom/custom_component/component.py +++ b/src/lfx/src/lfx/custom/custom_component/component.py @@ -843,7 +843,8 @@ def _process_connection_or_parameters(self, key, value) -> None: # if value is a list of components, we need to process each component # Note this update make sure it is not a list str | int | float | bool | type(None) if isinstance(value, list) and not any( - isinstance(val, str | int | float | bool | type(None) | Message | Data | StructuredTool) for val in value + isinstance(val, str | int | float | bool | type(None) | Message | Data | StructuredTool | dict) + for val in value ): for val in value: self._process_connection_or_parameter(key, val) From facd0b1ea7e67cbceae7f3e53e99b207c5279cd4 Mon Sep 17 00:00:00 2001 From: Adam Aghili Date: Tue, 17 Mar 2026 01:47:57 -0400 Subject: [PATCH 28/29] chore: update pyproject versions 1.9.0 update pyproject versions 1.9.0 --- pyproject.toml | 4 +- src/backend/base/pyproject.toml | 4 +- src/frontend/package-lock.json | 43 +------ src/frontend/package.json | 2 +- src/lfx/pyproject.toml | 2 +- uv.lock | 216 ++++++++++++++++---------------- 6 files changed, 116 insertions(+), 155 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fe79631d0950..3c820bab0491 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langflow" -version = "1.8.1" +version = "1.9.0" description = "A Python package with a built-in web application" requires-python = ">=3.10,<3.14" license = "MIT" @@ -17,7 +17,7 @@ maintainers = [ ] # Define your main dependencies here dependencies = [ - "langflow-base[complete]~=0.8.1", + "langflow-base[complete]~=0.9.0", ] diff --git a/src/backend/base/pyproject.toml b/src/backend/base/pyproject.toml index fac8fc792a4d..c8ea735777f7 100644 --- a/src/backend/base/pyproject.toml +++ b/src/backend/base/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "langflow-base" -version = "0.8.1" +version = "0.9.0" description = "A Python package with a built-in web application" requires-python = ">=3.10,<3.14" license = "MIT" @@ -17,7 +17,7 @@ maintainers = [ ] dependencies = [ - "lfx~=0.3.1", + "lfx~=0.4.0", "fastapi>=0.135.0,<1.0.0", "httpx[http2]>=0.27,<1.0.0", "aiofile>=3.9.0,<4.0.0", diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 04d90cdae059..ab4fa5315030 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "langflow", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "langflow", - "version": "1.8.1", + "version": "1.9.0", "dependencies": { "@chakra-ui/number-input": "^2.1.2", "@chakra-ui/system": "^2.6.2", @@ -210,7 +210,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1069,7 +1068,6 @@ "resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.9.2.tgz", "integrity": "sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==", "license": "MIT", - "peer": true, "dependencies": { "@chakra-ui/shared-utils": "2.0.5", "csstype": "^3.1.2", @@ -1081,7 +1079,6 @@ "resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-2.6.2.tgz", "integrity": "sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==", "license": "MIT", - "peer": true, "dependencies": { "@chakra-ui/color-mode": "2.2.0", "@chakra-ui/object-utils": "2.1.0", @@ -1188,7 +1185,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -1225,7 +1221,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -1235,7 +1230,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz", "integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1331,7 +1325,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1355,7 +1348,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -5991,7 +5983,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -6362,7 +6353,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -7074,7 +7064,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -7085,7 +7074,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -7101,8 +7089,7 @@ "version": "1.15.9", "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/stack-utils": { "version": "2.0.3", @@ -7727,7 +7714,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8374,7 +8360,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8711,7 +8696,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", - "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -9135,7 +9119,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -9677,7 +9660,6 @@ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -9915,7 +9897,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -10974,7 +10955,6 @@ "integrity": "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -11587,7 +11567,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -13189,7 +13168,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -13239,7 +13217,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -13279,7 +13256,6 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 10.16.0" } @@ -15559,7 +15535,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15966,7 +15941,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -16039,7 +16013,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -16070,7 +16043,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -16676,7 +16648,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -17187,8 +17158,7 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/source-map": { "version": "0.5.7", @@ -17308,7 +17278,6 @@ "integrity": "sha512-heMfJjOfbHvL+wlCAwFZlSxcakyJ5yQDam6e9k2RRArB1veJhRnsjO6lO1hOXjJYrqxfHA/ldIugbBVlCDqfvQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -17677,7 +17646,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -18291,7 +18259,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18802,7 +18769,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -19711,7 +19677,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/frontend/package.json b/src/frontend/package.json index 6185adb5b00d..39a1ee15591d 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,6 +1,6 @@ { "name": "langflow", - "version": "1.8.1", + "version": "1.9.0", "private": true, "engines": { "node": ">=20.19.0" diff --git a/src/lfx/pyproject.toml b/src/lfx/pyproject.toml index 864a4edc3dbc..17f5b4b86eac 100644 --- a/src/lfx/pyproject.toml +++ b/src/lfx/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lfx" -version = "0.3.1" +version = "0.4.0" description = "Langflow Executor - A lightweight CLI tool for executing and serving Langflow AI flows" readme = "README.md" authors = [ diff --git a/uv.lock b/uv.lock index 702499301a7f..cbb12f7fa7c2 100644 --- a/uv.lock +++ b/uv.lock @@ -2438,26 +2438,22 @@ name = "easyocr" version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ninja" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "opencv-python-headless", version = "4.13.0.92", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pillow" }, - { name = "pyclipper" }, - { name = "python-bidi" }, - { name = "pyyaml" }, - { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "shapely" }, - { name = "torch", version = "2.2.2", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "torch", version = "2.2.2+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "ninja", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine != 'x86_64') or (python_full_version < '3.12' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, + { name = "opencv-python-headless", version = "4.11.0.86", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine != 'x86_64') or (python_full_version < '3.12' and sys_platform != 'darwin')" }, + { name = "opencv-python-headless", version = "4.13.0.92", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, + { name = "pillow", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "pyclipper", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "python-bidi", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "pyyaml", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "shapely", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, - { name = "torchvision", version = "0.17.2", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "torchvision", version = "0.17.2+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "torchvision", version = "0.25.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine != 'x86_64' and sys_platform == 'darwin'" }, { name = "torchvision", version = "0.25.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, ] @@ -3262,10 +3258,10 @@ name = "gassist" version = "0.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama" }, - { name = "flask" }, - { name = "flask-cors" }, - { name = "tqdm" }, + { name = "colorama", marker = "(python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux') or sys_platform == 'win32'" }, + { name = "flask", marker = "(python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux') or sys_platform == 'win32'" }, + { name = "flask-cors", marker = "(python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux') or sys_platform == 'win32'" }, + { name = "tqdm", marker = "(python_full_version < '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'linux') or sys_platform == 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/b0/2e/f79632d7300874f7f0e60b61a6ab22455a245e1556116a1729542a77b0da/gassist-0.0.1-py3-none-any.whl", hash = "sha256:bb0fac74b453153a6c74b2db40a14fdde7879cbc10ec692ed170e576c8e2b6aa", size = 23819, upload-time = "2025-05-09T18:22:23.609Z" }, @@ -4429,9 +4425,9 @@ name = "imageio" version = "2.37.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pillow" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine != 'x86_64') or (python_full_version < '3.12' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, + { name = "pillow", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } wheels = [ @@ -5567,7 +5563,7 @@ wheels = [ [[package]] name = "langflow" -version = "1.8.1" +version = "1.9.0" source = { editable = "." } dependencies = [ { name = "langflow-base", extra = ["complete"] }, @@ -5743,7 +5739,7 @@ dev = [ [[package]] name = "langflow-base" -version = "0.8.1" +version = "0.9.0" source = { editable = "src/backend/base" } dependencies = [ { name = "aiofile" }, @@ -6912,7 +6908,7 @@ name = "lazy-loader" version = "0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging" }, + { name = "packaging", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } wheels = [ @@ -6921,7 +6917,7 @@ wheels = [ [[package]] name = "lfx" -version = "0.3.1" +version = "0.4.0" source = { editable = "src/lfx" } dependencies = [ { name = "ag-ui-protocol" }, @@ -7724,7 +7720,7 @@ name = "mlx" version = "0.31.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mlx-metal", marker = "python_full_version >= '3.12' and sys_platform == 'darwin'" }, + { name = "mlx-metal", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f9/f1663dafd45af02467f4f41777c13ec34b9104b2b0450d870c3f906285cd/mlx-0.31.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:bc46c911cc060d2eaf21b9e24a1712dc56763b660b53631b9057a32ab1c0271a", size = 574137, upload-time = "2026-03-12T02:15:54.996Z" }, @@ -7746,13 +7742,13 @@ name = "mlx-lm" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "mlx", marker = "python_full_version >= '3.12' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "protobuf", marker = "python_full_version >= '3.12'" }, - { name = "pyyaml", marker = "python_full_version >= '3.12'" }, - { name = "sentencepiece", marker = "python_full_version >= '3.12'" }, - { name = "transformers", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "mlx", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "protobuf", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "pyyaml", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "sentencepiece", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "transformers", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/62/f46e1355256a114808517947f8e83ad6be310c7288c551db0fa678f47923/mlx_lm-0.29.1.tar.gz", hash = "sha256:b99180d8f33d33a077b814e550bfb2d8a59ae003d668fd1f4b3fff62a381d34b", size = 232302, upload-time = "2025-12-16T16:58:27.959Z" } wheels = [ @@ -7774,19 +7770,19 @@ name = "mlx-vlm" version = "0.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "datasets", marker = "python_full_version >= '3.12'" }, - { name = "fastapi", marker = "python_full_version >= '3.12'" }, - { name = "mlx", marker = "python_full_version >= '3.12'" }, - { name = "mlx-lm", marker = "python_full_version >= '3.12'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "opencv-python", version = "4.13.0.92", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pillow", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "soundfile", marker = "python_full_version >= '3.12'" }, - { name = "tqdm", marker = "python_full_version >= '3.12'" }, - { name = "transformers", marker = "python_full_version >= '3.12'" }, - { name = "uvicorn", marker = "python_full_version >= '3.12'" }, + { name = "datasets", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "fastapi", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "mlx", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "mlx-lm", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "opencv-python", version = "4.13.0.92", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "pillow", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "requests", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "soundfile", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "tqdm", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "transformers", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "uvicorn", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ff/9f/de419334820da334203de28eaf861b57ae0d06b0882770e5e5d0671dc5dd/mlx_vlm-0.3.3.tar.gz", hash = "sha256:5a08c802d1bf32cc47bd6aebe348d3554ce21bfce417a585bba83f9d213a6e66", size = 231935, upload-time = "2025-08-20T14:52:51.323Z" } wheels = [ @@ -8471,9 +8467,9 @@ name = "ocrmac" version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "pillow" }, - { name = "pyobjc-framework-vision" }, + { name = "click", marker = "sys_platform == 'darwin'" }, + { name = "pillow", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-vision", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/07/3e15ab404f75875c5e48c47163300eb90b7409044d8711fc3aaf52503f2e/ocrmac-1.0.1.tar.gz", hash = "sha256:507fe5e4cbd67b2d03f6729a52bbc11f9d0b58241134eb958a5daafd4b9d93d9", size = 1454317, upload-time = "2026-01-08T16:44:26.412Z" } wheels = [ @@ -8642,7 +8638,7 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine != 'x86_64') or (python_full_version < '3.12' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } wheels = [ @@ -8666,7 +8662,7 @@ resolution-markers = [ "python_full_version == '3.12.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, @@ -9879,7 +9875,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "python_full_version < '3.13' or sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -11139,7 +11135,7 @@ name = "pyobjc-framework-cocoa" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } wheels = [ @@ -11155,8 +11151,8 @@ name = "pyobjc-framework-coreml" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/2d/baa9ea02cbb1c200683cb7273b69b4bee5070e86f2060b77e6a27c2a9d7e/pyobjc_framework_coreml-12.1.tar.gz", hash = "sha256:0d1a4216891a18775c9e0170d908714c18e4f53f9dc79fb0f5263b2aa81609ba", size = 40465, upload-time = "2025-11-14T10:14:02.265Z" } wheels = [ @@ -11172,8 +11168,8 @@ name = "pyobjc-framework-quartz" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } wheels = [ @@ -11189,10 +11185,10 @@ name = "pyobjc-framework-vision" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreml" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreml", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/5a/08bb3e278f870443d226c141af14205ff41c0274da1e053b72b11dfc9fb2/pyobjc_framework_vision-12.1.tar.gz", hash = "sha256:a30959100e85dcede3a786c544e621ad6eb65ff6abf85721f805822b8c5fe9b0", size = 59538, upload-time = "2025-11-14T10:23:21.979Z" } wheels = [ @@ -12641,14 +12637,14 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "imageio", marker = "python_full_version < '3.11'" }, - { name = "lazy-loader", marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pillow", marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "imageio", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "lazy-loader", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "packaging", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "pillow", marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } wheels = [ @@ -12690,15 +12686,15 @@ resolution-markers = [ "python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "imageio", marker = "python_full_version >= '3.11'" }, - { name = "lazy-loader", marker = "python_full_version >= '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pillow", marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "imageio", marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "lazy-loader", marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, + { name = "packaging", marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "pillow", marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } wheels = [ @@ -13270,8 +13266,8 @@ name = "soundfile" version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "python_full_version >= '3.12'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "cffi", marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and platform_machine != 'x86_64' and sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } wheels = [ @@ -13599,7 +13595,7 @@ resolution-markers = [ "python_full_version < '3.11' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } wheels = [ @@ -13621,8 +13617,8 @@ resolution-markers = [ "python_full_version == '3.11.*' and platform_machine != 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } wheels = [ @@ -13909,9 +13905,9 @@ dependencies = [ { name = "torch", version = "2.2.2", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.17.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:1f2910fe3c21ad6875b2720d46fad835b2e4b336e9553d31ca364d24c90b1d4f" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.17.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:9b83e55ee7d0a1704f52b9c0ac87388e7a6d1d98a6bde7b0b35f9ab54d7bda54" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14fd1d4a033c325bdba2d03a69c3450cab6d3a625f85cc375781d9237ca5d04d" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.17.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:1f2910fe3c21ad6875b2720d46fad835b2e4b336e9553d31ca364d24c90b1d4f" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.17.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:9b83e55ee7d0a1704f52b9c0ac87388e7a6d1d98a6bde7b0b35f9ab54d7bda54" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14fd1d4a033c325bdba2d03a69c3450cab6d3a625f85cc375781d9237ca5d04d" }, ] [[package]] @@ -13944,11 +13940,11 @@ dependencies = [ { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "platform_machine != 'x86_64' and sys_platform == 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7810841916a2a870cbfb581f4084b59479908fb9db6edfdf78139931392d8677" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a76ce7b8d4fce291a25721ee2f921c783acc6dbd4fc32dc741ed2a1d5a8dde2f" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:724f212a58a0d0d758649ce288601056b5f46a01de545702f42bccc5b25cb0cc" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6c444119c6af8fa48b79c59f1529fc15a45a2bbf823cf85f851e7203d84f727f" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:80895a40faa5783ce19ed5a692a54062c7888a385490e866324355f8d59b2eb3" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7810841916a2a870cbfb581f4084b59479908fb9db6edfdf78139931392d8677" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a76ce7b8d4fce291a25721ee2f921c783acc6dbd4fc32dc741ed2a1d5a8dde2f" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:724f212a58a0d0d758649ce288601056b5f46a01de545702f42bccc5b25cb0cc" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6c444119c6af8fa48b79c59f1529fc15a45a2bbf823cf85f851e7203d84f727f" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:80895a40faa5783ce19ed5a692a54062c7888a385490e866324355f8d59b2eb3" }, ] [[package]] @@ -13971,21 +13967,21 @@ dependencies = [ { name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:73ce04dea64914ff1110008311204c366107d651d3b04faa0dbee77efb7133b7" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7f851245a2687743742157988ed9c42c6e312b95bbe6cfcac9e7d0d0c28ae62f" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp310-cp310-win_amd64.whl", hash = "sha256:3e2ae9981e32a5b9db685659d5c7af0f04b159ff20394650a90124baf6ada51a" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59be99d1c470ef470b134468aa6afa6f968081a503acb4ee883d70332f822e35" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:aa016ab73e06a886f72edc8929ed2ed4c85aaaa6e10500ecdef921b03129b19e" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:c7eb5f219fdfaf1f65e68c00eb81172ab4fa08a9874dae9dad2bca360da34d0f" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:727334e9a721cfc1ac296ce0bf9e69d9486821bfa5b1e75a8feb6f78041db481" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c1be164e93c68b2dbf460fd58975377c892dbcf3358fb72941709c3857351bba" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2d444009c0956669ada149f61ed78f257c1cc96d259efa6acf3929ca96ceb3f0" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fe54cbd5942cd0b26a90f1748f0d4421caf67be35c281c6c3b8573733a03d630" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:90eec299e1f82cfaf080ccb789df3838cb9a54b57e2ebe33852cd392c692de5c" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:783c8fc580bbfc159bff52f4f72cdd538e42b32956e70dffa42b940db114e151" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e985e12a9a232618e5a43476de5689e4b14989f5da6b93909c57afa57ec27012" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:813f0106eb3e268f3783da67b882458e544c6fb72f946e6ca64b5ed4e62c6a77" }, - { url = "https://download.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:9212210f417888e6261c040495180f053084812cf873dedba9fc51ff4b24b2d3" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:73ce04dea64914ff1110008311204c366107d651d3b04faa0dbee77efb7133b7" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7f851245a2687743742157988ed9c42c6e312b95bbe6cfcac9e7d0d0c28ae62f" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp310-cp310-win_amd64.whl", hash = "sha256:3e2ae9981e32a5b9db685659d5c7af0f04b159ff20394650a90124baf6ada51a" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59be99d1c470ef470b134468aa6afa6f968081a503acb4ee883d70332f822e35" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:aa016ab73e06a886f72edc8929ed2ed4c85aaaa6e10500ecdef921b03129b19e" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp311-cp311-win_amd64.whl", hash = "sha256:c7eb5f219fdfaf1f65e68c00eb81172ab4fa08a9874dae9dad2bca360da34d0f" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:727334e9a721cfc1ac296ce0bf9e69d9486821bfa5b1e75a8feb6f78041db481" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c1be164e93c68b2dbf460fd58975377c892dbcf3358fb72941709c3857351bba" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2d444009c0956669ada149f61ed78f257c1cc96d259efa6acf3929ca96ceb3f0" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fe54cbd5942cd0b26a90f1748f0d4421caf67be35c281c6c3b8573733a03d630" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:90eec299e1f82cfaf080ccb789df3838cb9a54b57e2ebe33852cd392c692de5c" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:783c8fc580bbfc159bff52f4f72cdd538e42b32956e70dffa42b940db114e151" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e985e12a9a232618e5a43476de5689e4b14989f5da6b93909c57afa57ec27012" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:813f0106eb3e268f3783da67b882458e544c6fb72f946e6ca64b5ed4e62c6a77" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.25.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:9212210f417888e6261c040495180f053084812cf873dedba9fc51ff4b24b2d3" }, ] [[package]] From 13908b2d31f462f5d012dfd153e8579ad80f8ef9 Mon Sep 17 00:00:00 2001 From: Adam Aghili Date: Tue, 17 Mar 2026 01:56:39 -0400 Subject: [PATCH 29/29] fix: 1.9.0 nightly 1.9.0 nightly fix --- .github/workflows/nightly_build.yml | 6 ++++++ .secrets.baseline | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index 569f980901ec..b4907d495751 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -90,6 +90,8 @@ jobs: with: ref: ${{ needs.resolve-release-branch.outputs.branch }} persist-credentials: true + - name: Confirm branch + run: git branch --show-current - name: "Setup Environment" uses: astral-sh/setup-uv@v6 with: @@ -99,6 +101,10 @@ jobs: prune-cache: false - name: Install the project run: uv sync + - name: Check script location + run: | + echo "PWD: $(pwd)" + find . -name "pypi_nightly_tag.py" - name: Generate main nightly tag id: generate_release_tag diff --git a/.secrets.baseline b/.secrets.baseline index a01cbe251607..aa3b1e012341 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -153,7 +153,7 @@ "filename": ".github/workflows/nightly_build.yml", "hashed_secret": "3e26d6750975d678acb8fa35a0f69237881576b0", "is_verified": false, - "line_number": 257, + "line_number": 263, "is_secret": false } ], @@ -5674,5 +5674,5 @@ } ] }, - "generated_at": "2026-03-16T13:08:35Z" + "generated_at": "2026-03-17T05:56:32Z" }