option.name ?? "").join(",")}
+ data-options={options.map((option) => option.name ?? "").join(",")}
+ data-cloud-incompatible={cloudIncompatibleOptions.join(",")}
+ />
+ ),
+}));
+
describe("ParameterRenderComponent", () => {
beforeEach(() => {
mockCloudOnly = false;
@@ -92,4 +113,35 @@ describe("ParameterRenderComponent", () => {
"Enter your cloud URL",
);
});
+
+ it("preserves incompatible sortable values while filtering them from the chooser in cloud mode", () => {
+ mockCloudOnly = true;
+
+ render(
+
,
+ );
+
+ const renderedProps = screen.getByTestId("sortable-list-props");
+ expect(renderedProps).toHaveAttribute("data-value", "Local");
+ expect(renderedProps).toHaveAttribute("data-options", "AWS");
+ expect(renderedProps).toHaveAttribute("data-cloud-incompatible", "Local");
+ });
});
diff --git a/src/frontend/src/components/core/parameterRenderComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/index.tsx
index 4e7902d302..00a7989750 100644
--- a/src/frontend/src/components/core/parameterRenderComponent/index.tsx
+++ b/src/frontend/src/components/core/parameterRenderComponent/index.tsx
@@ -298,15 +298,16 @@ export function ParameterRenderComponent({
case "sortableList": {
// Filter out cloud-incompatible options when cloud mode is active
let sortableOptions = templateData?.options;
+ let cloudIncompatibleOptions: unknown[] = [];
if (cloudOnly && nodeMetadata?.cloud_incompatible_options) {
const incompatible = nodeMetadata.cloud_incompatible_options[name];
if (incompatible && Array.isArray(incompatible)) {
+ cloudIncompatibleOptions = incompatible;
sortableOptions = sortableOptions?.filter((opt: unknown) => {
const optionName =
typeof opt === "object" && opt !== null && "name" in opt
? ((opt as { name?: unknown }).name ?? opt)
: opt;
-
return !incompatible.includes(optionName);
});
}
@@ -317,6 +318,7 @@ export function ParameterRenderComponent({
helperText={templateData?.helper_text}
helperMetadata={templateData?.helper_text_metadata}
options={sortableOptions}
+ cloudIncompatibleOptions={cloudIncompatibleOptions}
searchCategory={templateData?.search_category}
limit={templateData?.limit}
id={`sortablelist_${id}`}
diff --git a/src/lfx/src/lfx/_assets/component_index.json b/src/lfx/src/lfx/_assets/component_index.json
index 188cc8b64d..1eabe93cdd 100644
--- a/src/lfx/src/lfx/_assets/component_index.json
+++ b/src/lfx/src/lfx/_assets/component_index.json
@@ -109216,7 +109216,13 @@
"frozen": false,
"legacy": true,
"metadata": {
- "code_hash": "154e292dad2b",
+ "cloud_default_overrides": {
+ "url": {
+ "placeholder": "Enter SearXNG URL",
+ "value": ""
+ }
+ },
+ "code_hash": "85a7196f1720",
"dependencies": {
"dependencies": [
{
@@ -109304,7 +109310,7 @@
"show": true,
"title_case": false,
"type": "code",
- "value": "import json\nfrom collections.abc import Sequence\nfrom typing import Any\n\nimport requests\nfrom langchain.agents import Tool\nfrom langchain_core.tools import StructuredTool\nfrom pydantic.v1 import Field, create_model\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.inputs.inputs import DropdownInput, IntInput, MessageTextInput, MultiselectInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\n\n\nclass SearXNGToolComponent(LCToolComponent):\n search_headers: dict = {}\n display_name = \"SearXNG Search\"\n description = \"A component that searches for tools using SearXNG.\"\n name = \"SearXNGTool\"\n legacy: bool = True\n\n inputs = [\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n value=\"http://localhost\",\n required=True,\n refresh_button=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n value=10,\n required=True,\n ),\n MultiselectInput(\n name=\"categories\",\n display_name=\"Categories\",\n options=[],\n value=[],\n ),\n DropdownInput(\n name=\"language\",\n display_name=\"Language\",\n options=[],\n ),\n ]\n\n outputs = [\n Output(display_name=\"Tool\", name=\"result_tool\", method=\"build_tool\"),\n ]\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name is None:\n return build_config\n\n if field_name != \"url\":\n return build_config\n\n try:\n url = f\"{field_value}/config\"\n\n response = requests.get(url=url, headers=self.search_headers.copy(), timeout=10)\n data = None\n if response.headers.get(\"Content-Encoding\") == \"zstd\":\n data = json.loads(response.content)\n else:\n data = response.json()\n build_config[\"categories\"][\"options\"] = data[\"categories\"].copy()\n for selected_category in build_config[\"categories\"][\"value\"]:\n if selected_category not in build_config[\"categories\"][\"options\"]:\n build_config[\"categories\"][\"value\"].remove(selected_category)\n languages = list(data[\"locales\"])\n build_config[\"language\"][\"options\"] = languages.copy()\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[\"categories\"][\"options\"] = [\"Failed to parse\", str(e)]\n return build_config\n\n def build_tool(self) -> Tool:\n class SearxSearch:\n _url: str = \"\"\n _categories: list[str] = []\n _language: str = \"\"\n _headers: dict = {}\n _max_results: int = 10\n\n @staticmethod\n def search(query: str, categories: Sequence[str] = ()) -> list:\n if not SearxSearch._categories and not categories:\n msg = \"No categories provided.\"\n raise ValueError(msg)\n all_categories = SearxSearch._categories + list(set(categories) - set(SearxSearch._categories))\n try:\n url = f\"{SearxSearch._url}/\"\n headers = SearxSearch._headers.copy()\n response = requests.get(\n url=url,\n headers=headers,\n params={\n \"q\": query,\n \"categories\": \",\".join(all_categories),\n \"language\": SearxSearch._language,\n \"format\": \"json\",\n },\n timeout=10,\n ).json()\n\n num_results = min(SearxSearch._max_results, len(response[\"results\"]))\n return [response[\"results\"][i] for i in range(num_results)]\n except Exception as e: # noqa: BLE001\n logger.debug(\"Error running SearXNG Search\", exc_info=True)\n return [f\"Failed to search: {e}\"]\n\n SearxSearch._url = self.url\n SearxSearch._categories = self.categories.copy()\n SearxSearch._language = self.language\n SearxSearch._headers = self.search_headers.copy()\n SearxSearch._max_results = self.max_results\n\n globals_ = globals()\n local = {}\n local[\"SearxSearch\"] = SearxSearch\n globals_.update(local)\n\n schema_fields = {\n \"query\": (str, Field(..., description=\"The query to search for.\")),\n \"categories\": (\n list[str],\n Field(default=[], description=\"The categories to search in.\"),\n ),\n }\n\n searx_search_schema = create_model(\"SearxSearchSchema\", **schema_fields)\n\n return StructuredTool.from_function(\n func=local[\"SearxSearch\"].search,\n args_schema=searx_search_schema,\n name=\"searxng_search_tool\",\n description=\"A tool that searches for tools using SearXNG.\\nThe available categories are: \"\n + \", \".join(self.categories),\n )\n"
+ "value": "import json\nfrom collections.abc import Sequence\nfrom typing import Any\n\nimport requests\nfrom langchain.agents import Tool\nfrom langchain_core.tools import StructuredTool\nfrom pydantic.v1 import Field, create_model\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.inputs.inputs import DropdownInput, IntInput, MessageTextInput, MultiselectInput\nfrom lfx.io import Output\nfrom lfx.log.logger import logger\nfrom lfx.schema.dotdict import dotdict\n\n\nclass SearXNGToolComponent(LCToolComponent):\n search_headers: dict = {}\n display_name = \"SearXNG Search\"\n description = \"A component that searches for tools using SearXNG.\"\n name = \"SearXNGTool\"\n legacy: bool = True\n metadata = {\n \"cloud_default_overrides\": {\n \"url\": {\"value\": \"\", \"placeholder\": \"Enter SearXNG URL\"},\n },\n }\n\n inputs = [\n MessageTextInput(\n name=\"url\",\n display_name=\"URL\",\n value=\"http://localhost\",\n required=True,\n refresh_button=True,\n ),\n IntInput(\n name=\"max_results\",\n display_name=\"Max Results\",\n value=10,\n required=True,\n ),\n MultiselectInput(\n name=\"categories\",\n display_name=\"Categories\",\n options=[],\n value=[],\n ),\n DropdownInput(\n name=\"language\",\n display_name=\"Language\",\n options=[],\n ),\n ]\n\n outputs = [\n Output(display_name=\"Tool\", name=\"result_tool\", method=\"build_tool\"),\n ]\n\n def update_build_config(self, build_config: dotdict, field_value: Any, field_name: str | None = None) -> dotdict:\n if field_name is None:\n return build_config\n\n if field_name != \"url\":\n return build_config\n\n try:\n url = f\"{field_value}/config\"\n\n response = requests.get(url=url, headers=self.search_headers.copy(), timeout=10)\n data = None\n if response.headers.get(\"Content-Encoding\") == \"zstd\":\n data = json.loads(response.content)\n else:\n data = response.json()\n build_config[\"categories\"][\"options\"] = data[\"categories\"].copy()\n for selected_category in build_config[\"categories\"][\"value\"]:\n if selected_category not in build_config[\"categories\"][\"options\"]:\n build_config[\"categories\"][\"value\"].remove(selected_category)\n languages = list(data[\"locales\"])\n build_config[\"language\"][\"options\"] = languages.copy()\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[\"categories\"][\"options\"] = [\"Failed to parse\", str(e)]\n return build_config\n\n def build_tool(self) -> Tool:\n class SearxSearch:\n _url: str = \"\"\n _categories: list[str] = []\n _language: str = \"\"\n _headers: dict = {}\n _max_results: int = 10\n\n @staticmethod\n def search(query: str, categories: Sequence[str] = ()) -> list:\n if not SearxSearch._categories and not categories:\n msg = \"No categories provided.\"\n raise ValueError(msg)\n all_categories = SearxSearch._categories + list(set(categories) - set(SearxSearch._categories))\n try:\n url = f\"{SearxSearch._url}/\"\n headers = SearxSearch._headers.copy()\n response = requests.get(\n url=url,\n headers=headers,\n params={\n \"q\": query,\n \"categories\": \",\".join(all_categories),\n \"language\": SearxSearch._language,\n \"format\": \"json\",\n },\n timeout=10,\n ).json()\n\n num_results = min(SearxSearch._max_results, len(response[\"results\"]))\n return [response[\"results\"][i] for i in range(num_results)]\n except Exception as e: # noqa: BLE001\n logger.debug(\"Error running SearXNG Search\", exc_info=True)\n return [f\"Failed to search: {e}\"]\n\n SearxSearch._url = self.url\n SearxSearch._categories = self.categories.copy()\n SearxSearch._language = self.language\n SearxSearch._headers = self.search_headers.copy()\n SearxSearch._max_results = self.max_results\n\n globals_ = globals()\n local = {}\n local[\"SearxSearch\"] = SearxSearch\n globals_.update(local)\n\n schema_fields = {\n \"query\": (str, Field(..., description=\"The query to search for.\")),\n \"categories\": (\n list[str],\n Field(default=[], description=\"The categories to search in.\"),\n ),\n }\n\n searx_search_schema = create_model(\"SearxSearchSchema\", **schema_fields)\n\n return StructuredTool.from_function(\n func=local[\"SearxSearch\"].search,\n args_schema=searx_search_schema,\n name=\"searxng_search_tool\",\n description=\"A tool that searches for tools using SearXNG.\\nThe available categories are: \"\n + \", \".join(self.categories),\n )\n"
},
"language": {
"_input_type": "DropdownInput",
@@ -118910,6 +118916,6 @@
"num_components": 359,
"num_modules": 97
},
- "sha256": "697092ebc6582200cb8b02ffaaf7c5deb2ba8fca7f24c5d5c176867621a70ce6",
+ "sha256": "605b661e8fd730191d1e9a95fab859a33c5c5352286bb59db5f6babca1d68fb2",
"version": "0.3.1"
}
\ No newline at end of file
diff --git a/src/lfx/src/lfx/components/tools/searxng.py b/src/lfx/src/lfx/components/tools/searxng.py
index b1abc8eb1d..69a4dd2b8f 100644
--- a/src/lfx/src/lfx/components/tools/searxng.py
+++ b/src/lfx/src/lfx/components/tools/searxng.py
@@ -20,6 +20,11 @@ class SearXNGToolComponent(LCToolComponent):
description = "A component that searches for tools using SearXNG."
name = "SearXNGTool"
legacy: bool = True
+ metadata = {
+ "cloud_default_overrides": {
+ "url": {"value": "", "placeholder": "Enter SearXNG URL"},
+ },
+ }
inputs = [
MessageTextInput(
diff --git a/src/lfx/tests/unit/custom/test_cloud_default_overrides.py b/src/lfx/tests/unit/custom/test_cloud_default_overrides.py
new file mode 100644
index 0000000000..f457c855d1
--- /dev/null
+++ b/src/lfx/tests/unit/custom/test_cloud_default_overrides.py
@@ -0,0 +1,9 @@
+"""Tests for cloud-mode default overrides on mixed compatibility components."""
+
+from lfx.components.tools.searxng import SearXNGToolComponent
+
+
+def test_searxng_component_clears_localhost_default_in_cloud_mode():
+ assert SearXNGToolComponent.metadata["cloud_default_overrides"] == {
+ "url": {"value": "", "placeholder": "Enter SearXNG URL"},
+ }