Langflow 扩展 Bundle 移植实战:把组件从lfx.components提取到src/bundles的完整流程
【免费下载链接】langflowLangflow is a powerful tool for building and deploying AI-powered agents and workflows.项目地址: https://gitcode.com/GitHub_Trending/la/langflow
本文讲解 Langflow/LFX 仓库中把某个 provider 组件从树内目录src/lfx/src/lfx/components/<provider>/提取为独立分发的 Extension Bundle(src/bundles/<provider>/)的完整流程:目录骨架、pyproject.toml与extension.json的写法、工作区接线、迁移表条目、组件索引重建、集成测试、六步验证命令与 Docker 镜像适配,并给出port_bundle.py自动化工具与常见坑位。读完本文,你可以独立完成一次不破坏已保存 Flow 的 bundle 移植,并用仓库内置脚本完成版本发布计划。参考实现是 DuckDuckGo bundle:src/bundles/duckduckgo,每一步改动都有对应的验证命令。
0. 移植前的候选检查
动手前先确认组件适合被提取,四个检查项:
- provider 目录
src/lfx/src/lfx/components/<provider>/存在,且包含至少一个Component子类; - 组件只从
lfx.*导入,不出现from langflow...——bundle 是对着公共BUNDLE_API面(lfx)安装的,不是对着 Langflow 内部实现。可用grep -r "from langflow" src/lfx/src/lfx/components/<provider>/自查; src/lfx/src/lfx/components/deactivated/<provider>/下没有已停用/遗留的重复副本;- 组件引入的运行时依赖(如
langchain-community、厂商 SDK)能在 bundle 自己的pyproject.toml里声明清楚,不形成对lfx或langflow-base的循环依赖。
确定两个标识符,它们是整个移植过程中唯一会反复出现的字符串:
- bundle 名:snake_case 小写,与目录名一致,如
duckduckgo、arxiv; - 分发名(distribution name):
lfx-<bundle>,如lfx-duckduckgo。
两者只差一个字符(-对_),不要混用。
1. 搭建 bundle 目录
创建src/bundles/<bundle>/,目录树完全对齐参考实现 src/bundles/duckduckgo:
src/bundles/<bundle>/ ├── README.md ├── pyproject.toml └── src/ └── lfx_<bundle>/ ├── __init__.py ├── extension.json └── components/ └── <bundle>/ ├── __init__.py └── <source>.py # 一个组件类一个文件为什么是嵌套的src/lfx_<bundle>/components/<bundle>/?外层lfx_<bundle>是可导入的 Python 包(与 wheel 布局一致,是importlib.metadata.files()遍历的对象);内层components/<bundle>/是extension.json中bundles[].path声明的路径——保持components/<bundle>这一形状,意味着历史上引用过lfx.components.<bundle>.<file>.<Class>的已保存 Flow 只需迁移表里一条import-path 条目就能干净地完成重定向。
1a.pyproject.toml
以 src/bundles/duckduckgo/pyproject.toml 为模板替换名称和运行时依赖块。几个不直观的要点:
dependencies:列出组件 import 的全部运行时依赖。lfx的下限锚定在当前 Langflow/LFX 的major.minor线,上限卡在下一个lfxmajor 之前——例如"lfx>=1.10.0,<2.0.0"。这条通常不需要手写:port_bundle.py 在移植时从src/lfx/pyproject.toml读取当前版本填入(当前仓库的 duckduckgo 是lfx>=1.12.0.dev0,<2.0.0);后续每次发版由make patch通过 sync_bundle_lfx_pin.py 重新同步所有既有 bundle。细粒度的 BUNDLE_API 兼容性则由extension.json的"lfx": {"compat": [...]}契约在加载期对运行中 lfx 的BUNDLE_API_VERSION强制,而不是靠版本上限。.dev0下限是有意的:release 分支的 nightly 是规范化的X.Y.0.devN预发布版本,PEP 440 下它排序在X.Y.0之前,只有X.Y.0.dev0这类下限才能同时接纳分支自己的 nightly。- 平台受限依赖:若某运行时依赖在部分平台没有 wheel(如
ibm-db不提供 linux/aarch64),用 PEP 508 marker 门控,保证pip install langflow在那些平台上依然成功,例如"ibm-db>=3.2.9,<4.0.0; sys_platform != 'linux' or platform_machine != 'aarch64'"。同时该依赖必须懒加载(写在使用它的方法内部,而不是模块顶部),让 bundle 在被排除的平台上仍能加载,组件优雅降级而不是拖垮整个组件发现流程。跨平台安装测试通过 langflow 主安装把关硬依赖 bundle;如果bundle 本体(而非仅传递依赖)在某平台装不上,还要在根 pyproject.toml 里该依赖行加同样的 marker,避免 langflow 主包在那儿强制要求它。 [project.entry-points."langflow.extensions"]:写<dist-name> = "lfx_<bundle>"。加载器 src/lfx/src/lfx/extension/loader/_plugins.py 中的_manifest_via_entry_point靠它找到 manifest;当 editable 安装的dist.files只暴露 dist-info 条目时,就走这个 entry point 兜底(它用importlib.util.find_spec定位包目录,不会触发 bundle__init__的副作用)。[tool.hatch.build.targets.wheel]必须包含src/lfx_<bundle>/extension.json和 components 的 glob——wheel 安装通过dist.files读 manifest,文件没打进 wheel,bundle 就会被静默跳过。duckduckgo 的写法是:
[tool.hatch.build.targets.wheel] packages = ["src/lfx_duckduckgo"] include = ["src/lfx_duckduckgo/extension.json", "src/lfx_duckduckgo/components/**/*.py"]1b.src/lfx_<bundle>/extension.json
{ "$schema": "https://schemas.langflow.org/extension/v1.json", "id": "lfx-<bundle>", "version": "0.1.0", "name": "<Human-readable bundle name>", "description": "<One-line description>.", "lfx": { "compat": ["1"] }, "bundles": [ { "name": "<bundle>", "path": "components/<bundle>" } ] }对照仓库中的真实文件 src/bundles/duckduckgo/src/lfx_duckduckgo/extension.json:"id": "lfx-duckduckgo"、"bundles": [{"name": "duckduckgo", "path": "components/duckduckgo"}]、"lfx": {"compat": ["1"]}。id是带连字符的分发名;bundles[0].name是用于已保存 Flow ID 的 snake_case bundle 名(ext:<bundle>:<Class>@official)。
1c–1d. 两层__init__.py
包根init.py 负责从包根重新导出组件类,使lfx_<bundle>.<Class>可解析——迁移表的bare_class_name条目依赖这个 import 成立:
"""lfx-<bundle>: <description>.""" from lfx_<bundle>.components.<bundle>.<source> import <Class> __all__ = ["<Class>"]components/<bundle>/__init__.py则:
from .<source> import <Class> __all__ = ["<Class>"]1e. 移动组件源码
src/lfx_<bundle>/components/<bundle>/<source>.py就是被移动的文件:从src/lfx/src/lfx/components/<bundle>/<source>.py逐字节复制,不要改写 import。组件里的from lfx.*import 原样可用,因为lfx是 bundle 的运行时依赖。
1f.README.md
简短说明 bundle 提供什么、如何安装、如何开发,模板用 duckduckgo/README.md。
2. 删除树内组件
整个遗留目录删掉:
git rm -r src/lfx/src/lfx/components/<bundle>/然后外科手术式地删除 src/lfx/src/lfx/components/init.py 中的三处引用:
- import 块里的
<bundle>,行(约第 10 行); - 类型映射字典里的
"<bundle>": "__module__",条目; __all__风格列表里的"<bundle>",字符串。
自检:改完后
grep -n "<bundle>" src/lfx/src/lfx/components/__init__.py应无任何输出。
3. 接线工作区
3a. 根pyproject.toml
三处机械性修改(当前仓库已用# langflow-extensions:bundle-deps-start/end、bundle-sources-end、bundle-members-end标记对圈出锚点,port_bundle.py 就是按这些标记插入的,可抗依赖重排/版本升级):
# 1. 加入 [project] dependencies —— 普通依赖,保证 `pip install langflow` # 仍然拉到该组件,用户侧安装体验零变化。 dependencies = [ "langflow-base~=1.12.0", "lfx-duckduckgo>=0.1.0,<1.0.0", "lfx-<bundle>>=0.1.0", # <-- 新增 ] # 2. 加入 [tool.uv.sources] lfx-<bundle> = { workspace = true } # 3. 加入 [tool.uv.workspace] members members = [ "src/backend/base", ".", "src/lfx", "src/sdk", "src/bundles/duckduckgo", "src/bundles/<bundle>", # <-- 新增 ]3b.src/backend/base/pyproject.toml(可选)
仅当组件原有langflow-base[<bundle>]extra 时才动它:删掉该 extra,并把complete里的langflow-base[<bundle>]引用一并移除。duckduckgo 移植时做了这一步;若组件没有 extra(如 arxiv),整节跳过。
3c. 锁文件
uv lock git add uv.lock4. 追加迁移表条目
向 src/lfx/src/lfx/extension/migration/migration_table.json 追加条目。schema 要求四类遗留形态,覆盖已保存 Flow 可能出现过的所有写法:
{ "bare_class_name": "<Class>", "target": "ext:<bundle>:<Class>@official", "added_in": "<release>" }, { "import_path": "lfx.components.<bundle>.<source>.<Class>", "target": "ext:<bundle>:<Class>@official", "added_in": "<release>" }, { "import_path": "lfx.components.<bundle>.<Class>", "target": "ext:<bundle>:<Class>@official", "added_in": "<release>" }, { "legacy_slot": "ext:<bundle>:<Class>@official-pre-a", "target": "ext:<bundle>:<Class>@official", "added_in": "<release>" }对照仓库中 duckduckgo 的真实条目("added_in": "1.10.0"),可以看到四种形态一一对应:裸类名DuckDuckGoSearchComponent、文件级完整路径lfx.components.duckduckgo.duck_duck_go_search_run.DuckDuckGoSearchComponent、包级 re-export 路径lfx.components.duckduckgo.DuckDuckGoSearchComponent、以及 Phase-A 前的 slot ID。组件若声明了多个类,每个类重复这四条目块;其中bare_class_name条目仅当类名在当次发布的所有 Bundle 中全局唯一时才加入——这一点由 scripts/migrate/check_bare_names.py 在 CI 中强制(它用标准库 AST 遍历树内组件目录与已提取 bundle,断言每个裸名条目只映射到一个文件夹里的一个类)。
迁移表是只追加的:永远不要删除或改写既有条目——CI 会拒绝删除操作,否则多年前针对早已提取 bundle 保存的 Flow 将无法加载。
5. 重建组件索引
预构建的组件索引驱动懒加载,被移动组件的旧条目必须移除:
LFX_DEV=1 uv run python scripts/build_component_index.pyLFX_DEV=1强制走pkgutil.walk_packages的动态发现;不带它,脚本会读现有索引,即使源模块已经删掉也照样把过期条目再生产一遍。diff 应当只删除<bundle>块;若还动了别的,说明本地 checkout 有无关漂移。
6. 添加集成测试
创建src/lfx/tests/integration/extension/test_pilot_<bundle>_upgrade.py,以 test_pilot_duckduckgo_upgrade.py 为范本。四个关键用例:
- 裸类名 → 规范 ID(
migrate_flow_payload重写后rewritten_count == 1,且legacy_form_kind == "bare_class_name"); - 完整 import 路径 → 规范 ID;
- 包级 import 路径 → 规范 ID;
lfx-<bundle>分发包可导入,且extension.json位于importlib.metadata.files能发现的位置(editable 安装则验证direct_url.json可解析到源码树中的 manifest)。
duckduckgo 的 pilot 测试还多做了一层:用load_extension把迁移目标解析成运行时类,断言加载类与 bundle 导出类同源(loader 把 bundle 模块挂在_lfx_ext.<slot>.<bundle>命名空间下,所以对象恒等断言不成立,仓库锁定的是"同一源文件、同一限定名"这一已保存 Flow 真正依赖的不变量),再用一个 stub 包装器跑fetch_content_dataframe验证content/snippet列、max_results切片与max_snippet_length截断等运行时契约。集成测试是已保存 Flow 契约端到端被演练的唯一位置,不要跳过。
随移植一起迁移的测试覆盖
树内src/backend/tests/unit/components/<bundle>/test_<bundle>_component.py通常含test_component_versions用例,遍历file_names_mapping夹具验证旧 schema 版本的保存夹具仍能实例化。该夹具 import 自tests.base,在 bundle 内部不可导入。新的 bundle 本地测试(src/bundles/<bundle>/tests/)会丢掉它,而test_pilot_<bundle>_upgrade.py只覆盖命名空间迁移、不覆盖类内 schema 演化。若遗留夹具里有非空条目,用 bundle 友好的形式(对同一 mapping 参数化 bundle 测试,不 importtests.base)复刻该版本检查;若夹具是空的,就在 PR 描述里说明该回归,让评审人决定是否在合并前复刻。
7. 验证
按顺序运行"一旦某步出错就会大声失败"的最小命令集:
# 1. manifest 结构合法。validate 指向包目录(extension.json 所在处) # 而非 bundle 根 —— manifest 嵌在 src/lfx_<bundle>/ 里,wheel 才装得上。 # 验证器同时接受 ``def build(self): ...`` 与 ``outputs = [Output(method="...")]`` # 两种形态;两种都没有的组件会以 ``build-method-missing`` 失败, # 届时补一个 ``outputs`` 声明。 uv run lfx extension validate src/bundles/<bundle>/src/lfx_<bundle> # 2. 工作区可解析、bundle 可导入。 uv sync uv run python -c "from lfx_<bundle> import <Class>; print(<Class>.__name__)" # 3. 迁移表可解析、新条目可见。 uv run pytest src/lfx/tests/unit/extension/migration -q # 4. 加载器经 direct_url.json 发现 editable 安装。 uv run python -c " from lfx.extension.loader._plugins import installed_extension_roots roots = installed_extension_roots() assert 'lfx-<bundle>' in roots, roots print('discovered:', roots['lfx-<bundle']) "(上面第 4 条引号按原意应为roots['lfx-<bundle>']。)
# 5. 集成测试通过。 uv run pytest src/lfx/tests/integration/extension/test_pilot_<bundle>_upgrade.py -q # 6. Ruff 对触碰到的 Python 文件干净。不要把迁移表 JSON 传给 ruff # —— 它会当 Python lint,抱怨顶层表达式。 uv run ruff check src/bundles/<bundle> src/lfx/src/lfx/components/__init__.py src/lfx/tests/integration/extension/test_pilot_<bundle>_upgrade.py端到端冒烟测试(可选但廉价):带 bundle 起 dev server,在画布上点 Reload:
uv run lfx extension dev src/bundles/<bundle> # 在浏览器打开 http://localhost:7860: # - 确认 <Class> 出现在 <bundle> bundle 分组下; # - 右键 <bundle> 标题 -> Reload,无报错。发布计划与版本变更
src/bundles/<bundle>/src/下任何可发布的变更,或对 bundlepyproject.toml的修改,都要求分发版本号递增。先用 plan 命令生成与 CI 审查相同的计划(不改文件):
python scripts/ci/bundle_release_plan.py plan \ --base-ref origin/release-1.11.0 \ --check \ --output bundle-release-plan.json应用计划时改用 update 命令,不要手改版本字段。它会默认把受影响的 bundle 各升一个 patch,同步其extension.json版本与 LFX 依赖区间,抬升所有匹配的 Langflow 依赖下限,并作为一次可整体回滚的操作重新生成uv.lock:
python scripts/ci/bundle_release_plan.py update \ --base-ref origin/release-1.11.0 \ --output bundle-release-plan.jsonpatch 不够时可用--bump minor或显式--version lfx-<bundle>=X.Y.Z。发布工作流会上传版本/构件计划供审查,并拒绝复用已存在的 PyPI 版本号——除非其规范化 wheel 内容与本次构建的 wheel 完全一致。所有 bundle 固定同一精确版本的 Hatchling 构建后端,保证源不变时能复现不可变的已发布 wheel 元数据;该 pin 只应在一次明确的、全仓范围的发布迁移中更新。
8. Docker 镜像(仅当新 bundle 要进运行时镜像时)
共享的 docker/build_and_push.Dockerfile 把src/bundles整体拷进构建上下文(COPY ./src/bundles /app/src/bundles);被加入根依赖的精选 bundle 由fulltarget 的 workspace sync 自动拾取。basetarget 有意不装任何 provider 扩展。
- docker/build_and_push_backend.Dockerfile:把
./src/bundles/<bundle>加进显式的uv pip install行。
不要把 provider 扩展加进 base target。验证 full 镜像能发现该 bundle,且 base 镜像的分发清单保持不变。
常见坑位
- 组件 import 了
from langflow...:bundle 是装在lfx上,不是langflow上。要么把 import 改写成公共BUNDLE_API面,要么让组件留在树内。 extension.json没进 wheel:dist.files看不到它,非 editable 安装会静默跳过 bundle。确认[tool.hatch.build.targets.wheel]的 include glob 覆盖到了。- bundle 名里带连字符:只有分发名用连字符(
lfx-duckduckgo),bundle 名是 snake_case(duckduckgo);schema 会拒绝bundles[].name里的连字符。 - 忘了
langflow.extensionsentry point:editable 安装会静默发现失败——installed_extension_roots()返回空字典,bundle 永远进不了注册表。 - 迁移条目缺失:已保存 Flow 仍能通过校验,但画布渲染不出遗留节点,用户看到的是 "component not found" 提示。第 4 步的四条目块覆盖了 Langflow 历史上序列化过的所有形态。
自动化:port_bundle.py
机械步骤可以交给 scripts/migrate/port_bundle.py:它生成 bundle 骨架、移动树内目录(含lfx.base.<bundle>共享基座与嵌套子包)、剥离components/__init__.py三处引用、按langflow-extensions:bundle-*标记修补根pyproject.toml、迁移 ruff per-file-ignores、移动后端测试目录。带--migration-release时它还会直接追加迁移表四条目块、生成 pilot 集成测试骨架、删除组件索引里的该 bundle 分类并重算内嵌 SHA256,必要时修补 backend Dockerfile。它不会替你编辑需要人工判断的部分——发布版本号、类名全局唯一性检查(那是 check_bare_names.py 的职责)。脚本默认 dry-run,先打印计划再--apply落盘:
# Dry run 打印计划,评审通过后 --apply 再落盘; # 完整形态可加 --rewrite-consumers --update-index --update-dockerfiles # --remove-base-extra 等开关,详见脚本 docstring。 uv run python scripts/migrate/port_bundle.py --bundle arxiv --apply--rewrite-consumers会 grep 全仓外部消费者,按"先具体后兜底"的顺序做规范化替换(from lfx.components.<bundle> import X→from lfx_<bundle> import X、lfx.components.<bundle>.→lfx_<bundle>.components.<bundle>.、lfx.base.<bundle>→lfx_<bundle>.base),并刻意排除迁移表 JSON、组件索引、pyproject 与保存 Flow 的 JSON——那些字符串是迁移表要在 Flow 加载时改写的数据,机械替换会破坏"冻结的历史快照仍能加载"这一测试目标。
跑完脚本后,照本文第 7 节的验证块逐项执行;任何一步失败,脚本产生的 diff 就是唯一需要评审的工件。
【免费下载链接】langflowLangflow is a powerful tool for building and deploying AI-powered agents and workflows.项目地址: https://gitcode.com/GitHub_Trending/la/langflow
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考