diff --git a/scripts/migrate/consolidate_bundles.py b/scripts/migrate/consolidate_bundles.py index dd79030925..a8b9523ee1 100755 --- a/scripts/migrate/consolidate_bundles.py +++ b/scripts/migrate/consolidate_bundles.py @@ -158,6 +158,14 @@ PROVIDER_DEPS: dict[str, list[str]] = { # --- tranche 9: langwatch evaluator (pure httpx REST; the langwatch SDK extra # is for the tracing service, not this component) --- "langwatch": [], + # --- tranche 10: uppercase source dirs -> lowercase bundle slug (FAISS->faiss, + # Notion->notion via bundle_slug(); import paths keep historical casing) --- + "FAISS": [ + "faiss-cpu==1.9.0.post1; python_version < '3.14'", + "faiss-cpu>=1.13.2; python_version >= '3.14'", + _LC_COMMUNITY, + ], + "Notion": ["requests>=2.32.0", "Markdown>=3.8.0"], } _OPTIONAL_DEPS_HEADER = ( @@ -201,7 +209,7 @@ def discover_component_classes(provider_dir: Path) -> list[tuple[str, str]]: return found -def _shim_source(provider: str) -> str: +def _shim_source(provider: str, slug: str) -> str: return ( "# lfx-bundles-shim\n" f'"""Compatibility shim: lfx.components.{provider} moved to lfx-bundles.\n' @@ -215,7 +223,7 @@ def _shim_source(provider: str) -> str: "import sys\n" "\n" "try:\n" - f' sys.modules[__name__] = importlib.import_module("lfx_bundles.{provider}")\n' + f' sys.modules[__name__] = importlib.import_module("lfx_bundles.{slug}")\n' "except ModuleNotFoundError as exc:\n" ' if exc.name is not None and (exc.name == "lfx_bundles" or exc.name.startswith("lfx_bundles.")):\n' " msg = (\n" @@ -228,15 +236,28 @@ def _shim_source(provider: str) -> str: ) -def _rewrite_self_imports(text: str, provider: str) -> str: - """Rewrite absolute ``lfx.components.`` self-refs to ``lfx_bundles.``.""" - return re.sub(rf"\blfx\.components\.{re.escape(provider)}\b", f"lfx_bundles.{provider}", text) +def _rewrite_self_imports(text: str, provider: str, slug: str) -> str: + """Rewrite absolute ``lfx.components.`` self-refs to ``lfx_bundles.``.""" + return re.sub(rf"\blfx\.components\.{re.escape(provider)}\b", f"lfx_bundles.{slug}", text) + + +def bundle_slug(provider: str) -> str: + """Bundle directory / ext-slug name for a provider. + + Bundle names must satisfy ``BUNDLE_NAME_RE`` (lowercase), so the in-tree + source dir name is lowercased -- e.g. ``FAISS`` -> ``faiss``, ``Notion`` -> + ``notion``. Already-lowercase providers are unchanged. The historical + ``lfx.components.`` import paths (migration table) keep the + original casing; only the bundle dir + ``ext:`` id are lowercased. + """ + return provider.lower() def move_provider(provider: str, *, apply: bool) -> list[tuple[str, str]]: """Move one provider into the metapackage and leave a shim. Returns its classes.""" + slug = bundle_slug(provider) src = COMPONENTS_DIR / provider - dst = BUNDLES_PKG / provider + dst = BUNDLES_PKG / slug if not src.is_dir(): msg = f"provider directory not found: {src}" raise SystemExit(msg) @@ -248,19 +269,19 @@ def move_provider(provider: str, *, apply: bool) -> list[tuple[str, str]]: py_files = sorted(src.glob("*.py")) print(f" {provider}: {len(py_files)} file(s), {len(classes)} component class(es) -> {dst.relative_to(REPO_ROOT)}") for module_stem, class_name in classes: - print(f" {module_stem}.{class_name} -> ext:{provider}:{class_name}@official") + print(f" {module_stem}.{class_name} -> ext:{slug}:{class_name}@official") if not apply: return classes dst.mkdir(parents=True) for py in py_files: - content = _rewrite_self_imports(py.read_text(encoding="utf-8"), provider) + content = _rewrite_self_imports(py.read_text(encoding="utf-8"), provider, slug) (dst / py.name).write_text(content, encoding="utf-8") # Remove the moved source, then replace the in-tree dir with a one-file shim. shutil.rmtree(src) src.mkdir() - (src / "__init__.py").write_text(_shim_source(provider), encoding="utf-8") + (src / "__init__.py").write_text(_shim_source(provider, slug), encoding="utf-8") return classes @@ -324,8 +345,11 @@ def append_migration_entries(plan: dict[str, list[tuple[str, str]]], *, apply: b new_entries: list[dict] = [] new_ambiguous: list[dict] = [] for provider, classes in plan.items(): + # ext id uses the lowercase bundle slug; import paths keep the historical + # (possibly mixed-case) ``lfx.components.`` form for migration. + slug = bundle_slug(provider) for module_stem, class_name in classes: - target = f"ext:{provider}:{class_name}@official" + target = f"ext:{slug}:{class_name}@official" prior = existing_bare.get(class_name) if class_name in already_ambiguous: print(f" ! {class_name!r} already ambiguous; import-path entries only") @@ -337,7 +361,7 @@ def append_migration_entries(plan: dict[str, list[tuple[str, str]]], *, apply: b existing_bare[class_name] = target new_entries.append(_entry("import_path", f"lfx.components.{provider}.{module_stem}.{class_name}", target)) new_entries.append(_entry("import_path", f"lfx.components.{provider}.{class_name}", target)) - new_entries.append(_entry("legacy_slot", f"ext:{provider}:{class_name}@official-pre-a", target)) + new_entries.append(_entry("legacy_slot", f"ext:{slug}:{class_name}@official-pre-a", target)) # Idempotency guard: a partially-failed earlier run (provider dir moved # but the table already written, or vice versa) must not duplicate rows on diff --git a/src/lfx/tests/unit/components/test_faiss_vector_store_component.py b/src/backend/tests/unit/components/vectorstores/test_faiss_vector_store_component.py similarity index 95% rename from src/lfx/tests/unit/components/test_faiss_vector_store_component.py rename to src/backend/tests/unit/components/vectorstores/test_faiss_vector_store_component.py index 2a95069413..b13e5e14d2 100644 --- a/src/lfx/tests/unit/components/test_faiss_vector_store_component.py +++ b/src/backend/tests/unit/components/vectorstores/test_faiss_vector_store_component.py @@ -4,8 +4,8 @@ import pytest pytest.importorskip("langchain_community") -from lfx.components.FAISS.faiss import FaissVectorStoreComponent from lfx.io import BoolInput +from lfx_bundles.faiss.faiss import FaissVectorStoreComponent class TestFaissVectorStoreComponentDefaults: diff --git a/src/bundles/lfx-bundles/pyproject.toml b/src/bundles/lfx-bundles/pyproject.toml index 9b4a0b0bda..fc69189796 100644 --- a/src/bundles/lfx-bundles/pyproject.toml +++ b/src/bundles/lfx-bundles/pyproject.toml @@ -47,6 +47,7 @@ cuga = ["cuga>=0.2.20,<0.3.0; sys_platform != 'darwin' and python_version < '3.1 deepseek = ["langchain-openai>=1.1.6", "openai>=1.68.2,<3.0.0", "requests>=2.32.0"] elastic = ["elasticsearch~=8.19", "langchain-elasticsearch~=1.0.0", "opensearch-py==2.8.0"] exa = ["metaphor-python==0.1.23"] +faiss = ["faiss-cpu==1.9.0.post1; python_version < '3.14'", "faiss-cpu>=1.13.2; python_version >= '3.14'", "langchain-community>=0.4.1,<1.0.0"] firecrawl = ["firecrawl-py>=1.0.16,<2.0.0"] git = ["GitPython>=3.1.50", "langchain-community>=0.4.1,<1.0.0"] glean = [] @@ -65,6 +66,7 @@ milvus = ["langchain-milvus~=0.3.2"] mistral = ["langchain-mistralai~=1.1.1"] mongodb = ["pymongo>=4.10.1", "langchain-mongodb>=0.11.0"] needle = ["needle-python>=0.4.0", "langchain-community>=0.4.1,<1.0.0"] +notion = ["requests>=2.32.0", "Markdown>=3.8.0"] novita = ["langchain-openai>=1.1.6", "requests>=2.32.0"] nvidia = ["langchain-nvidia-ai-endpoints~=1.0.0", "nv-ingest-client>=26.1.0,<27.0.0 ; python_version >= '3.12'", "gassist>=0.0.1; sys_platform == 'win32'"] olivya = [] @@ -118,6 +120,7 @@ all = [ "lfx-bundles[deepseek]", "lfx-bundles[elastic]", "lfx-bundles[exa]", + "lfx-bundles[faiss]", "lfx-bundles[firecrawl]", "lfx-bundles[git]", "lfx-bundles[glean]", @@ -136,6 +139,7 @@ all = [ "lfx-bundles[mistral]", "lfx-bundles[mongodb]", "lfx-bundles[needle]", + "lfx-bundles[notion]", "lfx-bundles[novita]", "lfx-bundles[nvidia]", "lfx-bundles[olivya]", diff --git a/src/bundles/lfx-bundles/src/lfx_bundles/faiss/__init__.py b/src/bundles/lfx-bundles/src/lfx_bundles/faiss/__init__.py new file mode 100644 index 0000000000..93654b7ac1 --- /dev/null +++ b/src/bundles/lfx-bundles/src/lfx_bundles/faiss/__init__.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from lfx.components._importing import import_mod + +if TYPE_CHECKING: + from .faiss import FaissVectorStoreComponent + +_dynamic_imports = { + "FaissVectorStoreComponent": "faiss", +} + +__all__ = [ + "FaissVectorStoreComponent", +] + + +def __getattr__(attr_name: str) -> Any: + """Lazily import FAISS components on attribute access.""" + if attr_name not in _dynamic_imports: + msg = f"module '{__name__}' has no attribute '{attr_name}'" + raise AttributeError(msg) + try: + result = import_mod(attr_name, _dynamic_imports[attr_name], __spec__.parent) + except (ModuleNotFoundError, ImportError, AttributeError) as e: + msg = f"Could not import '{attr_name}' from '{__name__}': {e}" + raise AttributeError(msg) from e + globals()[attr_name] = result + return result + + +def __dir__() -> list[str]: + return list(__all__) diff --git a/src/lfx/src/lfx/components/FAISS/faiss.py b/src/bundles/lfx-bundles/src/lfx_bundles/faiss/faiss.py similarity index 99% rename from src/lfx/src/lfx/components/FAISS/faiss.py rename to src/bundles/lfx-bundles/src/lfx_bundles/faiss/faiss.py index b7f0cca05b..2f07af7906 100644 --- a/src/lfx/src/lfx/components/FAISS/faiss.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/faiss/faiss.py @@ -1,7 +1,6 @@ from pathlib import Path from langchain_community.vectorstores import FAISS - from lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store from lfx.helpers.data import docs_to_data from lfx.io import BoolInput, HandleInput, IntInput, StrInput diff --git a/src/bundles/lfx-bundles/src/lfx_bundles/notion/__init__.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/__init__.py new file mode 100644 index 0000000000..9e50918fa4 --- /dev/null +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/__init__.py @@ -0,0 +1,19 @@ +from .add_content_to_page import AddContentToPage +from .create_page import NotionPageCreator +from .list_database_properties import NotionDatabaseProperties +from .list_pages import NotionListPages +from .list_users import NotionUserList +from .page_content_viewer import NotionPageContent +from .search import NotionSearch +from .update_page_property import NotionPageUpdate + +__all__ = [ + "AddContentToPage", + "NotionDatabaseProperties", + "NotionListPages", + "NotionPageContent", + "NotionPageCreator", + "NotionPageUpdate", + "NotionSearch", + "NotionUserList", +] diff --git a/src/lfx/src/lfx/components/Notion/add_content_to_page.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/add_content_to_page.py similarity index 99% rename from src/lfx/src/lfx/components/Notion/add_content_to_page.py rename to src/bundles/lfx-bundles/src/lfx_bundles/notion/add_content_to_page.py index 12b3007f16..0576c4b8d8 100644 --- a/src/lfx/src/lfx/components/Notion/add_content_to_page.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/add_content_to_page.py @@ -4,14 +4,13 @@ from typing import Any import requests from bs4 import BeautifulSoup from langchain_core.tools import StructuredTool -from markdown import markdown -from pydantic import BaseModel, Field - from lfx.base.langchain_utilities.model import LCToolComponent from lfx.field_typing import Tool from lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput from lfx.log.logger import logger from lfx.schema.data import Data +from markdown import markdown +from pydantic import BaseModel, Field MIN_ROWS_IN_TABLE = 3 diff --git a/src/lfx/src/lfx/components/Notion/create_page.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/create_page.py similarity index 99% rename from src/lfx/src/lfx/components/Notion/create_page.py rename to src/bundles/lfx-bundles/src/lfx_bundles/notion/create_page.py index 65afc25d99..99fc363858 100644 --- a/src/lfx/src/lfx/components/Notion/create_page.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/create_page.py @@ -3,12 +3,11 @@ from typing import Any import requests from langchain_core.tools import StructuredTool -from pydantic import BaseModel, Field - from lfx.base.langchain_utilities.model import LCToolComponent from lfx.field_typing import Tool from lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput from lfx.schema.data import Data +from pydantic import BaseModel, Field class NotionPageCreator(LCToolComponent): diff --git a/src/lfx/src/lfx/components/Notion/list_database_properties.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/list_database_properties.py similarity index 99% rename from src/lfx/src/lfx/components/Notion/list_database_properties.py rename to src/bundles/lfx-bundles/src/lfx_bundles/notion/list_database_properties.py index 0282b9c3a6..b50c3c4c80 100644 --- a/src/lfx/src/lfx/components/Notion/list_database_properties.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/list_database_properties.py @@ -1,12 +1,11 @@ import requests from langchain_core.tools import StructuredTool -from pydantic import BaseModel, Field - from lfx.base.langchain_utilities.model import LCToolComponent from lfx.field_typing import Tool from lfx.inputs.inputs import SecretStrInput, StrInput from lfx.log.logger import logger from lfx.schema.data import Data +from pydantic import BaseModel, Field class NotionDatabaseProperties(LCToolComponent): diff --git a/src/lfx/src/lfx/components/Notion/list_pages.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/list_pages.py similarity index 99% rename from src/lfx/src/lfx/components/Notion/list_pages.py rename to src/bundles/lfx-bundles/src/lfx_bundles/notion/list_pages.py index 5800fcdcb4..39eb7f940b 100644 --- a/src/lfx/src/lfx/components/Notion/list_pages.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/list_pages.py @@ -3,13 +3,12 @@ from typing import Any import requests from langchain_core.tools import StructuredTool -from pydantic import BaseModel, Field - from lfx.base.langchain_utilities.model import LCToolComponent from lfx.field_typing import Tool from lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput from lfx.log.logger import logger from lfx.schema.data import Data +from pydantic import BaseModel, Field class NotionListPages(LCToolComponent): diff --git a/src/lfx/src/lfx/components/Notion/list_users.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/list_users.py similarity index 99% rename from src/lfx/src/lfx/components/Notion/list_users.py rename to src/bundles/lfx-bundles/src/lfx_bundles/notion/list_users.py index ef1a06c331..2fdea4a1a9 100644 --- a/src/lfx/src/lfx/components/Notion/list_users.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/list_users.py @@ -1,11 +1,10 @@ import requests from langchain_core.tools import StructuredTool -from pydantic import BaseModel - from lfx.base.langchain_utilities.model import LCToolComponent from lfx.field_typing import Tool from lfx.inputs.inputs import SecretStrInput from lfx.schema.data import Data +from pydantic import BaseModel class NotionUserList(LCToolComponent): diff --git a/src/lfx/src/lfx/components/Notion/page_content_viewer.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/page_content_viewer.py similarity index 99% rename from src/lfx/src/lfx/components/Notion/page_content_viewer.py rename to src/bundles/lfx-bundles/src/lfx_bundles/notion/page_content_viewer.py index 88e2140ceb..0845ae855b 100644 --- a/src/lfx/src/lfx/components/Notion/page_content_viewer.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/page_content_viewer.py @@ -1,12 +1,11 @@ import requests from langchain_core.tools import StructuredTool -from pydantic import BaseModel, Field - from lfx.base.langchain_utilities.model import LCToolComponent from lfx.field_typing import Tool from lfx.inputs.inputs import SecretStrInput, StrInput from lfx.log.logger import logger from lfx.schema.data import Data +from pydantic import BaseModel, Field class NotionPageContent(LCToolComponent): diff --git a/src/lfx/src/lfx/components/Notion/search.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/search.py similarity index 99% rename from src/lfx/src/lfx/components/Notion/search.py rename to src/bundles/lfx-bundles/src/lfx_bundles/notion/search.py index 4439c31fe5..6e8f97a691 100644 --- a/src/lfx/src/lfx/components/Notion/search.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/search.py @@ -2,12 +2,11 @@ from typing import Any import requests from langchain_core.tools import StructuredTool -from pydantic import BaseModel, Field - from lfx.base.langchain_utilities.model import LCToolComponent from lfx.field_typing import Tool from lfx.inputs.inputs import DropdownInput, SecretStrInput, StrInput from lfx.schema.data import Data +from pydantic import BaseModel, Field class NotionSearch(LCToolComponent): diff --git a/src/lfx/src/lfx/components/Notion/update_page_property.py b/src/bundles/lfx-bundles/src/lfx_bundles/notion/update_page_property.py similarity index 99% rename from src/lfx/src/lfx/components/Notion/update_page_property.py rename to src/bundles/lfx-bundles/src/lfx_bundles/notion/update_page_property.py index ee10ab4cb4..58557343d6 100644 --- a/src/lfx/src/lfx/components/Notion/update_page_property.py +++ b/src/bundles/lfx-bundles/src/lfx_bundles/notion/update_page_property.py @@ -3,13 +3,12 @@ from typing import Any import requests from langchain_core.tools import StructuredTool -from pydantic import BaseModel, Field - from lfx.base.langchain_utilities.model import LCToolComponent from lfx.field_typing import Tool from lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput from lfx.log.logger import logger from lfx.schema.data import Data +from pydantic import BaseModel, Field class NotionPageUpdate(LCToolComponent): diff --git a/src/lfx/src/lfx/_assets/component_index.json b/src/lfx/src/lfx/_assets/component_index.json index 9a2133399a..ef8438138e 100644 --- a/src/lfx/src/lfx/_assets/component_index.json +++ b/src/lfx/src/lfx/_assets/component_index.json @@ -1,1568 +1,5 @@ { "entries": [ - [ - "FAISS", - { - "FAISS": { - "base_classes": [ - "JSON", - "Table" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "FAISS Vector Store with search capabilities", - "display_name": "FAISS", - "documentation": "", - "edited": false, - "field_order": [ - "index_name", - "persist_directory", - "ingest_data", - "search_query", - "should_cache_vector_store", - "allow_dangerous_deserialization", - "embedding", - "number_of_results" - ], - "frozen": false, - "icon": "FAISS", - "legacy": false, - "metadata": { - "code_hash": "3e55e36d0692", - "dependencies": { - "dependencies": [ - { - "name": "langchain_community", - "version": "0.4.2" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 2 - }, - "module": "lfx.components.FAISS.faiss.FaissVectorStoreComponent" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "Search Results", - "group_outputs": false, - "method": "search_documents", - "name": "search_results", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Table", - "group_outputs": false, - "method": "as_dataframe", - "name": "dataframe", - "selected": "Table", - "tool_mode": true, - "types": [ - "Table" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "allow_dangerous_deserialization": { - "_input_type": "BoolInput", - "advanced": true, - "display_name": "Allow Dangerous Deserialization", - "dynamic": false, - "info": "Set to True to allow loading pickle files. WARNING: Only enable this if you trust the source of the data. Malicious pickle files can execute arbitrary code on your system.", - "list": false, - "list_add_label": "Add More", - "name": "allow_dangerous_deserialization", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": true, - "type": "bool", - "value": false - }, - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "from pathlib import Path\n\nfrom langchain_community.vectorstores import FAISS\n\nfrom lfx.base.vectorstores.model import LCVectorStoreComponent, check_cached_vector_store\nfrom lfx.helpers.data import docs_to_data\nfrom lfx.io import BoolInput, HandleInput, IntInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass FaissVectorStoreComponent(LCVectorStoreComponent):\n \"\"\"FAISS Vector Store with search capabilities.\"\"\"\n\n display_name: str = \"FAISS\"\n description: str = \"FAISS Vector Store with search capabilities\"\n name = \"FAISS\"\n icon = \"FAISS\"\n\n inputs = [\n StrInput(\n name=\"index_name\",\n display_name=\"Index Name\",\n value=\"langflow_index\",\n ),\n StrInput(\n name=\"persist_directory\",\n display_name=\"Persist Directory\",\n info=\"Path to save the FAISS index. It will be relative to where Langflow is running.\",\n ),\n *LCVectorStoreComponent.inputs,\n BoolInput(\n name=\"allow_dangerous_deserialization\",\n display_name=\"Allow Dangerous Deserialization\",\n info=\"Set to True to allow loading pickle files. WARNING: Only enable this if you trust the source \"\n \"of the data. Malicious pickle files can execute arbitrary code on your system.\",\n advanced=True,\n value=False,\n ),\n HandleInput(name=\"embedding\", display_name=\"Embedding\", input_types=[\"Embeddings\"]),\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=4,\n ),\n ]\n\n @staticmethod\n def resolve_path(path: str) -> str:\n \"\"\"Resolve the path relative to the Langflow root.\n\n Args:\n path: The path to resolve\n Returns:\n str: The resolved path as a string\n \"\"\"\n return str(Path(path).resolve())\n\n def get_persist_directory(self) -> Path:\n \"\"\"Returns the resolved persist directory path or the current directory if not set.\"\"\"\n if self.persist_directory:\n return Path(self.resolve_path(self.persist_directory))\n return Path()\n\n @check_cached_vector_store\n def build_vector_store(self) -> FAISS:\n \"\"\"Builds the FAISS object.\"\"\"\n path = self.get_persist_directory()\n path.mkdir(parents=True, exist_ok=True)\n\n # Convert DataFrame to Data if needed using parent's method\n self.ingest_data = self._prepare_ingest_data()\n\n documents = []\n for _input in self.ingest_data or []:\n if isinstance(_input, Data):\n documents.append(_input.to_lc_document())\n else:\n documents.append(_input)\n\n faiss = FAISS.from_documents(documents=documents, embedding=self.embedding)\n faiss.save_local(str(path), self.index_name)\n return faiss\n\n def search_documents(self) -> list[Data]:\n \"\"\"Search for documents in the FAISS vector store.\"\"\"\n path = self.get_persist_directory()\n index_path = path / f\"{self.index_name}.faiss\"\n\n if not index_path.exists():\n vector_store = self.build_vector_store()\n else:\n vector_store = FAISS.load_local(\n folder_path=str(path),\n embeddings=self.embedding,\n index_name=self.index_name,\n allow_dangerous_deserialization=self.allow_dangerous_deserialization,\n )\n\n if not vector_store:\n msg = \"Failed to load the FAISS index.\"\n raise ValueError(msg)\n\n if self.search_query and isinstance(self.search_query, str) and self.search_query.strip():\n docs = vector_store.similarity_search(\n query=self.search_query,\n k=self.number_of_results,\n )\n return docs_to_data(docs)\n return []\n" - }, - "embedding": { - "_input_type": "HandleInput", - "advanced": false, - "display_name": "Embedding", - "dynamic": false, - "info": "", - "input_types": [ - "Embeddings" - ], - "list": false, - "list_add_label": "Add More", - "name": "embedding", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "other", - "value": "" - }, - "index_name": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Index Name", - "dynamic": false, - "info": "", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "index_name", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "langflow_index" - }, - "ingest_data": { - "_input_type": "HandleInput", - "advanced": false, - "display_name": "Ingest Data", - "dynamic": false, - "info": "", - "input_types": [ - "Data", - "DataFrame", - "Table" - ], - "list": true, - "list_add_label": "Add More", - "name": "ingest_data", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "other", - "value": "" - }, - "number_of_results": { - "_input_type": "IntInput", - "advanced": true, - "display_name": "Number of Results", - "dynamic": false, - "info": "Number of results to return.", - "list": false, - "list_add_label": "Add More", - "name": "number_of_results", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": true, - "type": "int", - "value": 4 - }, - "persist_directory": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Persist Directory", - "dynamic": false, - "info": "Path to save the FAISS index. It will be relative to where Langflow is running.", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "persist_directory", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "search_query": { - "_input_type": "QueryInput", - "advanced": false, - "display_name": "Search Query", - "dynamic": false, - "info": "Enter a query to run a similarity search.", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "search_query", - "override_skip": false, - "placeholder": "Enter a query...", - "required": false, - "show": true, - "title_case": false, - "tool_mode": true, - "trace_as_input": true, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "query", - "value": "" - }, - "should_cache_vector_store": { - "_input_type": "BoolInput", - "advanced": true, - "display_name": "Cache Vector Store", - "dynamic": false, - "info": "If True, the vector store will be cached for the current build of the component. This is useful for components that have multiple output methods and want to share the same vector store.", - "list": false, - "list_add_label": "Add More", - "name": "should_cache_vector_store", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": true, - "type": "bool", - "value": true - } - }, - "tool_mode": false - } - } - ], - [ - "Notion", - { - "AddContentToPage": { - "base_classes": [ - "JSON", - "Tool" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Convert markdown text to Notion blocks and append them to a Notion page.", - "display_name": "Add Content to Page ", - "documentation": "https://developers.notion.com/reference/patch-block-children", - "edited": false, - "field_order": [ - "markdown_text", - "block_id", - "notion_secret" - ], - "frozen": false, - "icon": "NotionDirectoryLoader", - "legacy": false, - "metadata": { - "code_hash": "bd55241da847", - "dependencies": { - "dependencies": [ - { - "name": "requests", - "version": "2.34.2" - }, - { - "name": "bs4", - "version": "4.12.3" - }, - { - "name": "langchain_core", - "version": "1.4.0" - }, - { - "name": "markdown", - "version": "3.10.2" - }, - { - "name": "pydantic", - "version": "2.13.4" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 6 - }, - "module": "lfx.components.Notion.add_content_to_page.AddContentToPage" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "JSON", - "group_outputs": false, - "method": "run_model", - "name": "api_run_model", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Tool", - "group_outputs": false, - "method": "build_tool", - "name": "api_build_tool", - "selected": "Tool", - "tool_mode": true, - "types": [ - "Tool" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "block_id": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Page/Block ID", - "dynamic": false, - "info": "The ID of the page/block to add the content.", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "block_id", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "import json\nfrom typing import Any\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom langchain_core.tools import StructuredTool\nfrom markdown import markdown\nfrom pydantic import BaseModel, Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\nMIN_ROWS_IN_TABLE = 3\n\n\nclass AddContentToPage(LCToolComponent):\n display_name: str = \"Add Content to Page \"\n description: str = \"Convert markdown text to Notion blocks and append them to a Notion page.\"\n documentation: str = \"https://developers.notion.com/reference/patch-block-children\"\n icon = \"NotionDirectoryLoader\"\n\n inputs = [\n MultilineInput(\n name=\"markdown_text\",\n display_name=\"Markdown Text\",\n info=\"The markdown text to convert to Notion blocks.\",\n ),\n StrInput(\n name=\"block_id\",\n display_name=\"Page/Block ID\",\n info=\"The ID of the page/block to add the content.\",\n ),\n SecretStrInput(\n name=\"notion_secret\",\n display_name=\"Notion Secret\",\n info=\"The Notion integration token.\",\n required=True,\n ),\n ]\n\n class AddContentToPageSchema(BaseModel):\n markdown_text: str = Field(..., description=\"The markdown text to convert to Notion blocks.\")\n block_id: str = Field(..., description=\"The ID of the page/block to add the content.\")\n\n def run_model(self) -> Data:\n result = self._add_content_to_page(self.markdown_text, self.block_id)\n return Data(data=result, text=json.dumps(result))\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"add_content_to_notion_page\",\n description=\"Convert markdown text to Notion blocks and append them to a Notion page.\",\n func=self._add_content_to_page,\n args_schema=self.AddContentToPageSchema,\n )\n\n def _add_content_to_page(self, markdown_text: str, block_id: str) -> dict[str, Any] | str:\n try:\n html_text = markdown(markdown_text)\n soup = BeautifulSoup(html_text, \"html.parser\")\n blocks = self.process_node(soup)\n\n url = f\"https://api.notion.com/v1/blocks/{block_id}/children\"\n headers = {\n \"Authorization\": f\"Bearer {self.notion_secret}\",\n \"Content-Type\": \"application/json\",\n \"Notion-Version\": \"2022-06-28\",\n }\n\n data = {\n \"children\": blocks,\n }\n\n response = requests.patch(url, headers=headers, json=data, timeout=10)\n response.raise_for_status()\n\n return response.json()\n except requests.exceptions.RequestException as e:\n error_message = f\"Error: Failed to add content to Notion page. {e}\"\n if hasattr(e, \"response\") and e.response is not None:\n error_message += f\" Status code: {e.response.status_code}, Response: {e.response.text}\"\n return error_message\n except Exception as e: # noqa: BLE001\n logger.debug(\"Error adding content to Notion page\", exc_info=True)\n return f\"Error: An unexpected error occurred while adding content to Notion page. {e}\"\n\n def process_node(self, node):\n blocks = []\n if isinstance(node, str):\n text = node.strip()\n if text:\n if text.startswith(\"#\"):\n heading_level = text.count(\"#\", 0, 6)\n heading_text = text[heading_level:].strip()\n if heading_level in range(3):\n blocks.append(self.create_block(f\"heading_{heading_level + 1}\", heading_text))\n else:\n blocks.append(self.create_block(\"paragraph\", text))\n elif node.name == \"h1\":\n blocks.append(self.create_block(\"heading_1\", node.get_text(strip=True)))\n elif node.name == \"h2\":\n blocks.append(self.create_block(\"heading_2\", node.get_text(strip=True)))\n elif node.name == \"h3\":\n blocks.append(self.create_block(\"heading_3\", node.get_text(strip=True)))\n elif node.name == \"p\":\n code_node = node.find(\"code\")\n if code_node:\n code_text = code_node.get_text()\n language, code = self.extract_language_and_code(code_text)\n blocks.append(self.create_block(\"code\", code, language=language))\n elif self.is_table(str(node)):\n blocks.extend(self.process_table(node))\n else:\n blocks.append(self.create_block(\"paragraph\", node.get_text(strip=True)))\n elif node.name == \"ul\":\n blocks.extend(self.process_list(node, \"bulleted_list_item\"))\n elif node.name == \"ol\":\n blocks.extend(self.process_list(node, \"numbered_list_item\"))\n elif node.name == \"blockquote\":\n blocks.append(self.create_block(\"quote\", node.get_text(strip=True)))\n elif node.name == \"hr\":\n blocks.append(self.create_block(\"divider\", \"\"))\n elif node.name == \"img\":\n blocks.append(self.create_block(\"image\", \"\", image_url=node.get(\"src\")))\n elif node.name == \"a\":\n blocks.append(self.create_block(\"bookmark\", node.get_text(strip=True), link_url=node.get(\"href\")))\n elif node.name == \"table\":\n blocks.extend(self.process_table(node))\n\n for child in node.children:\n if isinstance(child, str):\n continue\n blocks.extend(self.process_node(child))\n\n return blocks\n\n def extract_language_and_code(self, code_text):\n lines = code_text.split(\"\\n\")\n language = lines[0].strip()\n code = \"\\n\".join(lines[1:]).strip()\n return language, code\n\n def is_code_block(self, text):\n return text.startswith(\"```\")\n\n def extract_code_block(self, text):\n lines = text.split(\"\\n\")\n language = lines[0].strip(\"`\").strip()\n code = \"\\n\".join(lines[1:]).strip(\"`\").strip()\n return language, code\n\n def is_table(self, text):\n rows = text.split(\"\\n\")\n if len(rows) < MIN_ROWS_IN_TABLE:\n return False\n\n has_separator = False\n for i, row in enumerate(rows):\n if \"|\" in row:\n cells = [cell.strip() for cell in row.split(\"|\")]\n cells = [cell for cell in cells if cell] # Remove empty cells\n if i == 1 and all(set(cell) <= set(\"-|\") for cell in cells):\n has_separator = True\n elif not cells:\n return False\n\n return has_separator\n\n def process_list(self, node, list_type):\n blocks = []\n for item in node.find_all(\"li\"):\n item_text = item.get_text(strip=True)\n checked = item_text.startswith(\"[x]\")\n is_checklist = item_text.startswith(\"[ ]\") or checked\n\n if is_checklist:\n item_text = item_text.replace(\"[x]\", \"\").replace(\"[ ]\", \"\").strip()\n blocks.append(self.create_block(\"to_do\", item_text, checked=checked))\n else:\n blocks.append(self.create_block(list_type, item_text))\n return blocks\n\n def process_table(self, node):\n blocks = []\n header_row = node.find(\"thead\").find(\"tr\") if node.find(\"thead\") else None\n body_rows = node.find(\"tbody\").find_all(\"tr\") if node.find(\"tbody\") else []\n\n if header_row or body_rows:\n table_width = max(\n len(header_row.find_all([\"th\", \"td\"])) if header_row else 0,\n *(len(row.find_all([\"th\", \"td\"])) for row in body_rows),\n )\n\n table_block = self.create_block(\"table\", \"\", table_width=table_width, has_column_header=bool(header_row))\n blocks.append(table_block)\n\n if header_row:\n header_cells = [cell.get_text(strip=True) for cell in header_row.find_all([\"th\", \"td\"])]\n header_row_block = self.create_block(\"table_row\", header_cells)\n blocks.append(header_row_block)\n\n for row in body_rows:\n cells = [cell.get_text(strip=True) for cell in row.find_all([\"th\", \"td\"])]\n row_block = self.create_block(\"table_row\", cells)\n blocks.append(row_block)\n\n return blocks\n\n def create_block(self, block_type: str, content: str, **kwargs) -> dict[str, Any]:\n block: dict[str, Any] = {\n \"object\": \"block\",\n \"type\": block_type,\n block_type: {},\n }\n\n if block_type in {\n \"paragraph\",\n \"heading_1\",\n \"heading_2\",\n \"heading_3\",\n \"bulleted_list_item\",\n \"numbered_list_item\",\n \"quote\",\n }:\n block[block_type][\"rich_text\"] = [\n {\n \"type\": \"text\",\n \"text\": {\n \"content\": content,\n },\n }\n ]\n elif block_type == \"to_do\":\n block[block_type][\"rich_text\"] = [\n {\n \"type\": \"text\",\n \"text\": {\n \"content\": content,\n },\n }\n ]\n block[block_type][\"checked\"] = kwargs.get(\"checked\", False)\n elif block_type == \"code\":\n block[block_type][\"rich_text\"] = [\n {\n \"type\": \"text\",\n \"text\": {\n \"content\": content,\n },\n }\n ]\n block[block_type][\"language\"] = kwargs.get(\"language\", \"plain text\")\n elif block_type == \"image\":\n block[block_type] = {\"type\": \"external\", \"external\": {\"url\": kwargs.get(\"image_url\", \"\")}}\n elif block_type == \"divider\":\n pass\n elif block_type == \"bookmark\":\n block[block_type][\"url\"] = kwargs.get(\"link_url\", \"\")\n elif block_type == \"table\":\n block[block_type][\"table_width\"] = kwargs.get(\"table_width\", 0)\n block[block_type][\"has_column_header\"] = kwargs.get(\"has_column_header\", False)\n block[block_type][\"has_row_header\"] = kwargs.get(\"has_row_header\", False)\n elif block_type == \"table_row\":\n block[block_type][\"cells\"] = [[{\"type\": \"text\", \"text\": {\"content\": cell}} for cell in content]]\n\n return block\n" - }, - "markdown_text": { - "_input_type": "MultilineInput", - "advanced": false, - "ai_enabled": false, - "copy_field": false, - "display_name": "Markdown Text", - "dynamic": false, - "info": "The markdown text to convert to Notion blocks.", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "multiline": true, - "name": "markdown_text", - "override_skip": false, - "password": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "notion_secret": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "Notion Secret", - "dynamic": false, - "info": "The Notion integration token.", - "input_types": [], - "load_from_db": true, - "name": "notion_secret", - "override_skip": false, - "password": true, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "value": "" - } - }, - "tool_mode": false - }, - "NotionDatabaseProperties": { - "base_classes": [ - "JSON", - "Tool" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Retrieve properties of a Notion database.", - "display_name": "List Database Properties ", - "documentation": "https://docs.langflow.org/bundles-notion", - "edited": false, - "field_order": [ - "database_id", - "notion_secret" - ], - "frozen": false, - "icon": "NotionDirectoryLoader", - "legacy": false, - "metadata": { - "code_hash": "83b11f251e20", - "dependencies": { - "dependencies": [ - { - "name": "requests", - "version": "2.34.2" - }, - { - "name": "langchain_core", - "version": "1.4.0" - }, - { - "name": "pydantic", - "version": "2.13.4" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 4 - }, - "module": "lfx.components.Notion.list_database_properties.NotionDatabaseProperties" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "JSON", - "group_outputs": false, - "method": "run_model", - "name": "api_run_model", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Tool", - "group_outputs": false, - "method": "build_tool", - "name": "api_build_tool", - "selected": "Tool", - "tool_mode": true, - "types": [ - "Tool" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "import requests\nfrom langchain_core.tools import StructuredTool\nfrom pydantic import BaseModel, Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import SecretStrInput, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass NotionDatabaseProperties(LCToolComponent):\n display_name: str = \"List Database Properties \"\n description: str = \"Retrieve properties of a Notion database.\"\n documentation: str = \"https://docs.langflow.org/bundles-notion\"\n icon = \"NotionDirectoryLoader\"\n\n inputs = [\n StrInput(\n name=\"database_id\",\n display_name=\"Database ID\",\n info=\"The ID of the Notion database.\",\n ),\n SecretStrInput(\n name=\"notion_secret\",\n display_name=\"Notion Secret\",\n info=\"The Notion integration token.\",\n required=True,\n ),\n ]\n\n class NotionDatabasePropertiesSchema(BaseModel):\n database_id: str = Field(..., description=\"The ID of the Notion database.\")\n\n def run_model(self) -> Data:\n result = self._fetch_database_properties(self.database_id)\n if isinstance(result, str):\n # An error occurred, return it as text\n return Data(text=result)\n # Success, return the properties\n return Data(text=str(result), data=result)\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"notion_database_properties\",\n description=\"Retrieve properties of a Notion database. Input should include the database ID.\",\n func=self._fetch_database_properties,\n args_schema=self.NotionDatabasePropertiesSchema,\n )\n\n def _fetch_database_properties(self, database_id: str) -> dict | str:\n url = f\"https://api.notion.com/v1/databases/{database_id}\"\n headers = {\n \"Authorization\": f\"Bearer {self.notion_secret}\",\n \"Notion-Version\": \"2022-06-28\", # Use the latest supported version\n }\n try:\n response = requests.get(url, headers=headers, timeout=10)\n response.raise_for_status()\n data = response.json()\n return data.get(\"properties\", {})\n except requests.exceptions.RequestException as e:\n return f\"Error fetching Notion database properties: {e}\"\n except ValueError as e:\n return f\"Error parsing Notion API response: {e}\"\n except Exception as e: # noqa: BLE001\n logger.debug(\"Error fetching Notion database properties\", exc_info=True)\n return f\"An unexpected error occurred: {e}\"\n" - }, - "database_id": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Database ID", - "dynamic": false, - "info": "The ID of the Notion database.", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "database_id", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "notion_secret": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "Notion Secret", - "dynamic": false, - "info": "The Notion integration token.", - "input_types": [], - "load_from_db": true, - "name": "notion_secret", - "override_skip": false, - "password": true, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "value": "" - } - }, - "tool_mode": false - }, - "NotionListPages": { - "base_classes": [ - "JSON", - "Tool" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Query a Notion database with filtering and sorting. The input should be a JSON string containing the 'filter' and 'sorts' objects. Example input:\n{\"filter\": {\"property\": \"Status\", \"select\": {\"equals\": \"Done\"}}, \"sorts\": [{\"timestamp\": \"created_time\", \"direction\": \"descending\"}]}", - "display_name": "List Pages ", - "documentation": "https://docs.langflow.org/bundles-notion", - "edited": false, - "field_order": [ - "notion_secret", - "database_id", - "query_json" - ], - "frozen": false, - "icon": "NotionDirectoryLoader", - "legacy": false, - "metadata": { - "code_hash": "1d82c403b3ad", - "dependencies": { - "dependencies": [ - { - "name": "requests", - "version": "2.34.2" - }, - { - "name": "langchain_core", - "version": "1.4.0" - }, - { - "name": "pydantic", - "version": "2.13.4" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 4 - }, - "module": "lfx.components.Notion.list_pages.NotionListPages" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "JSON", - "group_outputs": false, - "method": "run_model", - "name": "api_run_model", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Tool", - "group_outputs": false, - "method": "build_tool", - "name": "api_build_tool", - "selected": "Tool", - "tool_mode": true, - "types": [ - "Tool" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "import json\nfrom typing import Any\n\nimport requests\nfrom langchain_core.tools import StructuredTool\nfrom pydantic import BaseModel, Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass NotionListPages(LCToolComponent):\n display_name: str = \"List Pages \"\n description: str = (\n \"Query a Notion database with filtering and sorting. \"\n \"The input should be a JSON string containing the 'filter' and 'sorts' objects. \"\n \"Example input:\\n\"\n '{\"filter\": {\"property\": \"Status\", \"select\": {\"equals\": \"Done\"}}, '\n '\"sorts\": [{\"timestamp\": \"created_time\", \"direction\": \"descending\"}]}'\n )\n documentation: str = \"https://docs.langflow.org/bundles-notion\"\n icon = \"NotionDirectoryLoader\"\n\n inputs = [\n SecretStrInput(\n name=\"notion_secret\",\n display_name=\"Notion Secret\",\n info=\"The Notion integration token.\",\n required=True,\n ),\n StrInput(\n name=\"database_id\",\n display_name=\"Database ID\",\n info=\"The ID of the Notion database to query.\",\n ),\n MultilineInput(\n name=\"query_json\",\n display_name=\"Database query (JSON)\",\n info=\"A JSON string containing the filters and sorts that will be used for querying the database. \"\n \"Leave empty for no filters or sorts.\",\n ),\n ]\n\n class NotionListPagesSchema(BaseModel):\n database_id: str = Field(..., description=\"The ID of the Notion database to query.\")\n query_json: str | None = Field(\n default=\"\",\n description=\"A JSON string containing the filters and sorts for querying the database. \"\n \"Leave empty for no filters or sorts.\",\n )\n\n def run_model(self) -> list[Data]:\n result = self._query_notion_database(self.database_id, self.query_json)\n\n if isinstance(result, str):\n # An error occurred, return it as a single record\n return [Data(text=result)]\n\n records = []\n combined_text = f\"Pages found: {len(result)}\\n\\n\"\n\n for page in result:\n page_data = {\n \"id\": page[\"id\"],\n \"url\": page[\"url\"],\n \"created_time\": page[\"created_time\"],\n \"last_edited_time\": page[\"last_edited_time\"],\n \"properties\": page[\"properties\"],\n }\n\n text = (\n f\"id: {page['id']}\\n\"\n f\"url: {page['url']}\\n\"\n f\"created_time: {page['created_time']}\\n\"\n f\"last_edited_time: {page['last_edited_time']}\\n\"\n f\"properties: {json.dumps(page['properties'], indent=2)}\\n\\n\"\n )\n\n combined_text += text\n records.append(Data(text=text, **page_data))\n\n self.status = records\n return records\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"notion_list_pages\",\n description=self.description,\n func=self._query_notion_database,\n args_schema=self.NotionListPagesSchema,\n )\n\n def _query_notion_database(self, database_id: str, query_json: str | None = None) -> list[dict[str, Any]] | str:\n url = f\"https://api.notion.com/v1/databases/{database_id}/query\"\n headers = {\n \"Authorization\": f\"Bearer {self.notion_secret}\",\n \"Content-Type\": \"application/json\",\n \"Notion-Version\": \"2022-06-28\",\n }\n\n query_payload = {}\n if query_json and query_json.strip():\n try:\n query_payload = json.loads(query_json)\n except json.JSONDecodeError as e:\n return f\"Invalid JSON format for query: {e}\"\n\n try:\n response = requests.post(url, headers=headers, json=query_payload, timeout=10)\n response.raise_for_status()\n results = response.json()\n return results[\"results\"]\n except requests.exceptions.RequestException as e:\n return f\"Error querying Notion database: {e}\"\n except KeyError:\n return \"Unexpected response format from Notion API\"\n except Exception as e: # noqa: BLE001\n logger.debug(\"Error querying Notion database\", exc_info=True)\n return f\"An unexpected error occurred: {e}\"\n" - }, - "database_id": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Database ID", - "dynamic": false, - "info": "The ID of the Notion database to query.", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "database_id", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "notion_secret": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "Notion Secret", - "dynamic": false, - "info": "The Notion integration token.", - "input_types": [], - "load_from_db": true, - "name": "notion_secret", - "override_skip": false, - "password": true, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "query_json": { - "_input_type": "MultilineInput", - "advanced": false, - "ai_enabled": false, - "copy_field": false, - "display_name": "Database query (JSON)", - "dynamic": false, - "info": "A JSON string containing the filters and sorts that will be used for querying the database. Leave empty for no filters or sorts.", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "multiline": true, - "name": "query_json", - "override_skip": false, - "password": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - } - }, - "tool_mode": false - }, - "NotionPageContent": { - "base_classes": [ - "JSON", - "Tool" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Retrieve the content of a Notion page as plain text.", - "display_name": "Page Content Viewer ", - "documentation": "https://docs.langflow.org/bundles-notion", - "edited": false, - "field_order": [ - "page_id", - "notion_secret" - ], - "frozen": false, - "icon": "NotionDirectoryLoader", - "legacy": false, - "metadata": { - "code_hash": "759c5b3cc969", - "dependencies": { - "dependencies": [ - { - "name": "requests", - "version": "2.34.2" - }, - { - "name": "langchain_core", - "version": "1.4.0" - }, - { - "name": "pydantic", - "version": "2.13.4" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 4 - }, - "module": "lfx.components.Notion.page_content_viewer.NotionPageContent" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "JSON", - "group_outputs": false, - "method": "run_model", - "name": "api_run_model", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Tool", - "group_outputs": false, - "method": "build_tool", - "name": "api_build_tool", - "selected": "Tool", - "tool_mode": true, - "types": [ - "Tool" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "import requests\nfrom langchain_core.tools import StructuredTool\nfrom pydantic import BaseModel, Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import SecretStrInput, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass NotionPageContent(LCToolComponent):\n display_name = \"Page Content Viewer \"\n description = \"Retrieve the content of a Notion page as plain text.\"\n documentation = \"https://docs.langflow.org/bundles-notion\"\n icon = \"NotionDirectoryLoader\"\n\n inputs = [\n StrInput(\n name=\"page_id\",\n display_name=\"Page ID\",\n info=\"The ID of the Notion page to retrieve.\",\n ),\n SecretStrInput(\n name=\"notion_secret\",\n display_name=\"Notion Secret\",\n info=\"The Notion integration token.\",\n required=True,\n ),\n ]\n\n class NotionPageContentSchema(BaseModel):\n page_id: str = Field(..., description=\"The ID of the Notion page to retrieve.\")\n\n def run_model(self) -> Data:\n result = self._retrieve_page_content(self.page_id)\n if isinstance(result, str) and result.startswith(\"Error:\"):\n # An error occurred, return it as text\n return Data(text=result)\n # Success, return the content\n return Data(text=result, data={\"content\": result})\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"notion_page_content\",\n description=\"Retrieve the content of a Notion page as plain text.\",\n func=self._retrieve_page_content,\n args_schema=self.NotionPageContentSchema,\n )\n\n def _retrieve_page_content(self, page_id: str) -> str:\n blocks_url = f\"https://api.notion.com/v1/blocks/{page_id}/children?page_size=100\"\n headers = {\n \"Authorization\": f\"Bearer {self.notion_secret}\",\n \"Notion-Version\": \"2022-06-28\",\n }\n try:\n blocks_response = requests.get(blocks_url, headers=headers, timeout=10)\n blocks_response.raise_for_status()\n blocks_data = blocks_response.json()\n return self.parse_blocks(blocks_data.get(\"results\", []))\n except requests.exceptions.RequestException as e:\n error_message = f\"Error: Failed to retrieve Notion page content. {e}\"\n if hasattr(e, \"response\") and e.response is not None:\n error_message += f\" Status code: {e.response.status_code}, Response: {e.response.text}\"\n return error_message\n except Exception as e: # noqa: BLE001\n logger.debug(\"Error retrieving Notion page content\", exc_info=True)\n return f\"Error: An unexpected error occurred while retrieving Notion page content. {e}\"\n\n def parse_blocks(self, blocks: list) -> str:\n content = \"\"\n for block in blocks:\n block_type = block.get(\"type\")\n if block_type in {\"paragraph\", \"heading_1\", \"heading_2\", \"heading_3\", \"quote\"}:\n content += self.parse_rich_text(block[block_type].get(\"rich_text\", [])) + \"\\n\\n\"\n elif block_type in {\"bulleted_list_item\", \"numbered_list_item\"}:\n content += self.parse_rich_text(block[block_type].get(\"rich_text\", [])) + \"\\n\"\n elif block_type == \"to_do\":\n content += self.parse_rich_text(block[\"to_do\"].get(\"rich_text\", [])) + \"\\n\"\n elif block_type == \"code\":\n content += self.parse_rich_text(block[\"code\"].get(\"rich_text\", [])) + \"\\n\\n\"\n elif block_type == \"image\":\n content += f\"[Image: {block['image'].get('external', {}).get('url', 'No URL')}]\\n\\n\"\n elif block_type == \"divider\":\n content += \"---\\n\\n\"\n return content.strip()\n\n def parse_rich_text(self, rich_text: list) -> str:\n return \"\".join(segment.get(\"plain_text\", \"\") for segment in rich_text)\n\n def __call__(self, *args, **kwargs):\n return self._retrieve_page_content(*args, **kwargs)\n" - }, - "notion_secret": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "Notion Secret", - "dynamic": false, - "info": "The Notion integration token.", - "input_types": [], - "load_from_db": true, - "name": "notion_secret", - "override_skip": false, - "password": true, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "page_id": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Page ID", - "dynamic": false, - "info": "The ID of the Notion page to retrieve.", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "page_id", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - } - }, - "tool_mode": false - }, - "NotionPageCreator": { - "base_classes": [ - "JSON", - "Tool" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "A component for creating Notion pages.", - "display_name": "Create Page ", - "documentation": "https://docs.langflow.org/bundles-notion", - "edited": false, - "field_order": [ - "database_id", - "notion_secret", - "properties_json" - ], - "frozen": false, - "icon": "NotionDirectoryLoader", - "legacy": false, - "metadata": { - "code_hash": "31d9c58f9362", - "dependencies": { - "dependencies": [ - { - "name": "requests", - "version": "2.34.2" - }, - { - "name": "langchain_core", - "version": "1.4.0" - }, - { - "name": "pydantic", - "version": "2.13.4" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 4 - }, - "module": "lfx.components.Notion.create_page.NotionPageCreator" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "JSON", - "group_outputs": false, - "method": "run_model", - "name": "api_run_model", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Tool", - "group_outputs": false, - "method": "build_tool", - "name": "api_build_tool", - "selected": "Tool", - "tool_mode": true, - "types": [ - "Tool" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "import json\nfrom typing import Any\n\nimport requests\nfrom langchain_core.tools import StructuredTool\nfrom pydantic import BaseModel, Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass NotionPageCreator(LCToolComponent):\n display_name: str = \"Create Page \"\n description: str = \"A component for creating Notion pages.\"\n documentation: str = \"https://docs.langflow.org/bundles-notion\"\n icon = \"NotionDirectoryLoader\"\n\n inputs = [\n StrInput(\n name=\"database_id\",\n display_name=\"Database ID\",\n info=\"The ID of the Notion database.\",\n ),\n SecretStrInput(\n name=\"notion_secret\",\n display_name=\"Notion Secret\",\n info=\"The Notion integration token.\",\n required=True,\n ),\n MultilineInput(\n name=\"properties_json\",\n display_name=\"Properties (JSON)\",\n info=\"The properties of the new page as a JSON string.\",\n ),\n ]\n\n class NotionPageCreatorSchema(BaseModel):\n database_id: str = Field(..., description=\"The ID of the Notion database.\")\n properties_json: str = Field(..., description=\"The properties of the new page as a JSON string.\")\n\n def run_model(self) -> Data:\n result = self._create_notion_page(self.database_id, self.properties_json)\n if isinstance(result, str):\n # An error occurred, return it as text\n return Data(text=result)\n # Success, return the created page data\n output = \"Created page properties:\\n\"\n for prop_name, prop_value in result.get(\"properties\", {}).items():\n output += f\"{prop_name}: {prop_value}\\n\"\n return Data(text=output, data=result)\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"create_notion_page\",\n description=\"Create a new page in a Notion database. \"\n \"IMPORTANT: Use the tool to check the Database properties for more details before using this tool.\",\n func=self._create_notion_page,\n args_schema=self.NotionPageCreatorSchema,\n )\n\n def _create_notion_page(self, database_id: str, properties_json: str) -> dict[str, Any] | str:\n if not database_id or not properties_json:\n return \"Invalid input. Please provide 'database_id' and 'properties_json'.\"\n\n try:\n properties = json.loads(properties_json)\n except json.JSONDecodeError as e:\n return f\"Invalid properties format. Please provide a valid JSON string. Error: {e}\"\n\n headers = {\n \"Authorization\": f\"Bearer {self.notion_secret}\",\n \"Content-Type\": \"application/json\",\n \"Notion-Version\": \"2022-06-28\",\n }\n\n data = {\n \"parent\": {\"database_id\": database_id},\n \"properties\": properties,\n }\n\n try:\n response = requests.post(\"https://api.notion.com/v1/pages\", headers=headers, json=data, timeout=10)\n response.raise_for_status()\n return response.json()\n except requests.exceptions.RequestException as e:\n error_message = f\"Failed to create Notion page. Error: {e}\"\n if hasattr(e, \"response\") and e.response is not None:\n error_message += f\" Status code: {e.response.status_code}, Response: {e.response.text}\"\n return error_message\n\n def __call__(self, *args, **kwargs):\n return self._create_notion_page(*args, **kwargs)\n" - }, - "database_id": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Database ID", - "dynamic": false, - "info": "The ID of the Notion database.", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "database_id", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "notion_secret": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "Notion Secret", - "dynamic": false, - "info": "The Notion integration token.", - "input_types": [], - "load_from_db": true, - "name": "notion_secret", - "override_skip": false, - "password": true, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "properties_json": { - "_input_type": "MultilineInput", - "advanced": false, - "ai_enabled": false, - "copy_field": false, - "display_name": "Properties (JSON)", - "dynamic": false, - "info": "The properties of the new page as a JSON string.", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "multiline": true, - "name": "properties_json", - "override_skip": false, - "password": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - } - }, - "tool_mode": false - }, - "NotionPageUpdate": { - "base_classes": [ - "JSON", - "Tool" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Update the properties of a Notion page.", - "display_name": "Update Page Property ", - "documentation": "https://docs.langflow.org/bundles-notion", - "edited": false, - "field_order": [ - "page_id", - "properties", - "notion_secret" - ], - "frozen": false, - "icon": "NotionDirectoryLoader", - "legacy": false, - "metadata": { - "code_hash": "72aaad0ea8e7", - "dependencies": { - "dependencies": [ - { - "name": "requests", - "version": "2.34.2" - }, - { - "name": "langchain_core", - "version": "1.4.0" - }, - { - "name": "pydantic", - "version": "2.13.4" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 4 - }, - "module": "lfx.components.Notion.update_page_property.NotionPageUpdate" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "JSON", - "group_outputs": false, - "method": "run_model", - "name": "api_run_model", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Tool", - "group_outputs": false, - "method": "build_tool", - "name": "api_build_tool", - "selected": "Tool", - "tool_mode": true, - "types": [ - "Tool" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "import json\nfrom typing import Any\n\nimport requests\nfrom langchain_core.tools import StructuredTool\nfrom pydantic import BaseModel, Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import MultilineInput, SecretStrInput, StrInput\nfrom lfx.log.logger import logger\nfrom lfx.schema.data import Data\n\n\nclass NotionPageUpdate(LCToolComponent):\n display_name: str = \"Update Page Property \"\n description: str = \"Update the properties of a Notion page.\"\n documentation: str = \"https://docs.langflow.org/bundles-notion\"\n icon = \"NotionDirectoryLoader\"\n\n inputs = [\n StrInput(\n name=\"page_id\",\n display_name=\"Page ID\",\n info=\"The ID of the Notion page to update.\",\n ),\n MultilineInput(\n name=\"properties\",\n display_name=\"Properties\",\n info=\"The properties to update on the page (as a JSON string or a dictionary).\",\n ),\n SecretStrInput(\n name=\"notion_secret\",\n display_name=\"Notion Secret\",\n info=\"The Notion integration token.\",\n required=True,\n ),\n ]\n\n class NotionPageUpdateSchema(BaseModel):\n page_id: str = Field(..., description=\"The ID of the Notion page to update.\")\n properties: str | dict[str, Any] = Field(\n ..., description=\"The properties to update on the page (as a JSON string or a dictionary).\"\n )\n\n def run_model(self) -> Data:\n result = self._update_notion_page(self.page_id, self.properties)\n if isinstance(result, str):\n # An error occurred, return it as text\n return Data(text=result)\n # Success, return the updated page data\n output = \"Updated page properties:\\n\"\n for prop_name, prop_value in result.get(\"properties\", {}).items():\n output += f\"{prop_name}: {prop_value}\\n\"\n return Data(text=output, data=result)\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"update_notion_page\",\n description=\"Update the properties of a Notion page. \"\n \"IMPORTANT: Use the tool to check the Database properties for more details before using this tool.\",\n func=self._update_notion_page,\n args_schema=self.NotionPageUpdateSchema,\n )\n\n def _update_notion_page(self, page_id: str, properties: str | dict[str, Any]) -> dict[str, Any] | str:\n url = f\"https://api.notion.com/v1/pages/{page_id}\"\n headers = {\n \"Authorization\": f\"Bearer {self.notion_secret}\",\n \"Content-Type\": \"application/json\",\n \"Notion-Version\": \"2022-06-28\", # Use the latest supported version\n }\n\n # Parse properties if it's a string\n if isinstance(properties, str):\n try:\n parsed_properties = json.loads(properties)\n except json.JSONDecodeError as e:\n error_message = f\"Invalid JSON format for properties: {e}\"\n logger.exception(error_message)\n return error_message\n\n else:\n parsed_properties = properties\n\n data = {\"properties\": parsed_properties}\n\n try:\n logger.info(f\"Sending request to Notion API: URL: {url}, Data: {json.dumps(data)}\")\n response = requests.patch(url, headers=headers, json=data, timeout=10)\n response.raise_for_status()\n updated_page = response.json()\n\n logger.info(f\"Successfully updated Notion page. Response: {json.dumps(updated_page)}\")\n except requests.exceptions.HTTPError as e:\n error_message = f\"HTTP Error occurred: {e}\"\n if e.response is not None:\n error_message += f\"\\nStatus code: {e.response.status_code}\"\n error_message += f\"\\nResponse body: {e.response.text}\"\n logger.exception(error_message)\n return error_message\n except requests.exceptions.RequestException as e:\n error_message = f\"An error occurred while making the request: {e}\"\n logger.exception(error_message)\n return error_message\n except Exception as e: # noqa: BLE001\n error_message = f\"An unexpected error occurred: {e}\"\n logger.exception(error_message)\n return error_message\n\n return updated_page\n\n def __call__(self, *args, **kwargs):\n return self._update_notion_page(*args, **kwargs)\n" - }, - "notion_secret": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "Notion Secret", - "dynamic": false, - "info": "The Notion integration token.", - "input_types": [], - "load_from_db": true, - "name": "notion_secret", - "override_skip": false, - "password": true, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "page_id": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Page ID", - "dynamic": false, - "info": "The ID of the Notion page to update.", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "page_id", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "properties": { - "_input_type": "MultilineInput", - "advanced": false, - "ai_enabled": false, - "copy_field": false, - "display_name": "Properties", - "dynamic": false, - "info": "The properties to update on the page (as a JSON string or a dictionary).", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "multiline": true, - "name": "properties", - "override_skip": false, - "password": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_input": true, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - } - }, - "tool_mode": false - }, - "NotionSearch": { - "base_classes": [ - "JSON", - "Tool" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Searches all pages and databases that have been shared with an integration.", - "display_name": "Search ", - "documentation": "https://docs.langflow.org/bundles-notion", - "edited": false, - "field_order": [ - "notion_secret", - "query", - "filter_value", - "sort_direction" - ], - "frozen": false, - "icon": "NotionDirectoryLoader", - "legacy": false, - "metadata": { - "code_hash": "5cfaba329803", - "dependencies": { - "dependencies": [ - { - "name": "requests", - "version": "2.34.2" - }, - { - "name": "langchain_core", - "version": "1.4.0" - }, - { - "name": "pydantic", - "version": "2.13.4" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 4 - }, - "module": "lfx.components.Notion.search.NotionSearch" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "JSON", - "group_outputs": false, - "method": "run_model", - "name": "api_run_model", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Tool", - "group_outputs": false, - "method": "build_tool", - "name": "api_build_tool", - "selected": "Tool", - "tool_mode": true, - "types": [ - "Tool" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "from typing import Any\n\nimport requests\nfrom langchain_core.tools import StructuredTool\nfrom pydantic import BaseModel, Field\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import DropdownInput, SecretStrInput, StrInput\nfrom lfx.schema.data import Data\n\n\nclass NotionSearch(LCToolComponent):\n display_name: str = \"Search \"\n description: str = \"Searches all pages and databases that have been shared with an integration.\"\n documentation: str = \"https://docs.langflow.org/bundles-notion\"\n icon = \"NotionDirectoryLoader\"\n\n inputs = [\n SecretStrInput(\n name=\"notion_secret\",\n display_name=\"Notion Secret\",\n info=\"The Notion integration token.\",\n required=True,\n ),\n StrInput(\n name=\"query\",\n display_name=\"Search Query\",\n info=\"The text that the API compares page and database titles against.\",\n ),\n DropdownInput(\n name=\"filter_value\",\n display_name=\"Filter Type\",\n info=\"Limits the results to either only pages or only databases.\",\n options=[\"page\", \"database\"],\n value=\"page\",\n ),\n DropdownInput(\n name=\"sort_direction\",\n display_name=\"Sort Direction\",\n info=\"The direction to sort the results.\",\n options=[\"ascending\", \"descending\"],\n value=\"descending\",\n ),\n ]\n\n class NotionSearchSchema(BaseModel):\n query: str = Field(..., description=\"The search query text.\")\n filter_value: str = Field(default=\"page\", description=\"Filter type: 'page' or 'database'.\")\n sort_direction: str = Field(default=\"descending\", description=\"Sort direction: 'ascending' or 'descending'.\")\n\n def run_model(self) -> list[Data]:\n results = self._search_notion(self.query, self.filter_value, self.sort_direction)\n records = []\n combined_text = f\"Results found: {len(results)}\\n\\n\"\n\n for result in results:\n result_data = {\n \"id\": result[\"id\"],\n \"type\": result[\"object\"],\n \"last_edited_time\": result[\"last_edited_time\"],\n }\n\n if result[\"object\"] == \"page\":\n result_data[\"title_or_url\"] = result[\"url\"]\n text = f\"id: {result['id']}\\ntitle_or_url: {result['url']}\\n\"\n elif result[\"object\"] == \"database\":\n if \"title\" in result and isinstance(result[\"title\"], list) and len(result[\"title\"]) > 0:\n result_data[\"title_or_url\"] = result[\"title\"][0][\"plain_text\"]\n text = f\"id: {result['id']}\\ntitle_or_url: {result['title'][0]['plain_text']}\\n\"\n else:\n result_data[\"title_or_url\"] = \"N/A\"\n text = f\"id: {result['id']}\\ntitle_or_url: N/A\\n\"\n\n text += f\"type: {result['object']}\\nlast_edited_time: {result['last_edited_time']}\\n\\n\"\n combined_text += text\n records.append(Data(text=text, data=result_data))\n\n self.status = records\n return records\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"notion_search\",\n description=\"Search Notion pages and databases. \"\n \"Input should include the search query and optionally filter type and sort direction.\",\n func=self._search_notion,\n args_schema=self.NotionSearchSchema,\n )\n\n def _search_notion(\n self, query: str, filter_value: str = \"page\", sort_direction: str = \"descending\"\n ) -> list[dict[str, Any]]:\n url = \"https://api.notion.com/v1/search\"\n headers = {\n \"Authorization\": f\"Bearer {self.notion_secret}\",\n \"Content-Type\": \"application/json\",\n \"Notion-Version\": \"2022-06-28\",\n }\n\n data = {\n \"query\": query,\n \"filter\": {\"value\": filter_value, \"property\": \"object\"},\n \"sort\": {\"direction\": sort_direction, \"timestamp\": \"last_edited_time\"},\n }\n\n response = requests.post(url, headers=headers, json=data, timeout=10)\n response.raise_for_status()\n\n results = response.json()\n return results[\"results\"]\n" - }, - "filter_value": { - "_input_type": "DropdownInput", - "advanced": false, - "combobox": false, - "dialog_inputs": {}, - "display_name": "Filter Type", - "dynamic": false, - "external_options": {}, - "info": "Limits the results to either only pages or only databases.", - "name": "filter_value", - "options": [ - "page", - "database" - ], - "options_metadata": [], - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "toggle": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": true, - "type": "str", - "value": "page" - }, - "notion_secret": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "Notion Secret", - "dynamic": false, - "info": "The Notion integration token.", - "input_types": [], - "load_from_db": true, - "name": "notion_secret", - "override_skip": false, - "password": true, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "query": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Search Query", - "dynamic": false, - "info": "The text that the API compares page and database titles against.", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "query", - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": false, - "type": "str", - "value": "" - }, - "sort_direction": { - "_input_type": "DropdownInput", - "advanced": false, - "combobox": false, - "dialog_inputs": {}, - "display_name": "Sort Direction", - "dynamic": false, - "external_options": {}, - "info": "The direction to sort the results.", - "name": "sort_direction", - "options": [ - "ascending", - "descending" - ], - "options_metadata": [], - "override_skip": false, - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "toggle": false, - "tool_mode": false, - "trace_as_metadata": true, - "track_in_telemetry": true, - "type": "str", - "value": "descending" - } - }, - "tool_mode": false - }, - "NotionUserList": { - "base_classes": [ - "JSON", - "Tool" - ], - "beta": false, - "conditional_paths": [], - "custom_fields": {}, - "description": "Retrieve users from Notion.", - "display_name": "List Users ", - "documentation": "https://docs.langflow.org/bundles-notion", - "edited": false, - "field_order": [ - "notion_secret" - ], - "frozen": false, - "icon": "NotionDirectoryLoader", - "legacy": false, - "metadata": { - "code_hash": "7f03aedc507d", - "dependencies": { - "dependencies": [ - { - "name": "requests", - "version": "2.34.2" - }, - { - "name": "langchain_core", - "version": "1.4.0" - }, - { - "name": "pydantic", - "version": "2.13.4" - }, - { - "name": "lfx", - "version": null - } - ], - "total_dependencies": 4 - }, - "module": "lfx.components.Notion.list_users.NotionUserList" - }, - "minimized": false, - "output_types": [], - "outputs": [ - { - "allows_loop": false, - "cache": true, - "display_name": "JSON", - "group_outputs": false, - "method": "run_model", - "name": "api_run_model", - "selected": "JSON", - "tool_mode": true, - "types": [ - "JSON" - ], - "value": "__UNDEFINED__" - }, - { - "allows_loop": false, - "cache": true, - "display_name": "Tool", - "group_outputs": false, - "method": "build_tool", - "name": "api_build_tool", - "selected": "Tool", - "tool_mode": true, - "types": [ - "Tool" - ], - "value": "__UNDEFINED__" - } - ], - "pinned": false, - "template": { - "_type": "Component", - "code": { - "advanced": true, - "dynamic": true, - "fileTypes": [], - "file_path": "", - "info": "", - "list": false, - "load_from_db": false, - "multiline": true, - "name": "code", - "password": false, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "type": "code", - "value": "import requests\nfrom langchain_core.tools import StructuredTool\nfrom pydantic import BaseModel\n\nfrom lfx.base.langchain_utilities.model import LCToolComponent\nfrom lfx.field_typing import Tool\nfrom lfx.inputs.inputs import SecretStrInput\nfrom lfx.schema.data import Data\n\n\nclass NotionUserList(LCToolComponent):\n display_name = \"List Users \"\n description = \"Retrieve users from Notion.\"\n documentation = \"https://docs.langflow.org/bundles-notion\"\n icon = \"NotionDirectoryLoader\"\n\n inputs = [\n SecretStrInput(\n name=\"notion_secret\",\n display_name=\"Notion Secret\",\n info=\"The Notion integration token.\",\n required=True,\n ),\n ]\n\n class NotionUserListSchema(BaseModel):\n pass\n\n def run_model(self) -> list[Data]:\n users = self._list_users()\n records = []\n combined_text = \"\"\n\n for user in users:\n output = \"User:\\n\"\n for key, value in user.items():\n output += f\"{key.replace('_', ' ').title()}: {value}\\n\"\n output += \"________________________\\n\"\n\n combined_text += output\n records.append(Data(text=output, data=user))\n\n self.status = records\n return records\n\n def build_tool(self) -> Tool:\n return StructuredTool.from_function(\n name=\"notion_list_users\",\n description=\"Retrieve users from Notion.\",\n func=self._list_users,\n args_schema=self.NotionUserListSchema,\n )\n\n def _list_users(self) -> list[dict]:\n url = \"https://api.notion.com/v1/users\"\n headers = {\n \"Authorization\": f\"Bearer {self.notion_secret}\",\n \"Notion-Version\": \"2022-06-28\",\n }\n\n response = requests.get(url, headers=headers, timeout=10)\n response.raise_for_status()\n\n data = response.json()\n results = data[\"results\"]\n\n users = []\n for user in results:\n user_data = {\n \"id\": user[\"id\"],\n \"type\": user[\"type\"],\n \"name\": user.get(\"name\", \"\"),\n \"avatar_url\": user.get(\"avatar_url\", \"\"),\n }\n users.append(user_data)\n\n return users\n" - }, - "notion_secret": { - "_input_type": "SecretStrInput", - "advanced": false, - "display_name": "Notion Secret", - "dynamic": false, - "info": "The Notion integration token.", - "input_types": [], - "load_from_db": true, - "name": "notion_secret", - "override_skip": false, - "password": true, - "placeholder": "", - "required": true, - "show": true, - "title_case": false, - "track_in_telemetry": false, - "type": "str", - "value": "" - } - }, - "tool_mode": false - } - } - ], [ "agentics", { @@ -32550,9 +30987,9 @@ ] ], "metadata": { - "num_components": 140, - "num_modules": 22 + "num_components": 131, + "num_modules": 20 }, - "sha256": "733aff4334f3f5751835fe7244f78e70556846522a41a050ceafb82fe91083b3", + "sha256": "513e5389677c0c442beef080340961c78b97bc531e4e1e62d4c581575b6720d7", "version": "1.11.0" } diff --git a/src/lfx/src/lfx/components/FAISS/__init__.py b/src/lfx/src/lfx/components/FAISS/__init__.py index 93654b7ac1..6b6dbe064d 100644 --- a/src/lfx/src/lfx/components/FAISS/__init__.py +++ b/src/lfx/src/lfx/components/FAISS/__init__.py @@ -1,34 +1,22 @@ -from __future__ import annotations +# lfx-bundles-shim +"""Compatibility shim: lfx.components.FAISS moved to lfx-bundles. -from typing import TYPE_CHECKING, Any +This module re-points to the installed bundle distribution. It contains +no component implementations and no third-party dependencies, and is +removed once the deprecation window closes (M4). +""" -from lfx.components._importing import import_mod +import importlib +import sys -if TYPE_CHECKING: - from .faiss import FaissVectorStoreComponent - -_dynamic_imports = { - "FaissVectorStoreComponent": "faiss", -} - -__all__ = [ - "FaissVectorStoreComponent", -] - - -def __getattr__(attr_name: str) -> Any: - """Lazily import FAISS components on attribute access.""" - if attr_name not in _dynamic_imports: - msg = f"module '{__name__}' has no attribute '{attr_name}'" - raise AttributeError(msg) - try: - result = import_mod(attr_name, _dynamic_imports[attr_name], __spec__.parent) - except (ModuleNotFoundError, ImportError, AttributeError) as e: - msg = f"Could not import '{attr_name}' from '{__name__}': {e}" - raise AttributeError(msg) from e - globals()[attr_name] = result - return result - - -def __dir__() -> list[str]: - return list(__all__) +try: + sys.modules[__name__] = importlib.import_module("lfx_bundles.faiss") +except ModuleNotFoundError as exc: + if exc.name is not None and (exc.name == "lfx_bundles" or exc.name.startswith("lfx_bundles.")): + msg = ( + "The 'FAISS' components moved to the 'lfx-bundles' distribution. " + "Install it with: pip install lfx-bundles " + "(or 'pip install langflow', which bundles it)." + ) + raise ModuleNotFoundError(msg, name="lfx_bundles") from exc + raise diff --git a/src/lfx/src/lfx/components/Notion/__init__.py b/src/lfx/src/lfx/components/Notion/__init__.py index 9e50918fa4..04dc30fae0 100644 --- a/src/lfx/src/lfx/components/Notion/__init__.py +++ b/src/lfx/src/lfx/components/Notion/__init__.py @@ -1,19 +1,22 @@ -from .add_content_to_page import AddContentToPage -from .create_page import NotionPageCreator -from .list_database_properties import NotionDatabaseProperties -from .list_pages import NotionListPages -from .list_users import NotionUserList -from .page_content_viewer import NotionPageContent -from .search import NotionSearch -from .update_page_property import NotionPageUpdate +# lfx-bundles-shim +"""Compatibility shim: lfx.components.Notion moved to lfx-bundles. -__all__ = [ - "AddContentToPage", - "NotionDatabaseProperties", - "NotionListPages", - "NotionPageContent", - "NotionPageCreator", - "NotionPageUpdate", - "NotionSearch", - "NotionUserList", -] +This module re-points to the installed bundle distribution. It contains +no component implementations and no third-party dependencies, and is +removed once the deprecation window closes (M4). +""" + +import importlib +import sys + +try: + sys.modules[__name__] = importlib.import_module("lfx_bundles.notion") +except ModuleNotFoundError as exc: + if exc.name is not None and (exc.name == "lfx_bundles" or exc.name.startswith("lfx_bundles.")): + msg = ( + "The 'Notion' components moved to the 'lfx-bundles' distribution. " + "Install it with: pip install lfx-bundles " + "(or 'pip install langflow', which bundles it)." + ) + raise ModuleNotFoundError(msg, name="lfx_bundles") from exc + raise diff --git a/src/lfx/src/lfx/extension/migration/migration_table.json b/src/lfx/src/lfx/extension/migration/migration_table.json index 072980ec8f..40e6f142bc 100644 --- a/src/lfx/src/lfx/extension/migration/migration_table.json +++ b/src/lfx/src/lfx/extension/migration/migration_table.json @@ -4580,6 +4580,186 @@ "legacy_slot": "ext:langwatch:LangWatchComponent@official-pre-a", "target": "ext:langwatch:LangWatchComponent@official", "added_in": "1.11.0" + }, + { + "bare_class_name": "FaissVectorStoreComponent", + "target": "ext:faiss:FaissVectorStoreComponent@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.FAISS.faiss.FaissVectorStoreComponent", + "target": "ext:faiss:FaissVectorStoreComponent@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.FAISS.FaissVectorStoreComponent", + "target": "ext:faiss:FaissVectorStoreComponent@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:faiss:FaissVectorStoreComponent@official-pre-a", + "target": "ext:faiss:FaissVectorStoreComponent@official", + "added_in": "1.11.0" + }, + { + "bare_class_name": "AddContentToPage", + "target": "ext:notion:AddContentToPage@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.add_content_to_page.AddContentToPage", + "target": "ext:notion:AddContentToPage@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.AddContentToPage", + "target": "ext:notion:AddContentToPage@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:notion:AddContentToPage@official-pre-a", + "target": "ext:notion:AddContentToPage@official", + "added_in": "1.11.0" + }, + { + "bare_class_name": "NotionPageCreator", + "target": "ext:notion:NotionPageCreator@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.create_page.NotionPageCreator", + "target": "ext:notion:NotionPageCreator@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.NotionPageCreator", + "target": "ext:notion:NotionPageCreator@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:notion:NotionPageCreator@official-pre-a", + "target": "ext:notion:NotionPageCreator@official", + "added_in": "1.11.0" + }, + { + "bare_class_name": "NotionDatabaseProperties", + "target": "ext:notion:NotionDatabaseProperties@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.list_database_properties.NotionDatabaseProperties", + "target": "ext:notion:NotionDatabaseProperties@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.NotionDatabaseProperties", + "target": "ext:notion:NotionDatabaseProperties@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:notion:NotionDatabaseProperties@official-pre-a", + "target": "ext:notion:NotionDatabaseProperties@official", + "added_in": "1.11.0" + }, + { + "bare_class_name": "NotionListPages", + "target": "ext:notion:NotionListPages@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.list_pages.NotionListPages", + "target": "ext:notion:NotionListPages@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.NotionListPages", + "target": "ext:notion:NotionListPages@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:notion:NotionListPages@official-pre-a", + "target": "ext:notion:NotionListPages@official", + "added_in": "1.11.0" + }, + { + "bare_class_name": "NotionUserList", + "target": "ext:notion:NotionUserList@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.list_users.NotionUserList", + "target": "ext:notion:NotionUserList@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.NotionUserList", + "target": "ext:notion:NotionUserList@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:notion:NotionUserList@official-pre-a", + "target": "ext:notion:NotionUserList@official", + "added_in": "1.11.0" + }, + { + "bare_class_name": "NotionPageContent", + "target": "ext:notion:NotionPageContent@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.page_content_viewer.NotionPageContent", + "target": "ext:notion:NotionPageContent@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.NotionPageContent", + "target": "ext:notion:NotionPageContent@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:notion:NotionPageContent@official-pre-a", + "target": "ext:notion:NotionPageContent@official", + "added_in": "1.11.0" + }, + { + "bare_class_name": "NotionSearch", + "target": "ext:notion:NotionSearch@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.search.NotionSearch", + "target": "ext:notion:NotionSearch@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.NotionSearch", + "target": "ext:notion:NotionSearch@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:notion:NotionSearch@official-pre-a", + "target": "ext:notion:NotionSearch@official", + "added_in": "1.11.0" + }, + { + "bare_class_name": "NotionPageUpdate", + "target": "ext:notion:NotionPageUpdate@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.update_page_property.NotionPageUpdate", + "target": "ext:notion:NotionPageUpdate@official", + "added_in": "1.11.0" + }, + { + "import_path": "lfx.components.Notion.NotionPageUpdate", + "target": "ext:notion:NotionPageUpdate@official", + "added_in": "1.11.0" + }, + { + "legacy_slot": "ext:notion:NotionPageUpdate@official-pre-a", + "target": "ext:notion:NotionPageUpdate@official", + "added_in": "1.11.0" } ], "ambiguous_bare_names": [ diff --git a/src/lfx/src/lfx/interface/components.py b/src/lfx/src/lfx/interface/components.py index 5d4a656f88..bced62ea37 100644 --- a/src/lfx/src/lfx/interface/components.py +++ b/src/lfx/src/lfx/interface/components.py @@ -640,7 +640,7 @@ def _process_single_module(modname: str) -> tuple[str, dict] | None: logger.error(f"Failed to import module {modname}: {e}", exc_info=True) return None # Extract the top-level subpackage name after "lfx.components." - # e.g., "lfx.components.Notion.add_content_to_page" -> "Notion" + # e.g., "lfx.components.processing.combine_text" -> "processing" mod_parts = modname.split(".") if len(mod_parts) <= MIN_MODULE_PARTS: return None diff --git a/src/lfx/tests/unit/components/test_bundle_shims.py b/src/lfx/tests/unit/components/test_bundle_shims.py index 27eda16161..c0b690de0a 100644 --- a/src/lfx/tests/unit/components/test_bundle_shims.py +++ b/src/lfx/tests/unit/components/test_bundle_shims.py @@ -91,13 +91,16 @@ def test_shim_source_contract(shim_dir: Path) -> None: message must name the matching distribution. """ provider = shim_dir.name + # Bundle names are lowercase (BUNDLE_NAME_RE), so a mixed-case in-tree dir + # (e.g. FAISS, Notion) aliases to its lowercased bundle (faiss, notion). + slug = provider.lower() src = (shim_dir / "__init__.py").read_text(encoding="utf-8") assert src.startswith(SHIM_MARKER), f"{provider}: first line must be the {SHIM_MARKER!r} marker" assert "sys.modules[__name__] = importlib.import_module(" in src, f"{provider}: must module-alias" - meta_target = f'importlib.import_module("lfx_bundles.{provider}")' - partner_target = f'importlib.import_module("lfx_{provider}.components.{provider}")' + meta_target = f'importlib.import_module("lfx_bundles.{slug}")' + partner_target = f'importlib.import_module("lfx_{slug}.components.{slug}")' is_meta = meta_target in src is_partner = partner_target in src assert is_meta or is_partner, f"{provider}: alias target is neither metapackage nor partner shape" @@ -106,9 +109,9 @@ def test_shim_source_contract(shim_dir: Path) -> None: assert 'exc.name == "lfx_bundles"' in src, f"{provider}: name-check must guard lfx_bundles" assert _METAPACKAGE_MSG in src, f"{provider}: locked install message missing ({_METAPACKAGE_MSG!r})" else: - assert f'exc.name == "lfx_{provider}"' in src, f"{provider}: name-check must guard lfx_{provider}" - assert f"{_PARTNER_MSG_PREFIX}{provider}" in src, ( - f"{provider}: locked install message missing ('pip install lfx-{provider}')" + assert f'exc.name == "lfx_{slug}"' in src, f"{provider}: name-check must guard lfx_{slug}" + assert f"{_PARTNER_MSG_PREFIX}{slug}" in src, ( + f"{provider}: locked install message missing ('pip install lfx-{slug}')" ) # Contract #5: anything other than the bundle-missing case re-raises. assert src.rstrip().endswith("raise"), f"{provider}: must re-raise non-bundle import errors untouched" diff --git a/uv.lock b/uv.lock index c341e18fd0..415cc37aac 100644 --- a/uv.lock +++ b/uv.lock @@ -9150,6 +9150,8 @@ all = [ { name = "couchbase" }, { name = "cuga", marker = "(python_full_version < '3.14' and platform_machine == 'arm64') or (python_full_version < '3.14' and sys_platform != 'darwin')" }, { name = "elasticsearch" }, + { name = "faiss-cpu", version = "1.9.0.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "faiss-cpu", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "firecrawl-py" }, { name = "gassist", marker = "sys_platform == 'win32'" }, { name = "gitpython" }, @@ -9177,6 +9179,7 @@ all = [ { name = "langchain-sambanova" }, { name = "langchain-unstructured" }, { name = "langchain-weaviate" }, + { name = "markdown" }, { name = "mem0ai" }, { name = "metaphor-python" }, { name = "needle-python" }, @@ -9278,6 +9281,11 @@ elastic = [ exa = [ { name = "metaphor-python" }, ] +faiss = [ + { name = "faiss-cpu", version = "1.9.0.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "faiss-cpu", version = "1.14.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "langchain-community" }, +] firecrawl = [ { name = "firecrawl-py" }, ] @@ -9336,6 +9344,10 @@ needle = [ { name = "langchain-community" }, { name = "needle-python" }, ] +notion = [ + { name = "markdown" }, + { name = "requests" }, +] novita = [ { name = "langchain-openai" }, { name = "requests" }, @@ -9457,6 +9469,8 @@ requires-dist = [ { name = "cuga", marker = "python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'cuga'", specifier = ">=0.2.20,<0.3.0" }, { name = "cuga", marker = "python_full_version < '3.14' and sys_platform != 'darwin' and extra == 'cuga'", specifier = ">=0.2.20,<0.3.0" }, { name = "elasticsearch", marker = "extra == 'elastic'", specifier = "~=8.19" }, + { name = "faiss-cpu", marker = "python_full_version >= '3.14' and extra == 'faiss'", specifier = ">=1.13.2" }, + { name = "faiss-cpu", marker = "python_full_version < '3.14' and extra == 'faiss'", specifier = "==1.9.0.post1" }, { name = "firecrawl-py", marker = "extra == 'firecrawl'", specifier = ">=1.0.16,<2.0.0" }, { name = "gassist", marker = "sys_platform == 'win32' and extra == 'nvidia'", specifier = ">=0.0.1" }, { name = "gitpython", marker = "extra == 'git'", specifier = ">=3.1.50" }, @@ -9474,6 +9488,7 @@ requires-dist = [ { name = "langchain-community", marker = "extra == 'cloudflare'", specifier = ">=0.4.1,<1.0.0" }, { name = "langchain-community", marker = "extra == 'confluence'", specifier = ">=0.4.1,<1.0.0" }, { name = "langchain-community", marker = "extra == 'couchbase'", specifier = ">=0.4.1,<1.0.0" }, + { name = "langchain-community", marker = "extra == 'faiss'", specifier = ">=0.4.1,<1.0.0" }, { name = "langchain-community", marker = "extra == 'git'", specifier = ">=0.4.1,<1.0.0" }, { name = "langchain-community", marker = "extra == 'huggingface'", specifier = ">=0.4.1,<1.0.0" }, { name = "langchain-community", marker = "extra == 'maritalk'", specifier = ">=0.4.1,<1.0.0" }, @@ -9537,6 +9552,7 @@ requires-dist = [ { name = "lfx-bundles", extras = ["deepseek"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["elastic"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["exa"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, + { name = "lfx-bundles", extras = ["faiss"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["firecrawl"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["git"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["glean"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, @@ -9555,6 +9571,7 @@ requires-dist = [ { name = "lfx-bundles", extras = ["mistral"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["mongodb"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["needle"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, + { name = "lfx-bundles", extras = ["notion"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["novita"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["nvidia"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["olivya"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, @@ -9586,6 +9603,7 @@ requires-dist = [ { name = "lfx-bundles", extras = ["yahoosearch"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["youtube"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, { name = "lfx-bundles", extras = ["zep"], marker = "extra == 'all'", editable = "src/bundles/lfx-bundles" }, + { name = "markdown", marker = "extra == 'notion'", specifier = ">=3.8.0" }, { name = "mem0ai", marker = "extra == 'mem0'", specifier = ">=2.0.2,<3.0.0" }, { name = "metaphor-python", marker = "extra == 'exa'", specifier = "==0.1.23" }, { name = "needle-python", marker = "extra == 'needle'", specifier = ">=0.4.0" }, @@ -9608,6 +9626,7 @@ requires-dist = [ { name = "requests", marker = "extra == 'deepseek'", specifier = ">=2.32.0" }, { name = "requests", marker = "extra == 'homeassistant'", specifier = ">=2.32.0" }, { name = "requests", marker = "extra == 'icosacomputing'", specifier = ">=2.32.0" }, + { name = "requests", marker = "extra == 'notion'", specifier = ">=2.32.0" }, { name = "requests", marker = "extra == 'novita'", specifier = ">=2.32.0" }, { name = "requests", marker = "extra == 'xai'", specifier = ">=2.32.0" }, { name = "scrapegraph-py", marker = "extra == 'scrapegraph'", specifier = ">=1.12.0" }, @@ -9624,7 +9643,7 @@ requires-dist = [ { name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = ">=1.0.0,<2.0.0" }, { name = "zep-python", marker = "extra == 'zep'", specifier = "==2.0.2" }, ] -provides-extras = ["agentql", "aiml", "altk", "apify", "assemblyai", "azure", "baidu", "bing", "chroma", "cleanlab", "clickhouse", "cloudflare", "codeagents", "cometapi", "composio", "confluence", "couchbase", "cuga", "deepseek", "elastic", "exa", "firecrawl", "git", "glean", "google", "groq", "homeassistant", "huggingface", "icosacomputing", "jigsawstack", "langwatch", "litellm", "lmstudio", "maritalk", "mem0", "milvus", "mistral", "mongodb", "needle", "novita", "nvidia", "olivya", "ollama", "openrouter", "perplexity", "pgvector", "pinecone", "qdrant", "redis", "sambanova", "scrapegraph", "searchapi", "serpapi", "spider", "supabase", "tavily", "toolguard", "twelvelabs", "unstructured", "upstash", "vectara", "vertexai", "vllm", "weaviate", "wikipedia", "wolframalpha", "xai", "yahoosearch", "youtube", "zep", "all"] +provides-extras = ["agentql", "aiml", "altk", "apify", "assemblyai", "azure", "baidu", "bing", "chroma", "cleanlab", "clickhouse", "cloudflare", "codeagents", "cometapi", "composio", "confluence", "couchbase", "cuga", "deepseek", "elastic", "exa", "faiss", "firecrawl", "git", "glean", "google", "groq", "homeassistant", "huggingface", "icosacomputing", "jigsawstack", "langwatch", "litellm", "lmstudio", "maritalk", "mem0", "milvus", "mistral", "mongodb", "needle", "notion", "novita", "nvidia", "olivya", "ollama", "openrouter", "perplexity", "pgvector", "pinecone", "qdrant", "redis", "sambanova", "scrapegraph", "searchapi", "serpapi", "spider", "supabase", "tavily", "toolguard", "twelvelabs", "unstructured", "upstash", "vectara", "vertexai", "vllm", "weaviate", "wikipedia", "wolframalpha", "xai", "yahoosearch", "youtube", "zep", "all"] [[package]] name = "lfx-cohere"