-
Notifications
You must be signed in to change notification settings - Fork 9.9k
feat(bundles): create lfx-bundles metapackage skeleton #13564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
6e77c8f
a7281f1
b3e245c
61a0d94
0b198ab
b1cb7d0
d32a878
e887249
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| """Tests for ``scripts/ci/update_bundle_versions.py``. | ||
|
|
||
| The nightly build calls this script to rename every ``src/bundles/*`` | ||
| package to its ``-nightly`` counterpart and re-point the root | ||
| ``pyproject.toml`` at the renamed distributions. These tests exercise the | ||
| real script module so regressions in the rename/dep regexes are caught | ||
| without running a nightly. The extras-suffix cases exist because the | ||
| ``lfx-bundles`` metapackage is referenced as ``lfx-bundles[all]`` (and | ||
| docling as ``lfx-docling[local]`` etc.) -- a dep regex that cannot see | ||
| through ``[extras]`` leaves the root pointing at the stable distribution | ||
| while the workspace member is renamed, which breaks the nightly resolve. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import importlib.util | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| REPO_ROOT = Path(__file__).resolve().parents[4] | ||
| _SCRIPT = REPO_ROOT / "scripts" / "ci" / "update_bundle_versions.py" | ||
|
|
||
|
|
||
| def _load_module(): | ||
| spec = importlib.util.spec_from_file_location("update_bundle_versions", _SCRIPT) | ||
| module = importlib.util.module_from_spec(spec) | ||
| sys.modules["update_bundle_versions"] = module | ||
| spec.loader.exec_module(module) | ||
| return module | ||
|
|
||
|
|
||
| mod = _load_module() | ||
|
|
||
|
|
||
| _ROOT_PYPROJECT = """\ | ||
| [project] | ||
| name = "langflow" | ||
| version = "1.11.0" | ||
| dependencies = [ | ||
| "lfx-bundles[all]>=1.0,<2.0", | ||
| "lfx-duckduckgo>=0.1.0,<1.0.0", | ||
| ] | ||
|
|
||
| [project.optional-dependencies] | ||
| docling = [ | ||
| "lfx-docling[local]>=0.1.0", | ||
| ] | ||
|
|
||
| [tool.uv.sources] | ||
| lfx-bundles = { workspace = true } | ||
| lfx-duckduckgo = { workspace = true } | ||
| lfx-docling = { workspace = true } | ||
| """ | ||
|
|
||
| _METAPACKAGE_PYPROJECT = """\ | ||
| [project] | ||
| name = "lfx-bundles" | ||
| version = "1.0.0" | ||
| dependencies = [ | ||
| "lfx>=1.11.0,<2.0.0", | ||
| ] | ||
|
|
||
| [project.optional-dependencies] | ||
| aiml = ["openai>=1.68.2,<3.0.0"] | ||
| tavily = [] | ||
| all = [ | ||
| "lfx-bundles[aiml]", | ||
| "lfx-bundles[tavily]", | ||
| ] | ||
|
|
||
| [project.entry-points."lfx.bundles"] | ||
| lfx_bundles = "lfx_bundles" | ||
| """ | ||
|
|
||
|
|
||
| class TestRenameBundlePyproject: | ||
| def test_metapackage_self_ref_extras_follow_the_rename(self, tmp_path): | ||
| pyproject = tmp_path / "pyproject.toml" | ||
| pyproject.write_text(_METAPACKAGE_PYPROJECT, encoding="utf-8") | ||
|
|
||
| renamed = mod.rename_bundle_pyproject(pyproject, "1.11.0.dev38", "38") | ||
|
|
||
| assert renamed == ("lfx-bundles", "lfx-bundles-nightly", "1.0.0.dev38") | ||
| content = pyproject.read_text(encoding="utf-8") | ||
| assert 'name = "lfx-bundles-nightly"' in content | ||
| assert 'version = "1.0.0.dev38"' in content | ||
| assert '"lfx-nightly==1.11.0.dev38"' in content | ||
| assert '"lfx-bundles-nightly[aiml]"' in content | ||
| assert '"lfx-bundles-nightly[tavily]"' in content | ||
| # No stable self-ref left behind to pull the stable dist from PyPI. | ||
| assert '"lfx-bundles[' not in content | ||
|
|
||
| def test_rename_is_idempotent(self, tmp_path): | ||
| pyproject = tmp_path / "pyproject.toml" | ||
| pyproject.write_text(_METAPACKAGE_PYPROJECT, encoding="utf-8") | ||
|
|
||
| first = mod.rename_bundle_pyproject(pyproject, "1.11.0.dev38", "38") | ||
| after_first = pyproject.read_text(encoding="utf-8") | ||
| second = mod.rename_bundle_pyproject(pyproject, "1.11.0.dev38", "38") | ||
|
|
||
| assert first == second | ||
| assert pyproject.read_text(encoding="utf-8") == after_first | ||
|
|
||
|
|
||
| class TestUpdateRootPyprojectForBundle: | ||
| def test_extras_suffixed_main_dep_is_rewritten_with_extras_preserved(self, tmp_path): | ||
| root = tmp_path / "pyproject.toml" | ||
| root.write_text(_ROOT_PYPROJECT, encoding="utf-8") | ||
|
|
||
| mod.update_root_pyproject_for_bundle(root, "lfx-bundles", "lfx-bundles-nightly", "1.0.0.dev38") | ||
|
|
||
| content = root.read_text(encoding="utf-8") | ||
| assert '"lfx-bundles-nightly[all]==1.0.0.dev38"' in content | ||
| assert '"lfx-bundles[all]' not in content | ||
| assert "lfx-bundles-nightly = { workspace = true }" in content | ||
|
|
||
| def test_extras_suffixed_optional_dep_is_rewritten(self, tmp_path): | ||
| root = tmp_path / "pyproject.toml" | ||
| root.write_text(_ROOT_PYPROJECT, encoding="utf-8") | ||
|
|
||
| mod.update_root_pyproject_for_bundle(root, "lfx-docling", "lfx-docling-nightly", "0.1.5.dev38") | ||
|
|
||
| content = root.read_text(encoding="utf-8") | ||
| assert '"lfx-docling-nightly[local]==0.1.5.dev38"' in content | ||
| assert '"lfx-docling[local]' not in content | ||
|
|
||
| def test_plain_dep_keeps_working(self, tmp_path): | ||
| root = tmp_path / "pyproject.toml" | ||
| root.write_text(_ROOT_PYPROJECT, encoding="utf-8") | ||
|
|
||
| mod.update_root_pyproject_for_bundle(root, "lfx-duckduckgo", "lfx-duckduckgo-nightly", "0.1.2.dev38") | ||
|
|
||
| content = root.read_text(encoding="utf-8") | ||
| assert '"lfx-duckduckgo-nightly==0.1.2.dev38"' in content | ||
| assert "lfx-duckduckgo-nightly = { workspace = true }" in content | ||
|
|
||
| def test_root_update_is_idempotent(self, tmp_path): | ||
| root = tmp_path / "pyproject.toml" | ||
| root.write_text(_ROOT_PYPROJECT, encoding="utf-8") | ||
|
|
||
| mod.update_root_pyproject_for_bundle(root, "lfx-bundles", "lfx-bundles-nightly", "1.0.0.dev38") | ||
| after_first = root.read_text(encoding="utf-8") | ||
| mod.update_root_pyproject_for_bundle(root, "lfx-bundles", "lfx-bundles-nightly", "1.0.0.dev38") | ||
|
|
||
| assert root.read_text(encoding="utf-8") == after_first |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # lfx-bundles | ||
|
|
||
| The long tail of Langflow's provider components as a **single manifest-less | ||
| metapackage**, modeled on `langchain-community`. This is the destination for | ||
| every vendor/third-party provider that does not warrant its own standalone | ||
| distribution; the curated partner providers (OpenAI, Anthropic, AWS, | ||
| DataStax, Cohere) ship as separate `lfx-<provider>` packages instead. | ||
|
|
||
| ## How it works | ||
|
|
||
| `lfx-bundles` declares the `lfx.bundles` entry point: | ||
|
|
||
| ```toml | ||
| [project.entry-points."lfx.bundles"] | ||
| lfx_bundles = "lfx_bundles" | ||
| ``` | ||
|
|
||
| At startup, lfx resolves this package and **folder-walks its immediate | ||
| subdirectories**. Each subdirectory is one bundle, registered at the | ||
| `@official` slot under its directory name — no `extension.json`, no per-provider | ||
| manifest. Adding a provider is just adding a folder. | ||
|
|
||
| ``` | ||
| src/lfx_bundles/ | ||
| ├── __init__.py # bare namespace marker | ||
| ├── <provider>/ # one bundle, e.g. tavily/, pinecone/, ... | ||
| │ └── *.py # Component subclasses | ||
| └── ... | ||
| ``` | ||
|
|
||
| A component's identity is its **bundle name** (`ext:<provider>:<Class>@official`), | ||
| which is stable whether the provider ships here or graduates to a standalone | ||
| `lfx-<provider>` package. Because a manifest-shipping package always shadows the | ||
| manifest-less metapackage, a provider can graduate with **no lockstep release**. | ||
|
|
||
| ## Installing | ||
|
|
||
| ```bash | ||
| pip install langflow # everything (langflow pins lfx-bundles[all]) | ||
| pip install lfx # engine only, no bundles | ||
| pip install "lfx[bundles]" # engine + this metapackage (deployment footnote) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neither of these commands works yet: lfx has no
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right — fixed in e887249. The install section now separates what works today ( |
||
| pip install "lfx-bundles[tavily]" # engine + one provider's deps | ||
| ``` | ||
|
|
||
| `lfx-bundles` itself depends only on `lfx`. Each provider's third-party SDKs are | ||
| **optional extras** (PEP 685-normalized keys, e.g. `lfx-bundles[google-genai]`); | ||
| the generated `all` extra pulls every provider's deps and is what `langflow` | ||
| depends on so `pip install langflow` is unchanged. | ||
|
|
||
| ## Adding a provider | ||
|
|
||
| Providers are moved here by `scripts/migrate/consolidate_bundles.py`, which also | ||
| maintains the per-provider extras and the generated `all` aggregate. **Do not** | ||
| hand-edit the extras block in `pyproject.toml`. Provider folder names must be | ||
| lowercase snake_case (`a-z`, `0-9`, `_`, 2–64 chars). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| [project] | ||
| name = "lfx-bundles" | ||
| version = "1.0.0" | ||
| description = "Langflow's long-tail provider bundles as a single manifest-less metapackage (the langchain-community model)." | ||
| readme = "README.md" | ||
| requires-python = ">=3.10,<3.15" | ||
| license = { text = "MIT" } | ||
| authors = [ | ||
| { name = "Langflow", email = "contact@langflow.org" }, | ||
| ] | ||
| keywords = ["langflow", "lfx", "extension", "bundle", "providers"] | ||
|
|
||
| # Runtime: only lfx (the BUNDLE_API surface). Each provider's third-party SDK | ||
| # is an optional extra (see [project.optional-dependencies]); installing | ||
| # lfx-bundles bare gives the provider *code* but defers each provider's SDK to | ||
| # its extra, so a user opts into exactly the providers they need. The | ||
| # generated ``all`` extra pulls every provider's deps and is what ``langflow`` | ||
| # depends on (``lfx-bundles[all]``) so ``pip install langflow`` stays | ||
| # functionally identical to today. | ||
| dependencies = [ | ||
| "lfx>=1.11.0.dev0,<2.0.0", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR body says this declares an
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated the PR body — it now states the actual |
||
| ] | ||
|
|
||
| [project.optional-dependencies] | ||
| # Per-provider extras + the ``all`` aggregate are populated by the bulk move | ||
| # (scripts/migrate/consolidate_bundles.py) as the long-tail providers land | ||
| # here. Extra keys are PEP 685-normalized (lowercase, hyphen-separated). | ||
| # ``all`` is GENERATED from the per-provider keys -- never hand-edit it. | ||
| # Empty until the first provider tranche moves in. | ||
| all = [] | ||
|
|
||
| [project.urls] | ||
| Homepage = "https://github.com/langflow-ai/langflow" | ||
| Documentation = "https://docs.langflow.org/extensions" | ||
| Repository = "https://github.com/langflow-ai/langflow" | ||
|
|
||
| # Manifest-less discovery via the ``lfx.bundles`` entry-point group (NOT | ||
| # ``langflow.extensions``). The loader (lfx.extension.loader._bundles_root) | ||
| # resolves this package with find_spec and folder-walks its immediate | ||
| # subdirectories -- each is one bundle at the @official slot, named after the | ||
| # directory. No extension.json; exempt from ``lfx extension validate``. | ||
| [project.entry-points."lfx.bundles"] | ||
| lfx_bundles = "lfx_bundles" | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| packages = ["src/lfx_bundles"] | ||
|
|
||
| [tool.hatch.build.targets.sdist] | ||
| include = [ | ||
| "src/lfx_bundles", | ||
| "README.md", | ||
| "pyproject.toml", | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| """lfx-bundles: the manifest-less metapackage of Langflow's long-tail providers. | ||
|
|
||
| This package is a bare namespace marker. Each immediate subdirectory is one | ||
| provider bundle, discovered at runtime by lfx's ``lfx.bundles`` entry-point | ||
| folder-walk (``lfx.extension.loader._bundles_root``) and registered at the | ||
| ``@official`` slot under its directory name. There are intentionally no | ||
| re-exports here and no ``extension.json`` -- providers are added as folders, | ||
| the langchain-community way. | ||
|
|
||
| Provider folders are lowercase snake_case (``BUNDLE_NAME_RE``); a component's | ||
| identity is its bundle name (``ext:<provider>:<Class>@official``), stable | ||
| whether the provider ships here or in a graduated ``lfx-<provider>`` package. | ||
|
|
||
| Providers are added by ``scripts/migrate/consolidate_bundles.py``, never by | ||
| hand. | ||
| """ |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this script still have a caller? #13206 dropped the
update_bundle_versions.pyinvocation from nightly_build.yml, NIGHTLY.md says bundles keep their stable names during nightly bumps, and grep finds no workflow or Makefile target invoking it now. The new test file's docstring also says "The nightly build calls this script", which isn't true anymore. If the rename track is coming back for the metapackage this needs the workflow wiring, otherwise I think the script and the new tests should go rather than grow.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — confirmed: no workflow or Makefile target on main or any release branch invokes it (the only remaining hits were doc comments). The rename track isn't coming back (Approach A / NIGHTLY.md retired it), so I deleted the script and the new tests in e887249, and updated the stale references in
sync_bundle_lfx_pin.py/test_bundle_lfx_pin.pythat pointed at it.