diff --git a/.agents/skills/edgeflow-node-batch-developer/SKILL.md b/.agents/skills/edgeflow-node-batch-developer/SKILL.md deleted file mode 100644 index 4f1e4053..00000000 --- a/.agents/skills/edgeflow-node-batch-developer/SKILL.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: edgeflow-node-batch-developer -description: 用 MakeBatchSpec 新增或修改 LLM-EdgeFlow 多输入输出、拆分聚合排名、可配置 LLM、Embedding/ASR/OCR/Rerank 或多模型 Node。覆盖来源关系、参数、Control 和缓存;单项纯变换或固定文本 LLM 优先对应轻量 skill。 ---- - -# 通用 Batch 与模型能力 Node - -使用现有 `MakeBatchSpec` 和普通 Run 函数,不新增 Node 生命周期体系。 -先读 [Node 共享契约](../llm-edgeflow-developer-guide/references/capability-nodes.md), -按需查 [batch 与参数概念](../../../doc/dev_guide/custom_node_concepts.md)。开发流程遵循 -[CONTRIBUTING](../../../CONTRIBUTING.md)。算法归属决定 common/custom 目录;执行方式相同。 - -## 声明数据关系,再写算法 - -1. 用 `InputsOf` 声明 typed 输入。`Required` 必须存在,`Optional` 可不连接, - `OptionalValue` 才允许连接后请求中仍缺值。选择符合算法的模型能力和输出关系。 -2. 保序结果使用带 anchor 的 `PreservedOutput`;多输出用 `OutputsOf` / `Produced`, - 派生批次用 `ProducedBatch` 与准确的 `PortFlow`。拆分、过滤、排名的来源正确性需算法 - 与测试保证,不能只写一个 flow 字符串。全部结果在局部成功后返回,由框架发布。 -3. `Parameters` / `Field` 声明参数;跨字段与连线规则用 `Validate` / `ValidateBindings`。 - 复杂 JSON 使用 `NodeConfigParser`,不要重复默认值/字段校验。 -4. `ModelsOf` / `Model` 声明能力槽;成员使用 `LlmCall`、`EmbeddingCall`、`AsrCall`、 - `OcrCall` 或 `RerankCall`。配置必须显式引用 model_id,模型文件加载归 Model/Backend。 - 保留门面返回的 `NodeResult` 失败,不另行映射框架错误码。 - -## 按当前实现选模板 - -| 实际需求 | 只读对应例子 | -| --- | --- | -| 多输入、参数、条件生成 | [starter_batch_node](../../../dev_support/node_authoring/starter_batch_node.cpp) | -| 多模型能力 | [starter_multi_model_node](../../../dev_support/node_authoring/starter_multi_model_node.cpp) | -| 1:1 的 Embedding/ASR/OCR/Rerank 起点 | `tools/scaffold_custom_node.py --kind model -m `;检查生成端口是否符合真实算法 | -| 拆分且分配子编号/输出 counts | [TextChunkNode](../../../src/common_nodes/text_chunk_node.cpp) 的 `SplitPayloads` | -| 排名与候选来源 | [TextRerankNode](../../../src/common_nodes/text_rerank_node.cpp) | -| 可配置 LLM 参数与请求上下文 | [PromptGuidedLlmNode](../../../src/custom_nodes/prompt_guided_llm_node.cpp),共用 [生成参数 helper](../../../include/nodes/generate_options_config.h) | -| 两批关联/分组/部分调用后回填 | `dev_support/node_authoring/starter_batch_{join,group,select_scatter}_node.cpp` | - -通用 Batch 没有单独的 `--kind batch` 生成器;从上述已编译例子提取所需 Spec,不猜命令。 -多路数据按完整 key 或明确的 request 分组关系关联,不能默认按数组下标配对。 -借用视图只在本次同步调用中使用;跨生命周期的数据必须拥有存储。 - -只在有需求时加入 Control 或缓存:`WithControls` 只能更新 `Field` 已绑定的参数, -例如需动态更新 temperature/max_tokens 时直接用 typed Fields。仅由 `WithParser` 声明的 -字段不能直接加入字段 Control,添加 `Prepare` 也不会使其成为 typed binding。 -若 typed Fields 与 parser 同时存在,字段 Control 还要求显式 `Prepare` 重建派生状态。 -复杂 `WithControl` 则显式复用规范化/语义校验与状态构建,返回完整有效候选;框架不会再跑 -初始化的 `Prepare`。wire schema 和示例见 [Control 指南](../../../doc/dev_guide/first_control.md)。 -缓存使用 `SessionResources::GetOrCreateResult`;key 显式纳入语义参数、输入和模型 revision。 -框架负责快照/并发更新及 single-flight,不代替算法决定缓存身份。 - -## 登记与验收 - -以 `REGISTER_FUNCTION_NODE` 登记 Spec,源码放在 `src/common_nodes/` 或 -`src/custom_nodes/`,自动编入 `edgeflow_capability_nodes_objects`。common 显式设置 category;领域算法不为进入 common 而泛化。 -优先扩展现有测试文件/套件。`tests/unit/nodes/test_*.cpp` 由 -`tests/RuntimeTests.cmake` 自动收集到节点 runner;新测试套件还需纳入 CTest filter。 -custom 脚手架用 `--write-test` 生成自动编入的测试,沿用 `CustomNodeCatalogTest` -的现有过滤器。 -只有确需并行且可证明线程安全时设置 `.ParallelSafe(true)`,再验证整个计划的并发限制。 - -聚焦测试至少执行真实 Run,检查不同请求、非零子编号、空/缺失输入、数量变化、来源关系、 -模型失败不发布结果及算法特有错误;Control/cache 改动按涉及行为补回滚/并发测试。 -构建 `edgeflow_test_nodes_runner` 和 `alg_pipeline_tool`,运行实际测试过滤器,检查新 -`describe-node`,用 [pipeline-composer](../pipeline-composer/SKILL.md) 接回方案并执行。 -最终证据与门禁见 [Verification](../llm-edgeflow-developer-guide/references/verification.md)。 diff --git a/.agents/skills/edgeflow-node-developer/SKILL.md b/.agents/skills/edgeflow-node-developer/SKILL.md new file mode 100644 index 00000000..e1402555 --- /dev/null +++ b/.agents/skills/edgeflow-node-developer/SKILL.md @@ -0,0 +1,75 @@ +--- +name: edgeflow-node-developer +description: 新增或修改 LLM-EdgeFlow 的 common/custom Node。所有 Node 使用同一结构(Inputs/Params/Models/Run/Spec + MakeNodeSpec),覆盖逐项变换、文本 LLM、多输入输出、拆分聚合排名、Embedding/ASR/OCR/Rerank、多模型、参数、Control 与缓存。 +--- + +# Node 开发 + +先从目标 Catalog 确认现有节点或配置不能完成需求;只改提示词或参数时用 +[pipeline-composer](../pipeline-composer/SKILL.md)。确需 C++ 时读 +[Node 共享契约](../llm-edgeflow-developer-guide/references/capability-nodes.md),按需查 +[概念与签名速查](../../../doc/dev_guide/custom_node_concepts.md)。开发流程遵循 +[CONTRIBUTING](../../../CONTRIBUTING.md)。中性通用操作放 `src/common_nodes/`,领域算法按操作放 +`src/custom_nodes/`;两者写法相同。 + +## 统一结构 + +每个 Node 是一份 `.cpp`:`Inputs`(输入)、可选的 `Params`(配置)与 `Models`(模型)、 +`Run`(全部处理逻辑)、`Spec`(把前三者登记给框架)、`REGISTER_FUNCTION_NODE`。 +多输出时结果结构叫 `Outputs`。`Run` 的参数依次为 `const Inputs&`、`const Params&`、 +`const Models&`,只写 Spec 实际声明的部分;用到会话缓存时再追加 `const SessionResources&`。 +改处理逻辑只动 `Run`;增减输入、配置项、模型或改变数量关系时,同时改结构体和 Spec。 +不要继承 `NodeBase`、手工操作 Context,也不要为某个业务往通用 Node 加分支。 + +## 从模板开始 + +| 实际需求 | 起点 | +| --- | --- | +| 逐项文本变换,数量与来源不变 | `tools/scaffold_custom_node.py --kind compute --write-test`;`Run` 用 `MapPayloads` 调用逐项函数 | +| 文本前处理 → 一次 LLM → 文本后处理 | `--kind model -m llm`([starter_llm_node](../../../dev_support/node_authoring/starter_llm_node.cpp));生成参数由 `GenerateParameters` 从配置读取 | +| 同时生成可运行方案 | [text-llm-node Recipe](../../../doc/dev_guide/recipe_text_llm_node.md),不要再单独运行脚手架 | +| 1:1 的 Embedding/ASR/OCR/Rerank | `--kind model -m `;检查生成端口是否符合真实算法 | +| 多输入、参数、条件生成 | [starter_batch_node](../../../dev_support/node_authoring/starter_batch_node.cpp) | +| 多模型能力 | [starter_multi_model_node](../../../dev_support/node_authoring/starter_multi_model_node.cpp) | +| 拆分且分配子编号/输出 counts | [TextChunkNode](../../../src/common_nodes/text_chunk_node.cpp) 的 `SplitPayloads` | +| 排名与候选来源 | [TextRerankNode](../../../src/common_nodes/text_rerank_node.cpp) | +| 生成参数加自有配置、请求上下文 | [PromptGuidedLlmNode](../../../src/custom_nodes/prompt_guided_llm_node.cpp),共用 [生成参数 helper](../../../include/nodes/generate_options_config.h) | +| 两批关联/分组/部分调用后回填 | `dev_support/node_authoring/starter_batch_{join,group,select_scatter}_node.cpp` | + +示例名称替换成实际操作名;已有实现直接修改,不用 `--force` 覆盖。脚手架默认生成 custom; +common Node 放在 `src/common_nodes/` 并明确 `.Category("common")`。生成的"未实现"占位不是可用算法。 + +## 声明数据关系 + +1. `InputsOf` 声明 typed 输入:`Required` 必须存在,`Optional` 可不连接,`OptionalValue` + 才允许连接后请求中仍缺值。多路数据按完整 key 或明确的请求分组关联,不按数组下标配对。 +2. 保序结果用带 anchor 的 `PreservedOutput`;多输出用 `OutputsOf` / `Produced`;派生批次用 + `ProducedBatch` 与准确的 `PortFlow`。拆分、过滤、排名的来源正确性由算法与测试保证。 + 全部结果在局部成功后返回,由框架发布。 +3. `Parameters` / `Field` 声明参数;跨字段与连线规则用 `Validate` / `ValidateBindings`。 + 复杂 JSON 用 `NodeConfigParser`,不重复默认值和字段校验。 +4. `ModelsOf` / `Model` 声明能力槽;成员类型 `LlmCall`、`EmbeddingCall`、`AsrCall`、`OcrCall`、 + `RerankCall` 决定能力。配置必须显式引用 model_id;保留门面返回的 `NodeResult` 失败。 + +只在有需求时加入 Control 或缓存。`WithControls` 只能更新 `Field` 已绑定的参数;仅由 +`WithParser` 声明的字段不能直接加入字段 Control。typed Fields 与 parser 同时存在时,字段 +Control 还要求显式 `Prepare`。复杂 `WithControl` 返回完整有效候选,框架不会再跑初始化的 +`Prepare`;见 [Control 指南](../../../doc/dev_guide/first_control.md)。缓存使用 +`SessionResources::GetOrCreateResult`,key 显式纳入语义参数、输入和模型 revision。 +借用视图只在本次同步调用中使用。只有可证明线程安全时设置 `.ParallelSafe(true)`。 + +## 验证与接入 + +优先扩展现有测试;`tests/unit/nodes/test_*.cpp` 自动收集到节点 runner,新套件还需纳入 +CTest filter;脚手架 `--write-test` 沿用 `CustomNodeCatalogTest`。聚焦测试执行真实 `Run`, +检查实际业务输出、空/缺失输入、多个请求及非零 `sub_id`、数量与来源、模型失败不发布结果; +Control/缓存改动补回滚与并发测试。仅创建成功或检查 Catalog 不证明算法正确。 + +```bash +cmake --build build --target edgeflow_test_nodes_runner alg_pipeline_tool -j 4 +./build/edgeflow_test_nodes_runner --gtest_list_tests +./build/alg_pipeline_tool describe-node +``` + +确认 Definition 后用 [pipeline-composer](../pipeline-composer/SKILL.md) 接回方案并执行。 +最终证据与门禁见 [Verification](../llm-edgeflow-developer-guide/references/verification.md)。 diff --git a/.agents/skills/edgeflow-node-llm-developer/SKILL.md b/.agents/skills/edgeflow-node-llm-developer/SKILL.md deleted file mode 100644 index 46aae0fb..00000000 --- a/.agents/skills/edgeflow-node-llm-developer/SKILL.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: edgeflow-node-llm-developer -description: 用 MakeLlmTextSpec 新增 LLM-EdgeFlow 文本 LLM Node,编写 BuildPrompt/FormatAnswer 两个函数,保持单输入单输出及一次生成。仅改提示词优先配置;动态采样、多模型、多轮或多路输入转 Batch skill。 ---- - -# 文本 LLM 两函数 Node - -先判断已有生成/模板节点的配置是否足够;足够则使用 -[pipeline-composer](../pipeline-composer/SKILL.md)。确需 C++ 前后处理时,读取 -[Node 共享契约](../llm-edgeflow-developer-guide/references/capability-nodes.md) 和 -[两函数练习](../../../doc/dev_guide/first_custom_node.md)。开发流程遵循 -[CONTRIBUTING](../../../CONTRIBUTING.md)。 - -## 开发最小算法 - -使用 [starter_llm_node.cpp](../../../dev_support/node_authoring/starter_llm_node.cpp): -`BuildPrompt` 从输入文本构造提示词,`FormatAnswer` 处理模型文本; -`MakeLlmTextSpec` 组合一次批量文本生成,自动保持数量、顺序与 `(req_id, sub_id)`。 -这里“一次”指一次生成调用,不是只解码一个 token。平台 JSON 提取/响应序列化仍归 Adapter。 - -```bash -python3 tools/scaffold_custom_node.py ExtractFactsNode --kind model -m llm \ - --write-test -``` - -名称按真实操作替换。脚手架写入 `src/custom_nodes/` 并生成测试;中性 common -操作仍使用同一 API,但放在 `src/common_nodes/` 并明确 category。 -Spec 用 `REGISTER_FUNCTION_NODE` 注册,无需另写生命周期或 Definition。 - -需要同时生成完整可运行方案时,可以使用 -[text-llm-node Recipe](../../../doc/dev_guide/recipe_text_llm_node.md),按该文档执行 prepare -和它返回的 verify 命令;不要再重复运行独立脚手架。Recipe 仅支持单输出部署,源方案 -必须恰有一个符合条件的文本 LLM 替换点,并且不会移植被替换节点的算法或采样字段。 - -## 识别何时改用 Batch - -轻量模板只要求模型引用 `bind_model`,不自动支持完整生成节点的所有配置。 -`MakeLlmTextSpec` 的 `GenerateOptions` 在构建 Spec 时固定;传入 Parameters 并不会让 -采样参数随请求快照改变。需要可配置/Control 采样时,用 -[Batch skill](../edgeflow-node-batch-developer/SKILL.md) 在 Run 中从参数构造 options。 -无字段 Control 的复杂配置可复用 `GenerateOptionsFields` / `ParseGenerateOptions`, -显式选择 token 默认值;需要字段 Control 时优先 typed `Field`,其与 parser-only 字段 -的区别及复杂更新路径见 Batch skill。 -多输入、上下文汇合、结构化多输出、条件第二次生成或多模型同样走 Batch。 - -## 验证真实函数行为 - -测试实际提示词、模型调用次数、后处理文本、空批次、多请求/非零子编号及模型失败。 -借助 `NodeHarness` 注入确定性模型,失败不能发布部分输出;不要仅断言创建成功或 Mock 固定答案。 -将独立业务期望写进生成测试,再构建节点 runner 和目标工具: - -```bash -cmake --build build --target edgeflow_test_nodes_runner alg_pipeline_tool -j 4 -./build/edgeflow_test_nodes_runner --gtest_list_tests -./build/alg_pipeline_tool describe-node ExtractFactsNode -``` - -运行实际过滤器,回到用户 Pipeline 校验和执行。使用测试注册的方案全程选择测试工具, -真实效果仍需对应模型与样例。最终证据和门禁见 -[Verification](../llm-edgeflow-developer-guide/references/verification.md)。 diff --git a/.agents/skills/edgeflow-node-map-developer/SKILL.md b/.agents/skills/edgeflow-node-map-developer/SKILL.md deleted file mode 100644 index 573e0f7c..00000000 --- a/.agents/skills/edgeflow-node-map-developer/SKILL.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: edgeflow-node-map-developer -description: 新增或修改 LLM-EdgeFlow 单输入、逐项一对一、无需模型的纯计算 Node,使用 MakeMapSpec 自动保持顺序和来源。适用于 common/custom 的文本或载荷变换;拆分、过滤、聚合转 Batch skill。 ---- - -# 逐项纯计算 Node - -先从目标 Catalog 确认现有节点或配置不能完成需求,再读取 -[Node 共享契约](../llm-edgeflow-developer-guide/references/capability-nodes.md)。 -[CONTRIBUTING](../../../CONTRIBUTING.md) 管理开发流程。中性通用操作放 `src/common_nodes/`; -领域算法放 `src/custom_nodes/`,按操作命名,二者使用同一作者 API。 - -## 选择与实现 - -适用条件:一项输入产生一项输出,不调用模型、不跨样本关联、不改变数量或来源。 -使用普通载荷函数与 `MakeMapSpec`,通过 `REGISTER_FUNCTION_NODE` 从 Spec 生成运行时与 -Definition。框架负责批循环、数量、顺序、来源和发布;不要继承 `NodeBase` 或手工操作 Context。 - -TextBatch → TextBatch 的 custom Node 可使用现有脚手架: - -```bash -python3 tools/scaffold_custom_node.py NormalizeTextNode --kind compute \ - --in-port input:TextBatch --out-port output:TextBatch --write-test -``` - -示例名称需替换成实际操作名,已有实现直接修改,不用 `--force` 覆盖。 -脚手架的最简 Map 路径只自动生成该文本形态;其他载荷类型按实际 `MakeMapSpec` 接口实现, -不要把生成的“未实现”占位当可用算法。填入真实算法和独立业务期望;需要字段时用 -`Parameters` / `Field` 绑定普通参数结构, -默认值、范围与语义只声明一次。common Node 选择相同 API,放在 -`src/common_nodes/` 并明确 `.Category("common")`;脚手架默认生成 custom。 - -以下情况改用 [Batch skill](../edgeflow-node-batch-developer/SKILL.md):过滤/拆分/聚合、 -多输入/输出、请求分组、需要检查整批数据或模型调用。逐项函数自身可以返回 `NodeResult` -表达失败,框架会阻止发布部分结果。文本 LLM 前后处理使用 -[LLM skill](../edgeflow-node-llm-developer/SKILL.md)。不要让 Map 的输出悄悄丢项或重新编号。 - -## 验证与接入 - -复用生成的测试或 `tests/unit/nodes/` 的现有套件,参考 -[NodeHarness](../../../tests/support/node_harness.h) 的真实接口。 -断言实际业务输出、空输入、多个请求及非零 `sub_id`、输入未修改;增加算法自身的边界。 -仅创建成功或检查 Catalog 不证明算法正确。 - -```bash -cmake --build build --target edgeflow_test_nodes_runner alg_pipeline_tool -j 4 -./build/edgeflow_test_nodes_runner --gtest_list_tests -./build/alg_pipeline_tool describe-node NormalizeTextNode -``` - -按实际套件运行聚焦测试。确认 Definition 的类型、字段和 `1:1`/`preserve` 后, -用 [pipeline-composer](../pipeline-composer/SKILL.md) 连接节点,validate/plan 并执行用户方案。 -最终交付按 [Verification](../llm-edgeflow-developer-guide/references/verification.md)。 diff --git a/.agents/skills/edgeflow-solution-planner/SKILL.md b/.agents/skills/edgeflow-solution-planner/SKILL.md index 6293fb19..9ea4952d 100644 --- a/.agents/skills/edgeflow-solution-planner/SKILL.md +++ b/.agents/skills/edgeflow-solution-planner/SKILL.md @@ -38,9 +38,7 @@ Catalog 和完整 Operator 请求/响应为依据;流程与设计边界遵循 | --- | --- | | 端口、业务契约与能力均匹配,只调整提示词/连线/模型实例 | [pipeline-composer](../pipeline-composer/SKILL.md) | | 外部字段提取、序列化、容量、载体或业务契约不同 | [edgeflow-adapter-developer](../edgeflow-adapter-developer/SKILL.md) | -| 新增单输入逐项纯变换,数量和来源保持 | [edgeflow-node-map-developer](../edgeflow-node-map-developer/SKILL.md) | -| 新增单文本前处理、一次 LLM 调用、文本后处理 | [edgeflow-node-llm-developer](../edgeflow-node-llm-developer/SKILL.md) | -| 多输入/输出、拆分/聚合/排名、动态采样或其他模型调用算法 | [edgeflow-node-batch-developer](../edgeflow-node-batch-developer/SKILL.md) | +| 新增逐项变换、文本 LLM 前后处理、多输入/输出、拆分/聚合/排名或其他模型调用算法 | [edgeflow-node-developer](../edgeflow-node-developer/SKILL.md) | | 现有运行协议可用,缺少模型预处理或输出语义 | [edgeflow-model-developer](../edgeflow-model-developer/SKILL.md) | | 缺少厂商运行时/硬件执行支持 | [edgeflow-backend-developer](../edgeflow-backend-developer/SKILL.md) | | 现有调度、类型或生命周期机制不能表达必要契约 | [developer guide](../llm-edgeflow-developer-guide/SKILL.md) 的 Core 路径,先举证缺口 | diff --git a/.agents/skills/json-prompt-solution/SKILL.md b/.agents/skills/json-prompt-solution/SKILL.md index 4d2ca2ba..6693cbff 100644 --- a/.agents/skills/json-prompt-solution/SKILL.md +++ b/.agents/skills/json-prompt-solution/SKILL.md @@ -30,9 +30,8 @@ description: Build LLM-EdgeFlow solutions that transform a field from a complete 平台类型。不同业务可以复用载体,同时注册自己的契约,保持旧业务语义。 遵循业务接入指南中的注册完整性要求。 4. 按 `CONTRIBUTING.md` 判断设计审查要求;追加新业务类型需记录接口决定并更新 - 现行契约文档,普通配置不需额外审批。算法能力缺失时才考虑 custom Node;简单文本前后处理 - 用 [LLM Node skill](../edgeflow-node-llm-developer/SKILL.md),复杂数据关系用 - [Batch Node skill](../edgeflow-node-batch-developer/SKILL.md)。不把平台转换放进 Core 或 Nodes。 + 现行契约文档,普通配置不需额外审批。算法能力缺失时才考虑 custom Node,见 + [Node skill](../edgeflow-node-developer/SKILL.md)。不把平台转换放进 Core 或 Nodes。 当前翻译参照 `doc/solutions/translate.md`、`src/adapter/biz/translate_bindings.cpp` 和 `configs/pipeline_translate_cpu.json`。其业务 `translate` 复用既有文本/JSON 载体、一个 diff --git a/.agents/skills/llm-edgeflow-developer-guide/SKILL.md b/.agents/skills/llm-edgeflow-developer-guide/SKILL.md index 2e411237..f471276d 100644 --- a/.agents/skills/llm-edgeflow-developer-guide/SKILL.md +++ b/.agents/skills/llm-edgeflow-developer-guide/SKILL.md @@ -15,17 +15,15 @@ start with current guides and affected code/tests. | :--- | :--- | | Business requirements needing component selection and a DAG | [Solution planner](../edgeflow-solution-planner/SKILL.md) | | Operator SDK, external payload, Converter, IoBinding | [Adapter developer](../edgeflow-adapter-developer/SKILL.md) | -| One input item to one output item, pure computation | [Map Node developer](../edgeflow-node-map-developer/SKILL.md) | -| Text preprocessing → one LLM call → text postprocessing | [LLM Node developer](../edgeflow-node-llm-developer/SKILL.md) | -| Multiple ports/models, derived outputs, configurable sampling, batch algorithms | [Batch Node developer](../edgeflow-node-batch-developer/SKILL.md) | +| Capability Node algorithms, from item transforms and text LLM to multi-port/model batches | [Node developer](../edgeflow-node-developer/SKILL.md) | | Model preprocessing, semantics and capabilities | [Model developer](../edgeflow-model-developer/SKILL.md) | | Vendor runtime, execution protocol implementation and resources | [Backend developer](../edgeflow-backend-developer/SKILL.md) | | Pipeline lifecycle, Validator/planning, typed Blackboard, sessions | [Orchestration](references/orchestration.md) | | Demo carriers, dataset, registration, result display | [Demo onboarding](../../../doc/dev_guide/business_onboarding.md#5-统一-demo-接入) | -Map, LLM and Batch are authoring forms of the same runtime. Common versus custom identifies +Every Node uses one authoring structure (`MakeNodeSpec`). Common versus custom identifies ownership, not a second set of authoring interfaces. For existing Node parameter/Control work, -read [shared Node contracts](references/capability-nodes.md) and only the applicable form; +read [shared Node contracts](references/capability-nodes.md); use the [compiled Control example](../../../doc/dev_guide/first_control.md) when needed. External field selection/response assembly is Integration work even when the carrier layout diff --git a/.agents/skills/llm-edgeflow-developer-guide/references/capability-nodes.md b/.agents/skills/llm-edgeflow-developer-guide/references/capability-nodes.md index 5a020752..12a8fce3 100644 --- a/.agents/skills/llm-edgeflow-developer-guide/references/capability-nodes.md +++ b/.agents/skills/llm-edgeflow-developer-guide/references/capability-nodes.md @@ -1,15 +1,18 @@ # Capability Nodes Use this reference for production Node implementation. Start first-time LLM authors with the -[two-function exercise](../../../../doc/dev_guide/first_custom_node.md); consult the -[concept guide](../../../../doc/dev_guide/custom_node_concepts.md) for batch contracts. +[first Node exercise](../../../../doc/dev_guide/first_custom_node.md); consult the +[concept guide](../../../../doc/dev_guide/custom_node_concepts.md) for signatures and batch contracts. 1. Query the target build's `alg_pipeline_tool catalog` and `describe-node` before adding a capability. 2. Keep neutral operations in `src/common_nodes/` and domain algorithms in `src/custom_nodes/`, organized by operation. Common Nodes, Core and Engine must not depend on custom implementations. Platform conversion stays in Adapter. Follow CONTRIBUTING for design review criteria. -3. Use ordinary functions and one Spec contract, registered with `REGISTER_FUNCTION_NODE`. - Map and LLM helpers compose the same authoring contract as Batch; +3. Every Node has the same structure: `Inputs`, optional `Params` and `Models`, one `Run` and a + `Spec` built with `MakeNodeSpec`, registered with `REGISTER_FUNCTION_NODE`. `Run` takes + `const Inputs&`, then `const Params&` and `const Models&` only when the Spec declares them, and + finally `const SessionResources&` when it uses session caches. Per-item work inside `Run` uses + `MapPayloads`, which keeps provenance and names the failing item. `NodeBase` is internal runtime infrastructure, not another business authoring choice. 4. Declare input views with `InputsOf` and typed members. `Required` requires a value; `Optional` permits an unconnected port; `OptionalValue` also permits a connected port without a request value @@ -33,7 +36,7 @@ Use this reference for production Node implementation. Start first-time LLM auth model lookup or request Blackboard access. TextEmbeddingNode is the compiled cache example. Use `GetOrCreateResult` for a factory returning `NodeResult`; the facade preserves failures for single-flight waiters without caching them. Resource keys and model revision remain explicit. -9. Use `WithControls` for typed `Field` updates, or Batch's `WithControl` for complex command schemas and ordinary +9. Use `WithControls` for typed `Field` updates, or `WithControl` for complex command schemas and ordinary state-building functions. The framework serializes updates, retains old state on failure and reads one immutable snapshot per request. TextTemplateNode and TextRuleMatchNode are production examples. Follow the [Control guide](../../../../doc/dev_guide/first_control.md) for wire schema and delivery. @@ -42,22 +45,21 @@ Use this reference for production Node implementation. Start first-time LLM auth Combining `WithParser` and field controls (`WithControls`) requires an explicit `Prepare`; field updates rerun it before semantic/binding validation and candidate publication. Parser-only fields are not typed bindings and cannot be selected by `WithControls`, even with - `Prepare`; use typed Fields or a complex Batch updater with explicit normalization/validation. -10. Declare category, description and any actual business restrictions in the Spec. - Ordinary Map/LLM functions work on payloads; their existing helpers preserve provenance and flow. + `Prepare`; use typed Fields or a complex `WithControl` updater with explicit normalization/validation. +10. Declare category and description in the Spec; Nodes are not restricted to particular businesses. Keep the default conservative parallel safety for sequential use; explicitly establish `.ParallelSafe(true)` only when making the implementation available to parallel graphs. Generated Definition is the only Catalog source; do not maintain a second UI registry. Initialization consumes a ValidatedNodePlan; do not call PipelineValidator inside a Node. -For parser-based LLM generation parameters, compose `GenerateOptionsFields(default_max_tokens)` and -`ParseGenerateOptions` from [generate_options_config.h](../../../../include/nodes/generate_options_config.h) -through the existing `NodeConfigParser`; each caller supplies its token default explicitly. -`MakeLlmTextSpec` captures fixed `GenerateOptions` when the Spec is built; those options do not change -with the parameters snapshot. When sampling options must follow configuration or Control updates, -use `MakeBatchSpec` and pass options from the current parameters to `LlmCall::Generate`, as in -[PromptGuidedLlmNode](../../../../src/custom_nodes/prompt_guided_llm_node.cpp) for configuration. -For sampling field controls, prefer typed Fields; that parser-based example does not itself add Control. +When generation options are a Node's only parameters, use `GenerateParameters(default_max_tokens)` +from [generate_options_config.h](../../../../include/nodes/generate_options_config.h) and pass the +`GenerateOptions` received by `Run` to `LlmCall::Generate`, as LlmGenerateNode and the LLM starter do. +With additional fields, compose `GenerateOptionsFields(default_max_tokens)` and `ParseGenerateOptions` +through `NodeConfigParser`, as in +[PromptGuidedLlmNode](../../../../src/custom_nodes/prompt_guided_llm_node.cpp); each caller supplies +its token default explicitly. For sampling field controls, prefer typed Fields; that parser-based +example does not itself add Control. Use existing production implementations and matching `tests/unit/nodes/test_*_node.cpp` suites. Add focused behavior and contract coverage for changed configuration, missing values, output provenance, diff --git a/AGENTS.md b/AGENTS.md index 314acc41..9b8e8250 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,15 +17,13 @@ sections before editing or delivering. [pipeline-composer](.agents/skills/pipeline-composer/SKILL.md). - Complete JSON request → prompt processing → JSON response solutions: [json-prompt-solution](.agents/skills/json-prompt-solution/SKILL.md), then its relevant route. -- Component implementation: choose the smallest authoring path below. Common/custom Node - ownership is independent of Map/LLM/Batch authoring; shared contracts remain in the linked guides. +- Component implementation: choose the skill below. Common and custom Nodes share one + authoring structure; shared contracts remain in the linked guides. | Component / task | Skill | | :--- | :--- | | Operator input/output, Converter, IoBinding | [edgeflow-adapter-developer](.agents/skills/edgeflow-adapter-developer/SKILL.md) | - | Node: pure per-item 1:1 transform | [edgeflow-node-map-developer](.agents/skills/edgeflow-node-map-developer/SKILL.md) | - | Node: text preparation, one LLM call, text result processing | [edgeflow-node-llm-developer](.agents/skills/edgeflow-node-llm-developer/SKILL.md) | - | Node: batch/multi-port/model algorithms, derived output, dynamic sampling | [edgeflow-node-batch-developer](.agents/skills/edgeflow-node-batch-developer/SKILL.md) | + | Node: item transforms, text LLM, multi-port/model algorithms, derived output | [edgeflow-node-developer](.agents/skills/edgeflow-node-developer/SKILL.md) | | Model semantics and preprocessing | [edgeflow-model-developer](.agents/skills/edgeflow-model-developer/SKILL.md) | | Backend runtime and resources | [edgeflow-backend-developer](.agents/skills/edgeflow-backend-developer/SKILL.md) | diff --git a/dev_support/node_authoring/benchmark/README.md b/dev_support/node_authoring/benchmark/README.md index b2ce5c82..7b8bbf91 100644 --- a/dev_support/node_authoring/benchmark/README.md +++ b/dev_support/node_authoring/benchmark/README.md @@ -14,7 +14,8 @@ python3 dev_support/node_authoring/benchmark/run.py \ `summary.json` 的 `baseline_helper_revision` 记录该版本。 - 旧基类依赖的类写法端口辅助函数已移出 `NodeBase`,由 [legacy_node_base.h](../legacy_node_base.h) 的 `LegacyNodeBase` 提供;脚本提取旧头文件时改为继承它。 -- 旧 Map 为等价显式 `LegacyNodeBase` 透传实现的重建,不冒充历史生产 Node。 +- `old_map` 为等价显式 `LegacyNodeBase` 透传实现的重建,不冒充历史生产 Node; + `new_map` 用 `MakeNodeSpec` 与 `MapPayloads` 实现同样的逐条透传。 - 每批 32 条、每条 1024 字节,固定 Echo mock;预热 100 次,每个进程测 10000 次,独立重复 5 次。 - 测量 `Process` 的线程 CPU 时间及 C++ `new/new[]` 请求次数、字节数;不包含输入发布和输出断言, 也不代表全部 malloc、存活堆或峰值 RSS。每次校验文本及 req/sub,记录实际模型调用次数。 diff --git a/dev_support/node_authoring/benchmark/probe.cpp b/dev_support/node_authoring/benchmark/probe.cpp index 10ce74b5..4f6678bd 100644 --- a/dev_support/node_authoring/benchmark/probe.cpp +++ b/dev_support/node_authoring/benchmark/probe.cpp @@ -83,26 +83,27 @@ class ExplicitMap final : public LegacyNodeBase { BoundOutput output_{"output"}; }; std::string Identity(const std::string& s) { return s; } -auto ProbeMapSpec() { - return MakeMapSpec(Input("input"), Output("output"), - &Identity); -} struct Inputs { const TextBatch* input{}; }; +NodeResult RunMap(const Inputs& in) { + return MapPayloads(*in.input, &Identity); +} +auto ProbeMapSpec() { + return MakeNodeSpec(InputsOf({Required("input", &Inputs::input)}), + PreservedOutput("output", "input"), &RunMap); +} struct Models { LlmCall llm; }; -NodeResult RunBatch(const Inputs& in, const NoParameters&, - const Models& models) { +NodeResult RunBatch(const Inputs& in, const Models& models) { if (in.input->empty()) return TextBatch{}; auto prompts = MapPayloads(*in.input, &Identity); auto answer = models.llm.Generate(prompts, GenerateOptions{}); if (!answer.ok()) return answer; return MapPayloads(answer.value(), &Identity); } -NodeResult RunBatchInPlace(const Inputs& in, const NoParameters&, - const Models& models) { +NodeResult RunBatchInPlace(const Inputs& in, const Models& models) { if (in.input->empty()) return TextBatch{}; auto prompts = MapPayloads(*in.input, &Identity); auto answer = models.llm.Generate(prompts, GenerateOptions{}); @@ -112,16 +113,16 @@ NodeResult RunBatchInPlace(const Inputs& in, const NoParameters&, return output; } auto ProbeBatchSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({Required("input", &Inputs::input)}), PreservedOutput("output", "input"), - ModelsOf({Llm("llm", "bind_model", &Models::llm)}), &RunBatch); + ModelsOf({Model("llm", "bind_model", &Models::llm)}), &RunBatch); } auto ProbeBatchInPlaceSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({Required("input", &Inputs::input)}), PreservedOutput("output", "input"), - ModelsOf({Llm("llm", "bind_model", &Models::llm)}), + ModelsOf({Model("llm", "bind_model", &Models::llm)}), &RunBatchInPlace); } REGISTER_FUNCTION_NODE(ProbeBatchInPlace, ProbeBatchInPlaceSpec()); diff --git a/dev_support/node_authoring/starter_batch_group_node.cpp b/dev_support/node_authoring/starter_batch_group_node.cpp index b3bc3fc2..f87cef57 100644 --- a/dev_support/node_authoring/starter_batch_group_node.cpp +++ b/dev_support/node_authoring/starter_batch_group_node.cpp @@ -11,14 +11,11 @@ struct Inputs { const TextBatch* references = nullptr; }; -struct Options {}; - struct Models { LlmCall generator; }; -NodeResult Run(const Inputs& inputs, const Options& /*options*/, - const Models& models) { +NodeResult Run(const Inputs& inputs, const Models& models) { if (!inputs.queries || inputs.queries->empty()) { return NodeResult::Success(TextBatch{}); } @@ -54,17 +51,16 @@ NodeResult Run(const Inputs& inputs, const Options& /*options*/, } auto Spec() { - return MakeBatchSpec(InputsOf({ - Required("queries", &Inputs::queries), - Optional("references", &Inputs::references, - InputFlow::AggregateByRequest), - }), - PreservedOutput("output", "queries"), - Parameters({}), - ModelsOf({ - Llm("generator", "bind_model", &Models::generator), - }), - &Run) + return MakeNodeSpec(InputsOf({ + Required("queries", &Inputs::queries), + Optional("references", &Inputs::references, + InputFlow::AggregateByRequest), + }), + PreservedOutput("output", "queries"), + ModelsOf({ + Model("generator", "bind_model", &Models::generator), + }), + &Run) .Description( "Batch starter with traceable GroupByRequest reference aggregation"); } diff --git a/dev_support/node_authoring/starter_batch_join_node.cpp b/dev_support/node_authoring/starter_batch_join_node.cpp index 2340c010..cb50cc84 100644 --- a/dev_support/node_authoring/starter_batch_join_node.cpp +++ b/dev_support/node_authoring/starter_batch_join_node.cpp @@ -11,14 +11,11 @@ struct Inputs { const TextBatch* attributes = nullptr; }; -struct Options {}; - struct Models { LlmCall generator; }; -NodeResult Run(const Inputs& inputs, const Options& /*options*/, - const Models& models) { +NodeResult Run(const Inputs& inputs, const Models& models) { if (!inputs.questions || inputs.questions->empty()) { return NodeResult::Success(TextBatch{}); } @@ -44,16 +41,15 @@ NodeResult Run(const Inputs& inputs, const Options& /*options*/, } auto Spec() { - return MakeBatchSpec(InputsOf({ - Required("questions", &Inputs::questions), - Optional("attributes", &Inputs::attributes), - }), - PreservedOutput("output", "questions"), - Parameters({}), - ModelsOf({ - Llm("generator", "bind_model", &Models::generator), - }), - &Run) + return MakeNodeSpec(InputsOf({ + Required("questions", &Inputs::questions), + Optional("attributes", &Inputs::attributes), + }), + PreservedOutput("output", "questions"), + ModelsOf({ + Model("generator", "bind_model", &Models::generator), + }), + &Run) .Description("Batch starter with traceable Left Join across inputs"); } diff --git a/dev_support/node_authoring/starter_batch_node.cpp b/dev_support/node_authoring/starter_batch_node.cpp index 087ef779..451f257a 100644 --- a/dev_support/node_authoring/starter_batch_node.cpp +++ b/dev_support/node_authoring/starter_batch_node.cpp @@ -11,7 +11,7 @@ struct Inputs { const TextBatch* context = nullptr; }; -struct Options { +struct Params { bool retry_once = false; }; @@ -20,7 +20,7 @@ struct Models { }; // 所有请求值都只在这个普通业务函数内使用。 -NodeResult Run(const Inputs& inputs, const Options& options, +NodeResult Run(const Inputs& inputs, const Params& params, const Models& models) { TextBatch prompts; prompts.reserve(inputs.questions->size()); @@ -35,7 +35,7 @@ NodeResult Run(const Inputs& inputs, const Options& options, prompt + question.data); } auto result = models.generator.Generate(prompts); - if (!result.ok() && options.retry_once && + if (!result.ok() && params.retry_once && result.failure().kind == NodeErrorKind::kModelCallError) { return models.generator.Generate(prompts); } @@ -43,18 +43,18 @@ NodeResult Run(const Inputs& inputs, const Options& options, } auto Spec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({ Required("questions", &Inputs::questions), Optional("context", &Inputs::context, InputFlow::AggregateByRequest), }), PreservedOutput("output", "questions"), - Parameters({ - Field("retry_once", &Options::retry_once).Default(false), + Parameters({ + Field("retry_once", &Params::retry_once).Default(false), }), ModelsOf({ - Llm("generator", "bind_model", &Models::generator), + Model("generator", "bind_model", &Models::generator), }), &Run) .Description("Batch starter with optional request context and one retry"); diff --git a/dev_support/node_authoring/starter_batch_select_scatter_node.cpp b/dev_support/node_authoring/starter_batch_select_scatter_node.cpp index 3ec060e2..47a35839 100644 --- a/dev_support/node_authoring/starter_batch_select_scatter_node.cpp +++ b/dev_support/node_authoring/starter_batch_select_scatter_node.cpp @@ -10,7 +10,7 @@ struct Inputs { const TextBatch* input = nullptr; }; -struct Options { +struct Params { std::string polish_tag = "[POLISH]"; }; @@ -19,7 +19,7 @@ struct Models { LlmCall polisher; }; -NodeResult Run(const Inputs& inputs, const Options& options, +NodeResult Run(const Inputs& inputs, const Params& params, const Models& models) { if (!inputs.input || inputs.input->empty()) { return NodeResult::Success(TextBatch{}); @@ -35,7 +35,7 @@ NodeResult Run(const Inputs& inputs, const Options& options, // 第 2 步:选出需要润色的条目 (包含 polish_tag) auto selection_res = SelectBatch(drafts, [&](const std::string& text) { - return text.find(options.polish_tag) != std::string::npos; + return text.find(params.polish_tag) != std::string::npos; }); if (!selection_res.ok()) { return NodeResult::Failure( @@ -62,17 +62,17 @@ NodeResult Run(const Inputs& inputs, const Options& options, } auto Spec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({ Required("input", &Inputs::input), }), PreservedOutput("output", "input"), - Parameters({ - Field("polish_tag", &Options::polish_tag).Default("[POLISH]"), + Parameters({ + Field("polish_tag", &Params::polish_tag).Default("[POLISH]"), }), ModelsOf({ - Llm("generator", "bind_model", &Models::generator), - Llm("polisher", "polish_model", &Models::polisher), + Model("generator", "bind_model", &Models::generator), + Model("polisher", "polish_model", &Models::polisher), }), &Run) .Description( diff --git a/dev_support/node_authoring/starter_control_node.cpp b/dev_support/node_authoring/starter_control_node.cpp index 08ec6ede..2c7cca29 100644 --- a/dev_support/node_authoring/starter_control_node.cpp +++ b/dev_support/node_authoring/starter_control_node.cpp @@ -6,7 +6,11 @@ namespace llm_edgeflow { namespace custom_nodes { namespace StarterControlNode_impl { -struct StarterControlNodeParams { +struct Inputs { + const TextBatch* input = nullptr; +}; + +struct Params { std::string prefix; }; @@ -14,24 +18,25 @@ struct StarterControlNodeParams { inline constexpr int kUpdatePrefix = 1001; // 业务逻辑处理普通数据,而非平台结构。 -static std::string ApplyPrefix(const std::string& input, - const StarterControlNodeParams& params) { - return params.prefix + input; +NodeResult Run(const Inputs& inputs, const Params& params) { + return MapPayloads(*inputs.input, [¶ms](const std::string& text) { + return params.prefix + text; + }); } -auto StarterControlNodeSpec() { - return MakeMapSpec( - Input("input"), Output("output"), - Parameters( +auto Spec() { + return MakeNodeSpec( + InputsOf{Required("input", &Inputs::input)}, + PreservedOutput("output", "input"), + Parameters( { - Field("prefix", &StarterControlNodeParams::prefix) + Field("prefix", &Params::prefix) .Default("") .Description( "Text prepended to each input; at most 64 UTF-8 " "bytes. Control replaces this initial value."), }) - .Validate([](const StarterControlNodeParams& params, - std::string* diagnostic) { + .Validate([](const Params& params, std::string* diagnostic) { if (params.prefix.size() > 64) { if (diagnostic) { *diagnostic = "prefix exceeds 64 UTF-8 bytes"; @@ -40,7 +45,7 @@ auto StarterControlNodeSpec() { } return true; }), - &ApplyPrefix) + &Run) .Description("Control authoring starter") .WithControls({ ReplaceFields(kUpdatePrefix, "set_prefix", {"prefix"}, @@ -48,7 +53,7 @@ auto StarterControlNodeSpec() { }); } -REGISTER_FUNCTION_NODE(StarterControlNode, StarterControlNodeSpec()); +REGISTER_FUNCTION_NODE(StarterControlNode, Spec()); } // namespace StarterControlNode_impl } // namespace custom_nodes diff --git a/dev_support/node_authoring/starter_llm_node.cpp b/dev_support/node_authoring/starter_llm_node.cpp index 7c6d3fd9..5b1eb26f 100644 --- a/dev_support/node_authoring/starter_llm_node.cpp +++ b/dev_support/node_authoring/starter_llm_node.cpp @@ -12,13 +12,37 @@ static std::string BuildPrompt(const std::string& text) { return text; } static std::string FormatAnswer(const std::string& text) { return text; } -auto StarterLlmSpec() { - return MakeLlmTextSpec(Input("input"), Output("output"), - &BuildPrompt, &FormatAnswer) +struct Inputs { + const TextBatch* input = nullptr; +}; + +struct Models { + LlmCall generator; +}; + +// 逐条构造提示词,整批调用一次模型,再逐条整理回答;来源编号由框架保留。 +// 生成参数(max_tokens、temperature 等)来自节点配置。 +NodeResult Run(const Inputs& inputs, const GenerateOptions& options, + const Models& models) { + auto answers = models.generator.Generate( + MapPayloads(*inputs.input, &BuildPrompt), options); + if (!answers.ok()) return answers; + return MapPayloads(answers.value(), &FormatAnswer); +} + +auto Spec() { + return MakeNodeSpec(InputsOf{Required("input", &Inputs::input)}, + PreservedOutput("output", "input"), + GenerateParameters(128), + ModelsOf{ + Model("generator", "bind_model", &Models::generator, + "引用 models[].model_id;所选模型必须提供 llm " + "文本生成能力。")}, + &Run) .Description("LLM authoring starter"); } -REGISTER_FUNCTION_NODE(StarterLlmNode, StarterLlmSpec()); +REGISTER_FUNCTION_NODE(StarterLlmNode, Spec()); } // namespace } // namespace custom_nodes diff --git a/dev_support/node_authoring/starter_multi_model_node.cpp b/dev_support/node_authoring/starter_multi_model_node.cpp index 8735ba4a..8801e456 100644 --- a/dev_support/node_authoring/starter_multi_model_node.cpp +++ b/dev_support/node_authoring/starter_multi_model_node.cpp @@ -16,8 +16,7 @@ struct Models { EmbeddingCall encoder; }; -NodeResult Run(const Inputs& inputs, const NoParameters&, - const Models& models) { +NodeResult Run(const Inputs& inputs, const Models& models) { auto embeddings = models.encoder.Embed(*inputs.questions); if (!embeddings.ok()) { return NodeResult::Failure( @@ -41,12 +40,12 @@ NodeResult Run(const Inputs& inputs, const NoParameters&, } auto Spec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({Required("questions", &Inputs::questions)}), PreservedOutput("output", "questions"), ModelsOf({ - Llm("generator", "bind_llm", &Models::generator), - Embedding("encoder", "bind_embedding", &Models::encoder), + Model("generator", "bind_llm", &Models::generator), + Model("encoder", "bind_embedding", &Models::encoder), }), &Run) .Description( diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index b01d76a3..379710b5 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +所有 Node 只保留一种写法:`MakeBatchSpec` 改名为 `MakeNodeSpec`,删除 `MakeMapSpec` 与 +`MakeLlmTextSpec`;每个 Node 统一由 `Inputs`、可选的 `Params` 与 `Models`、`Run`、`Spec` 组成, +12 个生产 Node 按此命名。`Run` 只接收 Spec 实际声明的部分(不再写 `const NoParameters&` 或 +`const NoModels&`),删除成员函数形式的 `Run`;模型槽统一用 `Model(...)` 声明,删除 `Llm(...)` 与 +`Embedding(...)` 别名。逐项处理改用 `MapPayloads`,失败诊断会标出失败条目的 `req_id` / `sub_id`。 +LLM 入门模板与脚手架生成的 LLM 节点改为从节点配置读取 `max_tokens`、`temperature` 等生成参数 +(新增 `GenerateParameters`,默认值与原来一致)。删除 Node 的 `biz_names` 限制、Catalog 中的该字段 +和诊断 `NODE_BIZ_MISMATCH`。三个 Node skill 合并为 `edgeflow-node-developer`。 + 门禁与测试不再甄别历史名称:文档漂移检查只保留核心概念、架构图和版本一致性检查,删除旧业务名、 旧注册宏、已移除接口与路径的黑名单;LayerGuard 和治理检查删除针对已不存在头文件、接口和旧术语的 规则;固定已删除字段、命令或参数名的测试改为通用的未知字段检查或删除。元测试删除对 `ci.yml` diff --git a/doc/README.md b/doc/README.md index 14929fcd..d1bb6d90 100644 --- a/doc/README.md +++ b/doc/README.md @@ -26,15 +26,13 @@ | 已有能力的配置与运行 | [pipeline-composer](../.agents/skills/pipeline-composer/SKILL.md) | | 完整 JSON 输入、提示词处理、完整 JSON 输出 | [json-prompt-solution](../.agents/skills/json-prompt-solution/SKILL.md) | | Adapter、转换器、业务绑定与输出容量 | [edgeflow-adapter-developer](../.agents/skills/edgeflow-adapter-developer/SKILL.md) | -| Node:逐项纯计算、数量和来源不变 | [edgeflow-node-map-developer](../.agents/skills/edgeflow-node-map-developer/SKILL.md) | -| Node:文本前处理、一次 LLM 生成、文本后处理 | [edgeflow-node-llm-developer](../.agents/skills/edgeflow-node-llm-developer/SKILL.md) | -| Node:多输入输出、拆分聚合、动态采样、各类模型能力 | [edgeflow-node-batch-developer](../.agents/skills/edgeflow-node-batch-developer/SKILL.md) | +| Node:逐项变换、文本 LLM、多输入输出、拆分聚合、各类模型能力 | [edgeflow-node-developer](../.agents/skills/edgeflow-node-developer/SKILL.md) | | Model:预处理与模型语义 | [edgeflow-model-developer](../.agents/skills/edgeflow-model-developer/SKILL.md) | | Backend:运行时与厂商资源 | [edgeflow-backend-developer](../.agents/skills/edgeflow-backend-developer/SKILL.md) | | Core、Demo 与跨组件开发 | [llm-edgeflow-developer-guide](../.agents/skills/llm-edgeflow-developer-guide/SKILL.md) | -Map、LLM、Batch 是同一 Node 框架的三种作者入口。`common/custom` 按中性操作或领域算法 -选择代码归属;Embedding、ASR、OCR、Rerank 复用 Batch 的能力声明,不各自维护生命周期。 +所有 Node 使用同一种写法(`MakeNodeSpec`)。`common/custom` 按中性操作或领域算法 +选择代码归属;Embedding、ASR、OCR、Rerank 复用同一套能力声明,不各自维护生命周期。 例如:“用 `$edgeflow-solution-planner` 分析此请求和响应,列出需要新增的部件与 DAG”; 或“用 `$edgeflow-adapter-developer` 为现有算法补齐新的 JSON 输入输出契约”。 diff --git a/doc/dev_guide/custom_node_concepts.md b/doc/dev_guide/custom_node_concepts.md index 3f8f560e..f1d33c5d 100644 --- a/doc/dev_guide/custom_node_concepts.md +++ b/doc/dev_guide/custom_node_concepts.md @@ -1,48 +1,55 @@ # 自定义 Node:常用写法与按需参考 -普通逐条处理只需要选择输入输出类型、编写业务函数,并在使用模型时明确引用模型实例。 -纯计算使用已有 `MakeMapSpec`;文本 LLM 前后处理使用 `MakeLlmTextSpec`。 -它们自动保持数量、顺序与来源,作者函数只处理载荷,无需接触来源编号、`PortFlow` 或并发声明。 +每个 Node 使用同一结构:`Inputs` 声明输入,可选的 `Params` 与 `Models` 声明配置和模型, +`Run` 写全部处理逻辑,`Spec` 用 `MakeNodeSpec` 把它们登记给框架,再用 +`REGISTER_FUNCTION_NODE` 注册。逐项处理在 `Run` 中调用 `MapPayloads`,数量、顺序与来源编号由它保持。 先完成[第一个自定义 Node](first_custom_node.md)。下面前三节解释常用声明;仅在改变数据关系、 增加共享资源或启用并行执行时,阅读后面的对应部分。 -## 三种写法速查 +## 结构与签名速查 -| 写法 | 适用场景 | 工厂 | 业务函数签名 | 返回 | -| --- | --- | --- | --- | --- | -| Map | 逐项纯计算,数量与来源不变 | `MakeMapSpec(Input, Output, [Parameters

], &Transform)` | `Out Transform(const In&)`
`Out Transform(const In&, const Params&)` | `Out` 或 `NodeResult` | -| LLM 两函数 | 文本前处理 → 一次生成 → 文本后处理 | `MakeLlmTextSpec(Input, Output, [Parameters

], &BuildPrompt, &FormatAnswer)` | `std::string BuildPrompt(const std::string&)`
`std::string BuildPrompt(const std::string&, const Params&)`
`std::string FormatAnswer(const std::string&)`
`std::string FormatAnswer(const std::string&, const Params&)` | 文本:`std::string`、`const char*`、`std::string_view`,或它们的 `NodeResult`;不接受 `char`、`int` 等算术类型 | -| Batch | 多输入多输出、拆分聚合、多模型、会话缓存 | `MakeBatchSpec(InputsOf, <输出声明>, [Parameters

], [ModelsOf], &Run)` | `NodeResult Run(const Inputs&, const Params&)`
`NodeResult Run(const Inputs&, const Params&, const Models&)`
`NodeResult Run(const Inputs&, const Params&, const Models&, const SessionResources&)` | `NodeResult`;多输出为 `NodeResult` | +```cpp +struct Inputs { const TextBatch* input = nullptr; }; // 要哪些输入 +struct Params { std::string prefix; }; // 要哪些配置(可省) +struct Models { LlmCall generator; }; // 要哪些模型(可省) + +NodeResult Run(const Inputs& inputs, const Params& params, + const Models& models); // 全部处理逻辑 + +auto Spec() { // 把上面三者登记给框架 + return MakeNodeSpec(InputsOf{...}, <输出声明>, + [Parameters{...}], [ModelsOf{...}], &Run); +} +REGISTER_FUNCTION_NODE(MyNode, Spec()); +``` -没有 `Parameters<...>` 时,`Params` 是 `NoParameters`。Batch 的 `Run` 仍然要写这个参数, -例如 `Run(const Inputs&, const NoParameters&, const Models&)`,见 -[多模型示例](../../dev_support/node_authoring/starter_multi_model_node.cpp)。 -LLM 两个钩子也接受能隐式转换为 `std::string` 的类型;`std::string_view` 会先复制为自有字符串。 -返回失败用 `NodeResult::Failure(...)`,成功用 `NodeResult::Success(...)`。 +| 项目 | 规则 | +| --- | --- | +| `Run` 签名 | `NodeResult Run(const Inputs&[, const Params&][, const Models&][, const SessionResources&])` | +| 参数顺序 | 依次为 Inputs、Params、Models;Spec 没有 `Parameters<...>` 就不写 Params,没有 `ModelsOf<...>` 就不写 Models;用到会话缓存时再追加 `SessionResources` | +| 返回值 | `NodeResult`;多输出为 `NodeResult`。成功可直接返回结果,失败用 `NodeResult::Failure(...)` | +| 逐项处理 | `MapPayloads(*inputs.input, &Transform)`:`Transform` 接收一条载荷,返回新载荷或 `NodeResult`;失败时诊断指出是哪一条 | +| 生成参数 | 只需生成参数时用 `GenerateParameters(默认 max_tokens)`,`Run` 收到 `const GenerateOptions&` 并传给 `LlmCall::Generate` | ## 常见编译错误对照 | 报错包含 | 原因 | 处理 | | --- | --- | --- | -| `Map function must accept either` | `Transform` 的参数不支持 `const In&`(可再带 `const Params&`) | 改成表中签名 | -| `Map function return payload type must match` | 返回类型与输出批的元素类型不同 | 返回 `Out` 或 `NodeResult` | -| `BuildPrompt must be callable as` / `FormatAnswer must be callable as` | 参数不支持 `const std::string&`(可再带 `const Params&`) | 改成表中签名;函数对象的调用操作符应为 const | -| `BuildPrompt must return` / `FormatAnswer must return` | 返回了非文本类型,例如 `char`、`int` | 返回 `std::string`(或 `const char*`、`std::string_view`),失败时返回 `NodeResult` | -| `Batch Run must be callable as one of` | 参数顺序不对、使用了非 const 引用,或漏写 `const NoParameters&` | 改成表中三种形式之一;函数对象的调用操作符应为 const | -| `Batch Run must return NodeResult` | 直接返回批次,或结果类型与输出声明不匹配 | `return NodeResult::Success(std::move(output));` | +| `Node Run must be callable as` | 参数顺序不对、使用了非 const 引用、多写了 Spec 未声明的 Params/Models(例如 `const NoParameters&`),或传入了类成员函数 | 按上表规则写成普通函数或 lambda;函数对象的调用操作符应为 const | +| `Node Run must return NodeResult` | 结果类型与输出声明不匹配,或 lambda 推导出了普通批次 | 普通函数声明返回 `NodeResult`;lambda 写 `-> NodeResult` | ## 1. 类型端口:这个操作接收什么、产生什么 把一个 Node 看作批量处理函数。输入端口是函数参数,输出端口是返回值;`TextBatch` 表示一组带来源编号的文本,而不是任意可以强制转换的内存。 -在[模板源码](../../dev_support/node_authoring/starter_llm_node.cpp)中,`Input("input")` -和 `Output("output")` 声明逻辑名字和类型。Spec 同时生成运行时绑定与 Definition, +在[模板源码](../../dev_support/node_authoring/starter_llm_node.cpp)中,`Required("input", &Inputs::input)` +和 `PreservedOutput("output", "input")` 声明逻辑名字和类型。Spec 同时生成运行时绑定与 Definition, 业务函数不直接读写 Blackboard。多输入使用 `InputsOf` 绑定普通输入结构, 多输出使用 `OutputsOf` / `Produced` 绑定普通结果结构;类型和逻辑端口只声明一次。 `Optional` 允许不连接端口;如果连接后当前请求仍可缺值,显式使用 `OptionalValue`, -由算法处理这个状态。普通 Map 和保序输出已确定数量与来源,不重复填写 `PortFlow`; +由算法处理这个状态。保序输出已确定数量与来源,不重复填写 `PortFlow`; 只有非默认关系才声明它。可选性独立于数量关系,不另建 flow preset。 有三个名字容易混淆: @@ -78,8 +85,7 @@ Spec 包装读取只读输入,必需值缺失或已连接输入类型不符时 ## 2. 模型绑定:拿到一个已经准备好的模型能力 -你需要的是“生成文本”的能力。`MakeLlmTextSpec` 组合一次模型调用;Batch 通过 -`ModelsOf` 中的 `Model` 声明取得调用门面。成员类型决定能力:`LlmCall`、`EmbeddingCall`、 +你需要的是“生成文本”的能力。`ModelsOf` 中的 `Model` 声明取得调用门面。成员类型决定能力:`LlmCall`、`EmbeddingCall`、 `AsrCall`、`OcrCall`、`RerankCall` 分别提供 `Generate`、`Embed`、`Transcribe`、`Recognize`、`Score`。 它们处理空批次、模型错误诊断及返回数量和来源检查,结果统一为 `NodeResult`。 模型失败直接传播,不为旧节点错误码再做一层映射;宿主收到的返回码由接入适配层按失败阶段映射, @@ -109,9 +115,9 @@ Pipeline 构建期间准备模型资源,作者包装在初始化时取得各 参考 [TextEmbeddingNode](../../src/common_nodes/text_embedding_node.cpp)。 换一个支持相同能力的模型时,通常更新 `models` 配置与 `bind_model` 即可。业务函数是否 -仍适合新模型,要用实际数据确认。轻量模板使用 `GenerateOptions{}` 的默认采样参数; -需要调参时,使用带参数的 Spec / 自由 Batch,在普通函数中构造 options 并传给 `LlmCall`; -用 `Parameters` 的 `Field` 绑定结构成员与配置字段。 +仍适合新模型,要用实际数据确认。LLM 模板用 `GenerateParameters(128)` 声明 `max_tokens`、 +`temperature` 等生成参数,节点配置可以直接调整;还需要自有配置时,参考 +[PromptGuidedLlmNode](../../src/custom_nodes/prompt_guided_llm_node.cpp) 把生成参数放进自己的参数结构。 ## 3. Definition:让连线工具和运行器看懂你的操作 @@ -126,14 +132,12 @@ LLM”。`NodeDefinition` 就是把这些要求写成框架能读取的接口说 | `inputs`、`outputs` | 哪些端口必需、类型是什么、数量与来源如何变化 | | `config_fields` | 接受哪些参数、是否必填、默认值和范围是什么 | | `model_dependencies` | 需要哪些模型能力槽位、哪个字段引用各模型实例 | -| `biz_names` | 是否确有必要限制某些外部业务契约;通常留空便于复用 | -轻量模板只声明必填的 `bind_model`。因此复制完整样例的配置时,需要移除 +LLM 模板声明必填的 `bind_model` 和生成参数。复制完整样例的配置时,需要移除 `prompt_template`、`strip_markdown` 等它没有声明的字段。未知字段被拒绝,能尽早发现 “代码根本没有使用这个配置”的问题。 -所有生产 Node 用 `REGISTER_FUNCTION_NODE` 从 Spec 生成构造方法和说明。Map、Batch、 -LLM 便利组合使用同一契约;`NodeBase` 仅是框架内部运行机制。构建之后,Catalog、 +所有 Node 用 `REGISTER_FUNCTION_NODE` 从 Spec 生成构造方法和说明;`NodeBase` 仅是框架内部运行机制。构建之后,Catalog、 Validator 和 Studio 自动使用注册结果,不需要你再维护 UI 节点列表。 **什么时候需要改 Definition?** 只改提示词构造或输出文本格式、接口保持不变时,通常 @@ -141,7 +145,7 @@ Validator 和 Studio 自动使用注册结果,不需要你再维护 UI 节点 `Parameters` 将字段声明、默认值和结构成员绑定在一处;语义校验用 `Validate` / `ValidateBindings`,由预检与初始化共用。参考 -[自由 Batch starter](../../dev_support/node_authoring/starter_batch_node.cpp)。 +[多输入 starter](../../dev_support/node_authoring/starter_batch_node.cpp)。 Definition 会帮助原生校验发现类型、字段和连线错误,但不会自动实现业务代码。 Validator 根据 Definition 字段列表一次性校验未知字段、类型、范围和枚举,并填入默认值。 @@ -163,7 +167,7 @@ Control 更新单独归一化参数、构造下一状态后发布。 ## 改变数量或顺序时:来源编号 -普通 Map/LLM 模板已自动维护这些编号;过滤、拆分、聚合、重排和多路合并时才需要了解来源关系。 +用 `MapPayloads` 逐项处理时这些编号自动保持;过滤、拆分、聚合、重排和多路合并时才需要了解来源关系。 `TextBatch` 中每一项是 `TraceableItem`,包含三部分: @@ -177,7 +181,7 @@ Control 更新单独归一化参数、构造下一状态后发布。 标记 `(101, 0)`。循环下标 `0`、`1` 只代表本批中的位置,不能拿来替换请求编号。 一个请求拆成多个片段时,可能出现 `(101, 0)`、`(101, 1)`;它们仍属于同一个请求。 -轻量模板自动生成 `1:1` / `preserve` 关系:**输入一项,输出一项,顺序与两个编号都保持一致**。 +`MapPayloads` 与 `PreservedOutput` 对应 `1:1` / `preserve` 关系:**输入一项,输出一项,顺序与两个编号都保持一致**。 在一般的批处理代码中,构造输出的写法是: ```cpp @@ -187,9 +191,9 @@ outputs.emplace_back(item.req_id, item.sub_id, new_value); 模型可能在内部切批或补齐批次,框架的模型执行路径负责处理这些细节。模型调用门面负责验证 返回数量与来源,因为“模型调用返回成功”不能证明每条回答都对应正确请求。 -轻量模板把这项检查留在固定结构中。`BuildPrompt` 和 `FormatAnswer` 只接收文本,不接收 -编号,因此常规业务修改可以集中在载荷上。需要过滤、拆分或聚合时,改用能表达该算法的 -批处理实现,并同步声明数量和来源规则;不能让 1:1 模板悄悄丢掉一条输入。 +LLM 模板中的 `BuildPrompt` 和 `FormatAnswer` 只接收文本,不接收编号,因此常规业务修改可以 +集中在载荷上。需要过滤、拆分或聚合时,在 `Run` 中按算法构造输出,并同步声明数量和来源规则; +不能让保序输出悄悄丢掉一条输入。 ### 数量关系声明与 Validator 检查 @@ -287,7 +291,7 @@ TextEmbedding 会话缓存和两种复杂 Control。无需按场景维护另一 拒绝临时批次;使用期间输入必须存活且不修改、不移动。视图仅用于本次请求内的同步算法, 不能保存到 Node/Session 或异步任务。`Materialize`、Scatter 和 Split 的输出拥有数据。 错误在 AuthorNode 边界统一写入诊断,保留回调错误码、内容及完整来源 key;普通算法不提前 -写 Context。工具要求完整 key 唯一,不改变未使用这些工具的 Map/模型重复 key 行为。 +写 Context。工具要求完整 key 唯一,不改变未使用这些工具的逐项处理/模型重复 key 行为。 先声明结果数量和来源,再编码。例如“两条输入各输出一条”必须保留两组编号;“每个问题 取前三个候选”要按请求分组并声明排名来源,不能用整个 batch 的前三项代替。 diff --git a/doc/dev_guide/first_control.md b/doc/dev_guide/first_control.md index 58005934..da709828 100644 --- a/doc/dev_guide/first_control.md +++ b/doc/dev_guide/first_control.md @@ -42,9 +42,9 @@ hot-swap 声明一致;通常直接复用同一份命令声明。重复使用 | 声明点 | 作用 | | --- | --- | -| `PrefixControlNodeParams` / `Field("prefix", ...)` | 声明业务参数结构体,绑定初值默认空字符串、字段说明与 64 字节业务校验 | +| `Params` / `Field("prefix", ...)` | 声明业务参数结构体,绑定初值默认空字符串、字段说明与 64 字节业务校验 | | `kUpdatePrefix` / `ReplaceFields(...)` | 声明具名命令 ID 与受控字段集合,自动投影 Control payload schema | -| `ApplyPrefix(...)` | 纯业务转换函数,接收普通数据与参数,无需接触锁或平台结构 | +| `Run(...)` | 业务处理函数,用 `MapPayloads` 逐条加前缀;接收普通数据与参数,无需接触锁或平台结构 | | `WithControls(...)` | 将受控命令挂载到 Spec,框架自动管理不可变快照与并发更新事务 | `WithControls` 引用 `ReplaceFields(kUpdatePrefix, "set_prefix", {"prefix"})`,框架复用 `Parameters` 已绑定的字段类型、默认值和业务校验规则自动生成 Control payload schema。初始配置写在节点的 `config`(例如 `{"prefix":"BASE:"}`),未设置时使用默认空字符串;Control 下发新值时通过相同校验规则验证,并通过不可变快照原子发布。非法初始配置会在预检拒绝,直接 Init 也返回具体原因。 diff --git a/doc/dev_guide/first_custom_node.md b/doc/dev_guide/first_custom_node.md index b9849ea5..e9e784bd 100644 --- a/doc/dev_guide/first_custom_node.md +++ b/doc/dev_guide/first_custom_node.md @@ -25,8 +25,9 @@ 调度、模型加载和平台数据拷贝继续由框架承担。Node 返回内部文本,Adapter 在方案执行 完成后将最终值转换到平台结构。你不用在 Node 中操作平台指针或输出池。 -[模板源码](../../dev_support/node_authoring/starter_llm_node.cpp)由两个文本函数、Spec 声明 -和注册宏组成,默认做文本透传和模型调用。脚手架直接使用这份文件, +[模板源码](../../dev_support/node_authoring/starter_llm_node.cpp)由两个文本函数和所有 Node +共用的统一结构(`Inputs`、`Models`、`Run`、`Spec`、注册宏)组成,默认做文本透传和模型调用。 +脚手架直接使用这份文件, 生成的源码会进入现有测试 runner 编译。 先关注 `BuildPrompt` 和 `FormatAnswer` 两个普通函数;其余部分可结合 [按需参考](custom_node_concepts.md)逐步阅读。 @@ -49,12 +50,13 @@ ./tools/scaffold_custom_node.py MyBusinessLlmNode --kind model -m llm --write-test ``` -打开 `src/custom_nodes/my_business_llm_node.cpp`。文件中的主要内容分成三部分: +打开 `src/custom_nodes/my_business_llm_node.cpp`。文件中的主要内容分成四部分: | 位置 | 第一次开发时怎么处理 | | --- | --- | | 顶部 `BuildPrompt` / `FormatAnswer` | 填写业务逻辑:接收一个字符串,返回一个字符串 | -| `MakeLlmTextSpec` | 声明输入输出,组合两个文本函数与一次 LLM 调用 | +| `Inputs` / `Models` / `Run` | 逐条调用 `BuildPrompt`,整批调用一次模型,再逐条调用 `FormatAnswer`;第一次不用改 | +| `Spec` | 用 `MakeNodeSpec` 声明输入输出、生成参数和模型 | | `REGISTER_FUNCTION_NODE` | 从同一 Spec 注册构造方法和 Definition | 两个文本函数是源文件内的普通函数,不需要继承节点类。你可以根据业务继续拆分小函数。 @@ -81,8 +83,9 @@ return answer; 你只处理 `text`;框架自动保留每条输入与回答的对应关系,无需在业务函数中传递或填写来源编号。前处理从只读输入构造新文本,不修改其他节点共享的输入。 这里没有模板语言、参数解析或 Markdown 解析器;需要时再使用已有通用节点。 -需要多个输入、条件二次推理、多种模型能力或拆分聚合时,改用 Batch 写法:三种写法的签名与 -常见编译错误见[写法速查](custom_node_concepts.md#三种写法速查),第 7 节列出了对应的示例。 +需要多个输入、条件二次推理、多种模型能力或拆分聚合时,仍在同一结构里修改 `Inputs`、`Models` +和 `Run`:签名规则与常见编译错误见[结构与签名速查](custom_node_concepts.md#结构与签名速查), +第 7 节列出了对应的示例。 外部请求的字段选择与响应组装属于 Adapter,见[输入输出边界](business_onboarding.md#输入输出以-operator-接口为边界)。 ## 4. 编译,让工具能够找到新节点 @@ -112,8 +115,8 @@ cp demo/fixtures/mock/pipeline_entity_extract_custom.conf demo/fixtures/mock/pip 编辑 `pipeline_first_node.json` 中 `id` 为 `custom_prompt` 的节点,做两处修改: - `node_type` 改成 `MyBusinessLlmNode`。 -- 将整个 `config` 对象替换为下面的内容。轻量节点只声明了模型绑定,不接受完整样例的 - 模板、清洗和采样配置字段。 +- 将整个 `config` 对象替换为下面的内容。新节点声明了模型绑定和 `max_tokens`、`temperature` + 等生成参数(不写时使用默认值),不接受完整样例的模板和清洗字段。 ```json {"bind_model": "entity_llm"} @@ -172,7 +175,7 @@ flowchart LR | 接下来遇到的问题 | 去哪里看 | | --- | --- | | 端口、编号、模型绑定、Definition、并发是什么意思 | [按需参考](custom_node_concepts.md) | -| 需要多个输入或条件重试 | [自由 Batch starter](../../dev_support/node_authoring/starter_batch_node.cpp) | +| 需要多个输入或条件重试 | [多输入 starter](../../dev_support/node_authoring/starter_batch_node.cpp) | | 需要 LLM 与 Embedding 两种能力 | [多模型 starter](../../dev_support/node_authoring/starter_multi_model_node.cpp) | | 需要配置化模板或复杂后处理 | [复杂算法的组织与现有辅助函数](custom_node_concepts.md#复杂算法仍按普通-c-函数组织) | | 需要接入全新的平台结构 | [业务接入指南](business_onboarding.md) | diff --git a/doc/dev_guide/recipe_text_llm_node.md b/doc/dev_guide/recipe_text_llm_node.md index e0cb9215..b453aa20 100644 --- a/doc/dev_guide/recipe_text_llm_node.md +++ b/doc/dev_guide/recipe_text_llm_node.md @@ -24,7 +24,9 @@ python3 tools/dev_recipe.py prepare text-llm-node \ 源 Node 的逻辑端口(如 prompt/text)会映射为新模板的 input/output,实际键保持不变。 新模板不会自动复制被替换 Custom Node 的业务算法;替换自带提示词构造或结果加工的节点时, -应在新的业务函数中实现所需行为,效果验收会检测行为差异。 +应在新的业务函数中实现所需行为,效果验收会检测行为差异。新节点的配置只写 `bind_model`, +`max_tokens`、`temperature` 等生成参数使用默认值;源节点调过的采样字段不会复制,需要时在 +方案配置中补上。 ## 编辑与验证 diff --git a/doc/developer_guide.md b/doc/developer_guide.md index 8ca5be2e..b3220443 100644 --- a/doc/developer_guide.md +++ b/doc/developer_guide.md @@ -24,7 +24,7 @@ Backend;出现调度、模型语义或硬件能力缺口时,再查阅相应 | :--- | :--- | :--- | :--- | | **接入适配层(Integration)** | 新增输入/输出结构、转换器与业务绑定 | `include/platform_mock/operator_data_types.h`
`src/adapter/input/_input.cpp`
`src/adapter/output/_output.cpp`
`src/adapter/biz/_bindings.cpp` | `InputConverterDefinition`
`OutputConverterDefinition`
`IoBindingDefinition`
`REGISTER_INPUT_CONVERTER`
`REGISTER_OUTPUT_CONVERTER`
`REGISTER_IO_BINDING` | | **流程编排层(Orchestration)** | 扩展动态黑板、会话模型管理与全局资源 | `include/core/alg_context.h`
`include/core/session_context.h` | `AlgContext::Read/Publish`
`SessionResourceKey` | -| **能力节点层(Capability Nodes)** | 新增通用操作或可跨方案复用的领域算法 | `src/common_nodes/*.cpp`
`src/custom_nodes/*.cpp`
`include/nodes/*.h` | `MakeBatchSpec` / `MakeMapSpec`
`REGISTER_FUNCTION_NODE(NodeName, spec)` | +| **能力节点层(Capability Nodes)** | 新增通用操作或可跨方案复用的领域算法 | `src/common_nodes/*.cpp`
`src/custom_nodes/*.cpp`
`include/nodes/*.h` | `MakeNodeSpec`
`REGISTER_FUNCTION_NODE(NodeName, spec)` | | **模型执行层(Model Execution)** | 新增模型语义或接入新推理后端 | `include/engine/model_interface.h`
`include/engine/backend_interface.h`
`src/engine/models/`
`src/engine/backends/` | `REGISTER_MODEL_WITH_DEFINITION`
`REGISTER_BACKEND_WITH_DEFINITION`
`ModelRuntimeFactory`
`FixedBatchExecutor` | --- @@ -133,18 +133,18 @@ typed port 契约时才新增 Node。Node 必须: - 使用 `REGISTER_FUNCTION_NODE`,由同一 Spec 生成完整 `NodeDefinition` 并注册; - 在 Catalog 可见,并覆盖非法配置、端口缺失/类型错误、输出、provenance 和并发声明。 -入门默认使用[轻量 LLM 模板](../dev_support/node_authoring/starter_llm_node.cpp): -脚手架生成后,先编写 `BuildPrompt` 和 `FormatAnswer` 两个普通文本函数;端口与来源 -处理保留在固定结构中。完整步骤见[第一个自定义 Node](dev_guide/first_custom_node.md)。 +入门默认使用 [LLM 模板](../dev_support/node_authoring/starter_llm_node.cpp): +脚手架生成后,先编写 `BuildPrompt` 和 `FormatAnswer` 两个普通文本函数;端口、来源与生成参数 +处理保留在所有 Node 共用的统一结构中。完整步骤见[第一个自定义 Node](dev_guide/first_custom_node.md)。 熟悉基本流程后,以 [`llm_generate_node.cpp`](../src/common_nodes/llm_generate_node.cpp)、 [`text_rerank_node.cpp`](../src/common_nodes/text_rerank_node.cpp) 及其同名测试为当前模板。 -组合 LLM 采样参数时可复用 [`GenerateOptionsFields` / `ParseGenerateOptions`](../include/nodes/generate_options_config.h), +LLM 采样参数复用 [`GenerateParameters` / `GenerateOptionsFields`](../include/nodes/generate_options_config.h), 显式指定该节点的 `max_tokens` 默认值,其余字段约束与解析共用同一实现。 -自定义 Node 可以在一次处理内完成前处理、调用声明绑定的模型和后处理,沿用现有 -`MakeBatchSpec`,无需新增专属基类。Spec 默认 `category = "custom"`;仅在存在 -真实业务契约限制时设置 `biz_names`。平台结构转换留在 Adapter,Core、Engine 和通用 +自定义 Node 可以在一次处理内完成前处理、调用声明绑定的模型和后处理,与所有 Node 一样 +使用 `MakeNodeSpec`,无需新增专属基类。Spec 默认 `category = "custom"`;Node 不绑定特定业务。 +平台结构转换留在 Adapter,Core、Engine 和通用 Node 不依赖自定义实现。编写、构建和复用步骤见 [自定义 Node 接入指南](../src/custom_nodes/README.md)。 diff --git a/include/core/diagnostic_code.h b/include/core/diagnostic_code.h index afff0ab5..c7a85f5c 100644 --- a/include/core/diagnostic_code.h +++ b/include/core/diagnostic_code.h @@ -32,7 +32,6 @@ namespace llm_edgeflow { X(kConfigFieldRange, "CONFIG_FIELD_RANGE") \ X(kConfigFieldEnum, "CONFIG_FIELD_ENUM") \ X(kUnknownModelReference, "UNKNOWN_MODEL_REFERENCE") \ - X(kNodeBizMismatch, "NODE_BIZ_MISMATCH") \ X(kMissingInputProducer, "MISSING_INPUT_PRODUCER") \ X(kDuplicatePortProducer, "DUPLICATE_PORT_PRODUCER") \ X(kMissingBizOutput, "MISSING_BIZ_OUTPUT") \ diff --git a/include/core/node_definition.h b/include/core/node_definition.h index e7fe0f2d..49cebdf1 100644 --- a/include/core/node_definition.h +++ b/include/core/node_definition.h @@ -84,7 +84,6 @@ struct NodeDefinition { NodeConfigValidator validate_config; std::vector model_dependencies; bool parallel_safe = false; - std::vector biz_names; }; const char* PortConstraintKindName(PortConstraintKind kind); diff --git a/include/nodes/authoring.h b/include/nodes/authoring.h index 019b3cb1..1c594038 100644 --- a/include/nodes/authoring.h +++ b/include/nodes/authoring.h @@ -5,6 +5,7 @@ #include "nodes/configuration_snapshot.h" #include "nodes/control_authoring.h" #include "nodes/function_node.h" +#include "nodes/generate_options_config.h" #include "nodes/model_calls.h" #include "nodes/node_error_codes.h" #include "nodes/node_result.h" diff --git a/include/nodes/function_node.h b/include/nodes/function_node.h index 310d4c5c..fd784fe0 100644 --- a/include/nodes/function_node.h +++ b/include/nodes/function_node.h @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -33,48 +32,6 @@ namespace llm_edgeflow { -template -struct BatchItemTraits { - static constexpr bool kIsTraceableBatch = false; - using PayloadType = void; - using ItemType = void; - using BatchType = T; -}; - -template -struct BatchItemTraits>> { - static constexpr bool kIsTraceableBatch = true; - using PayloadType = PayloadT; - using ItemType = TraceableItem; - using BatchType = std::vector>; -}; - -template <> -struct BatchItemTraits - : BatchItemTraits>> {}; - -template -struct Input { - static_assert(BatchItemTraits::kIsTraceableBatch, - "Input batch type must be a vector of TraceableItem"); - using BatchType = BatchT; - using PayloadType = typename BatchItemTraits::PayloadType; - - std::string name; - explicit Input(std::string port_name) : name(std::move(port_name)) {} -}; - -template -struct Output { - static_assert(BatchItemTraits::kIsTraceableBatch, - "Output batch type must be a vector of TraceableItem"); - using BatchType = BatchT; - using PayloadType = typename BatchItemTraits::PayloadType; - - std::string name; - explicit Output(std::string port_name) : name(std::move(port_name)) {} -}; - template struct IsNodeResult : std::false_type { using ValueType = T; @@ -87,280 +44,72 @@ struct IsNodeResult> : std::true_type { namespace detail { -template -auto InvokeMapItem(const Fn& fn, const InT& item, const ParamsT& params) { - if constexpr (std::is_invocable_v) { - return fn(item, params); - } else if constexpr (std::is_invocable_v) { - return fn(item); +template +inline constexpr bool kHasParameters = !std::is_same_v; +template +inline constexpr bool kHasModels = !std::is_same_v; + +// Run 的参数依次为 Inputs、Params、Models,只包含 Spec 实际声明的部分; +// 需要会话缓存时再追加 SessionResources。 +template +inline constexpr bool kRunInvocable = [] { + if constexpr (kHasParameters && kHasModels) { + return std::is_invocable_v; + } else if constexpr (kHasParameters) { + return std::is_invocable_v; + } else if constexpr (kHasModels) { + return std::is_invocable_v; } else { - static_assert( - std::is_invocable_v || - std::is_invocable_v, - "Map function must accept either (const InPayload&, const Params&) " - "or (const InPayload&)"); + return std::is_invocable_v; + } +}(); + +template +auto CallRun(const Fn& fn, const InputsT& inputs, const ParamsT& params, + const ModelsT& models, const Tail&... tail) { + if constexpr (kHasParameters && kHasModels) { + return fn(inputs, params, models, tail...); + } else if constexpr (kHasParameters) { + return fn(inputs, params, tail...); + } else if constexpr (kHasModels) { + return fn(inputs, models, tail...); + } else { + return fn(inputs, tail...); } } -template -struct MemberFunctionTraits; - -template -struct MemberFunctionTraits { - using ClassType = Class; - using ReturnType = Ret; -}; - -template -struct MemberFunctionTraits { - using ClassType = Class; - using ReturnType = Ret; -}; - template -auto InvokeBatch(const Fn& fn, const InputsT& inputs, const ParamsT& params, - const ModelsT& models, const SessionResources& resources) { - if constexpr (std::is_member_function_pointer_v) { - using ClassT = typename MemberFunctionTraits::ClassType; - ClassT logic{}; - if constexpr (std::is_invocable_v) { - return (logic.*fn)(inputs, params, models, resources); - } else if constexpr (std::is_invocable_v) { - return (logic.*fn)(inputs, params, models); - } else if constexpr (std::is_invocable_v) { - return (logic.*fn)(inputs, params); - } - } else { - if constexpr (std::is_invocable_v) { - return fn(inputs, params, models, resources); - } else if constexpr (std::is_invocable_v) { - return fn(inputs, params, models); - } else if constexpr (std::is_invocable_v) { - return fn(inputs, params); - } +auto InvokeRun(const Fn& fn, const InputsT& inputs, const ParamsT& params, + const ModelsT& models, const SessionResources& resources) { + if constexpr (kRunInvocable) { + return CallRun(fn, inputs, params, models, resources); + } else if constexpr (kRunInvocable) { + return CallRun(fn, inputs, params, models); } } template -struct BatchRunSignature { - static constexpr bool kCallable = [] { - if constexpr (std::is_member_function_pointer_v) { - using ClassT = typename MemberFunctionTraits::ClassType; - return std::is_invocable_v || - std::is_invocable_v || - std::is_invocable_v; - } else { - return std::is_invocable_v || - std::is_invocable_v || - std::is_invocable_v; - } - }(); - // 无签名匹配时 InvokeBatch 返回 void,且不会实例化非法调用。 - // Result 以其分派顺序为准。 - using Result = decltype(InvokeBatch( +struct RunSignature { + static constexpr bool kCallable = + kRunInvocable || + kRunInvocable; + // 无签名匹配时 InvokeRun 返回 void,且不会实例化非法调用。 + using Result = decltype(InvokeRun( std::declval(), std::declval(), std::declval(), std::declval(), std::declval())); }; -template -auto InvokeTextHook(const Fn& fn, const std::string& text, - const ParamsT& params) { - if constexpr (std::is_invocable_v) { - return fn(text, params); - } else if constexpr (std::is_invocable_v) { - return fn(text); - } -} - -// 两个 LLM 钩子都接受可隐式转换为 string 的值和显式的 string_view 拷贝。 -// 算术结果绝不能被静默截断为字符。 -template -inline constexpr bool kIsHookText = - std::is_convertible_v || - std::is_same_v, std::string_view>; - -template -std::string ToHookText(R&& value) { - return std::string(std::forward(value)); -} - -template -struct TextHookSignature { - static constexpr bool kCallable = - std::is_invocable_v || - std::is_invocable_v; - using Result = decltype(InvokeTextHook(std::declval(), - std::declval(), - std::declval())); - using Payload = typename IsNodeResult>::ValueType; - static constexpr bool kReturnsText = kIsHookText; -}; - } // namespace detail // --------------------------------------------------------------------------- -// Map 入口 -// --------------------------------------------------------------------------- - -template -class MapSpec { - public: - using InputBatch = InputBatchT; - using OutputBatch = OutputBatchT; - using ParametersType = ParamsT; - using InPayload = typename BatchItemTraits::PayloadType; - using OutPayload = typename BatchItemTraits::PayloadType; - using RawResult = decltype(detail::InvokeMapItem( - std::declval(), std::declval(), - std::declval())); - using UnwrappedResult = typename IsNodeResult::ValueType; - static constexpr bool kReturnsNodeResult = IsNodeResult::value; - - static_assert(std::is_same_v, - "Map function return payload type must match OutputBatch " - "element payload type"); - - MapSpec(Input in, Output out, - Parameters params, MapFnT fn) - : in_(std::move(in)), - out_(std::move(out)), - params_(std::move(params)), - fn_(std::move(fn)) {} - - MapSpec& Description(std::string desc) & { - description_ = std::move(desc); - return *this; - } - MapSpec Description(std::string desc) && { - description_ = std::move(desc); - return std::move(*this); - } - - MapSpec& Category(std::string cat) & { - category_ = std::move(cat); - return *this; - } - MapSpec Category(std::string cat) && { - category_ = std::move(cat); - return std::move(*this); - } - - MapSpec& ParallelSafe(bool safe) & { - parallel_safe_ = safe; - return *this; - } - MapSpec ParallelSafe(bool safe) && { - parallel_safe_ = safe; - return std::move(*this); - } - - MapSpec& BizNames(std::vector biz_names) { - biz_names_ = std::move(biz_names); - return *this; - } - - MapSpec WithControls(std::vector commands) && { - static_assert(std::is_copy_constructible_v, - "WithControls requires copy-constructible ParametersType"); - ValidateControlCommands(commands, params_); - control_commands_ = std::move(commands); - return std::move(*this); - } - - MapSpec WithControls(std::initializer_list commands) && { - return std::move(*this).WithControls( - std::vector(commands)); - } - - bool HasControls() const noexcept { return !control_commands_.empty(); } - - const std::vector& ControlCommands() const noexcept { - return control_commands_; - } - - const std::string& InputName() const noexcept { return in_.name; } - const std::string& OutputName() const noexcept { return out_.name; } - const Parameters& ParametersSpec() const noexcept { return params_; } - const MapFnT& Function() const& noexcept { return fn_; } - MapFnT&& Function() && noexcept { return std::move(fn_); } - - NodeDefinition BuildDefinition(std::string node_type) const { - NodeDefinition def; - def.node_type = std::move(node_type); - def.category = category_; - def.description = description_; - def.parallel_safe = parallel_safe_; - def.biz_names = biz_names_; - def.inputs = {NodePortDefinition{ - in_.name, BlackboardTypeTraits::TypeName(), true, "1:1", - "preserve", "request"}}; - def.outputs = {NodePortDefinition{ - out_.name, BlackboardTypeTraits::TypeName(), true, "1:1", - "preserve", "request"}}; - def.config_fields = params_.Fields(); - def.validate_config = [params = params_]( - const nlohmann::json& cfg, - const std::unordered_set& conn, - std::string* err) { - return params.ValidateWithBindings(cfg, conn, err); - }; - for (const auto& cmd : control_commands_) { - def.control_commands.push_back(cmd.ToCommandDefinition(params_)); - } - return def; - } - - private: - Input in_; - Output out_; - Parameters params_; - MapFnT fn_; - std::vector control_commands_; - std::string category_ = "custom"; - std::string description_; - bool parallel_safe_ = false; - std::vector biz_names_; -}; - -template -inline auto MakeMapSpec(Input in, Output out, - Parameters params, MapFnT fn) { - return MapSpec( - std::move(in), std::move(out), std::move(params), std::move(fn)); -} - -template -inline auto MakeMapSpec(Input in, Output out, - NoParameters, MapFnT fn) { - return MapSpec( - std::move(in), std::move(out), Parameters{}, std::move(fn)); -} - -template -inline auto MakeMapSpec(Input in, Output out, - MapFnT fn) { - return MapSpec( - std::move(in), std::move(out), Parameters{}, std::move(fn)); -} - -// --------------------------------------------------------------------------- -// Batch 入口 +// Node Spec // --------------------------------------------------------------------------- enum class InputFlow { @@ -1026,17 +775,6 @@ inline ModelSlotBindingHolder Model(std::string slot, std::move(slot), std::move(field), member, std::move(description))); } -template -inline ModelSlotBindingHolder Llm(std::string slot, std::string field, - LlmCall ModelsT::*member) { - return Model(std::move(slot), std::move(field), member); -} -template -inline ModelSlotBindingHolder Embedding( - std::string slot, std::string field, EmbeddingCall ModelsT::*member) { - return Model(std::move(slot), std::move(field), member); -} - template class ModelsOf { public: @@ -1126,7 +864,7 @@ class ModelsOf { template -class BatchSpec { +class NodeSpec { public: using InputsType = InputsT; using OutputBatch = OutputBatchT; @@ -1134,8 +872,7 @@ class BatchSpec { using ModelsType = ModelsT; using RunFunctionType = RunFnT; - using RunSignature = - detail::BatchRunSignature; + using RunSignature = detail::RunSignature; using RunResult = std::decay_t; static constexpr bool kRunResultValid = IsNodeResult::value && @@ -1144,20 +881,17 @@ class BatchSpec { static constexpr bool kRunSignatureValid = RunSignature::kCallable && kRunResultValid; static_assert(RunSignature::kCallable, - "Batch Run must be callable as one of: " - "NodeResult Run(const Inputs&, const Params&) | " - "NodeResult Run(const Inputs&, const Params&, " - "const Models&) | " - "NodeResult Run(const Inputs&, const Params&, " - "const Models&, " - "const SessionResources&). Without Parameters<...>, Params is " - "NoParameters. See doc/dev_guide/custom_node_concepts.md"); + "Node Run must be callable as NodeResult " + "Run(const Inputs&[, const Params&][, const Models&][, const " + "SessionResources&]); write Params only with Parameters<...> " + "and Models only with ModelsOf<...>. See " + "doc/dev_guide/custom_node_concepts.md"); static_assert(!RunSignature::kCallable || kRunResultValid, - "Batch Run must return NodeResult " + "Node Run must return NodeResult " "(NodeResult for multiple outputs)"); - BatchSpec(InputsOf inputs, OutputsOf output, - Parameters params, ModelsOf models, RunFnT fn) + NodeSpec(InputsOf inputs, OutputsOf output, + Parameters params, ModelsOf models, RunFnT fn) : inputs_(std::move(inputs)), output_(std::move(output)), params_(std::move(params)), @@ -1166,43 +900,34 @@ class BatchSpec { output_.CheckAnchors(inputs_); } - BatchSpec& Description(std::string desc) & { + NodeSpec& Description(std::string desc) & { description_ = std::move(desc); return *this; } - BatchSpec Description(std::string desc) && { + NodeSpec Description(std::string desc) && { description_ = std::move(desc); return std::move(*this); } - BatchSpec& Category(std::string cat) & { + NodeSpec& Category(std::string cat) & { category_ = std::move(cat); return *this; } - BatchSpec Category(std::string cat) && { + NodeSpec Category(std::string cat) && { category_ = std::move(cat); return std::move(*this); } - BatchSpec& ParallelSafe(bool safe) & { + NodeSpec& ParallelSafe(bool safe) & { parallel_safe_ = safe; return *this; } - BatchSpec ParallelSafe(bool safe) && { + NodeSpec ParallelSafe(bool safe) && { parallel_safe_ = safe; return std::move(*this); } - BatchSpec& BizNames(std::vector biz_names) & { - biz_names_ = std::move(biz_names); - return *this; - } - BatchSpec BizNames(std::vector biz_names) && { - biz_names_ = std::move(biz_names); - return std::move(*this); - } - - BatchSpec WithControls(std::vector commands) && { + NodeSpec WithControls(std::vector commands) && { static_assert(std::is_copy_constructible_v, "WithControls requires copy-constructible ParametersType"); ValidateControlCommands(commands, params_, &models_); @@ -1216,24 +941,24 @@ class BatchSpec { return std::move(*this); } - BatchSpec WithControls( + NodeSpec WithControls( std::initializer_list commands) && { return std::move(*this).WithControls( std::vector(commands)); } - BatchSpec& PortConstraints(std::vector constraints) & { + NodeSpec& PortConstraints(std::vector constraints) & { port_constraints_ = std::move(constraints); return *this; } - BatchSpec PortConstraints(std::vector constraints) && { + NodeSpec PortConstraints(std::vector constraints) && { port_constraints_ = std::move(constraints); return std::move(*this); } using ControlUpdater = std::function( const ParamsT&, const nlohmann::json&, const BindingFacts&)>; - BatchSpec WithControl(ControlCommandDefinition definition, - ControlUpdater update) && { + NodeSpec WithControl(ControlCommandDefinition definition, + ControlUpdater update) && { for (const auto& cmd : control_commands_) { if (cmd.Id() == definition.cmd_id) throw std::invalid_argument("Duplicate Control command"); @@ -1283,7 +1008,6 @@ class BatchSpec { def.category = category_; def.description = description_; def.parallel_safe = parallel_safe_; - def.biz_names = biz_names_; def.inputs = inputs_.ToPortDefinitions(); def.outputs = output_.ToPortDefinitions(); def.port_constraints = port_constraints_; @@ -1326,63 +1050,61 @@ class BatchSpec { std::string category_ = "custom"; std::string description_; bool parallel_safe_ = false; - std::vector biz_names_; }; -// Batch 重载 +// Parameters 与 ModelsOf 可省略;省略的部分也不出现在 Run 的参数中。 template -inline auto MakeBatchSpec(InputsOf inputs, - OutputsOf output, - Parameters params, ModelsOf models, - RunFnT fn) { - return BatchSpec( +inline auto MakeNodeSpec(InputsOf inputs, + OutputsOf output, + Parameters params, ModelsOf models, + RunFnT fn) { + return NodeSpec( std::move(inputs), std::move(output), std::move(params), std::move(models), std::move(fn)); } template -inline auto MakeBatchSpec(InputsOf inputs, - OutputsOf output, - ModelsOf models, RunFnT fn) { - return BatchSpec( +inline auto MakeNodeSpec(InputsOf inputs, + OutputsOf output, + ModelsOf models, RunFnT fn) { + return NodeSpec( std::move(inputs), std::move(output), Parameters{}, std::move(models), std::move(fn)); } template -inline auto MakeBatchSpec(InputsOf inputs, - OutputsOf output, - Parameters params, RunFnT fn) { - return BatchSpec( +inline auto MakeNodeSpec(InputsOf inputs, + OutputsOf output, + Parameters params, RunFnT fn) { + return NodeSpec( std::move(inputs), std::move(output), std::move(params), ModelsOf{}, std::move(fn)); } template -inline auto MakeBatchSpec(InputsOf inputs, - OutputsOf output, RunFnT fn) { - return BatchSpec( +inline auto MakeNodeSpec(InputsOf inputs, + OutputsOf output, RunFnT fn) { + return NodeSpec( std::move(inputs), std::move(output), Parameters{}, ModelsOf{}, std::move(fn)); } // --------------------------------------------------------------------------- -// AuthorNode 定义:同时支持 MapSpec 和 BatchSpec +// AuthorNode:由 NodeSpec 生成 NodeBase 运行时 // --------------------------------------------------------------------------- -template +template class AuthorNode; -// BatchSpec 特化 template -class AuthorNode> +class AuthorNode> : public NodeBase { public: - using SpecType = BatchSpec; + using SpecType = NodeSpec; AuthorNode(std::string node_name, SpecType spec) : NodeBase(std::move(node_name)), spec_(std::move(spec)) {} @@ -1473,8 +1195,8 @@ class AuthorNode> params_ptr = ¶meters_; } - auto res = detail::InvokeBatch(spec_.Function(), inputs, *params_ptr, - models_, resources_); + auto res = detail::InvokeRun(spec_.Function(), inputs, *params_ptr, + models_, resources_); if (!res.ok()) { auto failure = std::move(res).ExtractFailure(); int code = failure.cause_code != 0 @@ -1497,7 +1219,7 @@ class AuthorNode> } else { // 合法程序中不可达;避免引发连锁模板错误。 return this->Fail(req_ctx, node_error::author_node::kInternalError, - "Invalid Batch Run signature"); + "Invalid Node Run signature"); } } @@ -1510,221 +1232,6 @@ class AuthorNode> BindingFacts binding_facts_; }; -namespace detail { - -template -struct MapInputs { - const InputBatchT* items = nullptr; -}; - -// 在 Batch 运行时上执行 Map:一个必需的锚点输入、一个保序输出, -// 以及能指出失败条目的逐条循环。 -template -auto MakeMapRuntimeSpec(std::string node_name, SpecT map) { - using InputBatch = typename SpecT::InputBatch; - using OutputBatch = typename SpecT::OutputBatch; - using ParamsT = typename SpecT::ParametersType; - using Inputs = MapInputs; - auto run = [fn = std::move(map).Function(), name = std::move(node_name)]( - const Inputs& inputs, - const ParamsT& params) -> NodeResult { - OutputBatch outputs; - outputs.reserve(inputs.items->size()); - for (const auto& item : *inputs.items) { - if constexpr (SpecT::kReturnsNodeResult) { - auto res = InvokeMapItem(fn, item.data, params); - if (!res.ok()) { - auto failure = std::move(res).ExtractFailure(); - if (!failure.batch_detail.has_value()) { - failure.batch_detail = - BatchFailureDetail{name, BatchFailureReason::kCallbackFailed, - TraceableItemKey{item.req_id, item.sub_id}}; - } - if (failure.message.empty()) { - failure.message = name + " map function failed"; - } - return NodeResult::Failure(std::move(failure)); - } - outputs.emplace_back(item.req_id, item.sub_id, std::move(res).value()); - } else { - outputs.emplace_back(item.req_id, item.sub_id, - InvokeMapItem(fn, item.data, params)); - } - } - return NodeResult::Success(std::move(outputs)); - }; - auto batch = MakeBatchSpec( - InputsOf({Required(map.InputName(), &Inputs::items)}), - PreservedOutput(map.OutputName(), map.InputName()), - map.ParametersSpec(), std::move(run)); - if constexpr (std::is_copy_constructible_v) { - if (map.HasControls()) { - return std::move(batch).WithControls(map.ControlCommands()); - } - } - return batch; -} - -} // namespace detail - -// MapSpec 特化:Definition 来自 MapSpec;执行、参数和 Control 共用 -// Batch 运行时。 -template -class AuthorNode> - : public AuthorNode(), - std::declval< - MapSpec>()))> { - public: - using SpecType = MapSpec; - using RuntimeSpec = decltype(detail::MakeMapRuntimeSpec( - std::declval(), std::declval())); - - AuthorNode(std::string node_name, SpecType spec) - : AuthorNode( - node_name, detail::MakeMapRuntimeSpec(node_name, std::move(spec))) { - } -}; - -// --------------------------------------------------------------------------- -// LLM 文本快捷 Spec 工厂 -// --------------------------------------------------------------------------- - -struct LlmTextInputs { - const TextBatch* prompt = nullptr; -}; - -struct LlmTextModels { - LlmCall generator; -}; - -template -inline auto MakeLlmTextSpec(Input in_port, - Output out_port, - Parameters params, - BuildPromptFn build_prompt, - FormatAnswerFn format_answer, - GenerateOptions options = GenerateOptions{}) { - using PromptSignature = detail::TextHookSignature; - using AnswerSignature = detail::TextHookSignature; - static_assert( - PromptSignature::kCallable, - "BuildPrompt must be callable as std::string BuildPrompt(const " - "std::string&) or std::string BuildPrompt(const std::string&, const " - "Params&)"); - static_assert( - !PromptSignature::kCallable || PromptSignature::kReturnsText, - "BuildPrompt must return text: std::string, const char*, " - "std::string_view, " - "or NodeResult of one of them; char and integer results are rejected"); - static_assert( - AnswerSignature::kCallable, - "FormatAnswer must be callable as std::string FormatAnswer(const " - "std::string&) or std::string FormatAnswer(const std::string&, const " - "Params&)"); - static_assert( - !AnswerSignature::kCallable || AnswerSignature::kReturnsText, - "FormatAnswer must return text: std::string, const char*, " - "std::string_view, " - "or NodeResult of one of them; char and integer results are rejected"); - - auto run_fn = [build_prompt = std::move(build_prompt), - format_answer = std::move(format_answer), - options = std::move(options)]( - const LlmTextInputs& inputs, const ParamsT& parameters, - const LlmTextModels& models) -> NodeResult { - if constexpr (PromptSignature::kCallable && PromptSignature::kReturnsText && - AnswerSignature::kCallable && AnswerSignature::kReturnsText) { - if (!inputs.prompt || inputs.prompt->empty()) { - return NodeResult::Success(TextBatch{}); - } - - TextBatch prompts; - prompts.reserve(inputs.prompt->size()); - for (const auto& item : *inputs.prompt) { - auto res = detail::InvokeTextHook(build_prompt, item.data, parameters); - if constexpr (IsNodeResult::value) { - if (!res.ok()) { - return NodeResult::Failure( - std::move(res).ExtractFailure()); - } - prompts.emplace_back(item.req_id, item.sub_id, - detail::ToHookText(std::move(res).value())); - } else { - prompts.emplace_back(item.req_id, item.sub_id, - detail::ToHookText(std::move(res))); - } - } - - auto llm_res = models.generator.Generate(prompts, options); - if (!llm_res.ok()) return llm_res; - - // 赋值给自有输出前先复制 view,包括指向 item.data 自身的 view。 - // 钩子失败时不发布部分输出。 - auto outputs = std::move(llm_res).value(); - for (auto& item : outputs) { - auto res = detail::InvokeTextHook(format_answer, - std::as_const(item.data), parameters); - if constexpr (IsNodeResult::value) { - if (!res.ok()) { - return NodeResult::Failure( - std::move(res).ExtractFailure()); - } - item.data = detail::ToHookText(std::move(res).value()); - } else { - item.data = detail::ToHookText(std::move(res)); - } - } - return NodeResult::Success(std::move(outputs)); - } else { - return NodeResult::Failure(NodeErrorKind::kInternalError, - "Invalid LLM hook signature"); - } - }; - - return MakeBatchSpec( - InputsOf({ - Required(in_port.name, &LlmTextInputs::prompt), - }), - PreservedOutput(out_port.name, in_port.name), - std::move(params), - ModelsOf({ - Llm("generator", "bind_model", &LlmTextModels::generator), - }), - std::move(run_fn)); -} - -template -inline auto MakeLlmTextSpec(Parameters params, - BuildPromptFn build_prompt, - FormatAnswerFn format_answer, - GenerateOptions options = GenerateOptions{}) { - return MakeLlmTextSpec(Input("prompt"), Output("text"), - std::move(params), std::move(build_prompt), - std::move(format_answer), std::move(options)); -} - -template -inline auto MakeLlmTextSpec(BuildPromptFn build_prompt, - FormatAnswerFn format_answer, - GenerateOptions options = GenerateOptions{}) { - return MakeLlmTextSpec(Input("prompt"), Output("text"), - Parameters{}, std::move(build_prompt), - std::move(format_answer), std::move(options)); -} - -template -inline auto MakeLlmTextSpec(Input in_port, - Output out_port, - BuildPromptFn build_prompt, - FormatAnswerFn format_answer, - GenerateOptions options = GenerateOptions{}) { - return MakeLlmTextSpec(std::move(in_port), std::move(out_port), - Parameters{}, std::move(build_prompt), - std::move(format_answer), std::move(options)); -} - #define REGISTER_FUNCTION_NODE(NodeType, ...) \ struct NodeType final : public ::llm_edgeflow::AuthorNode< \ std::decay_t> { \ diff --git a/include/nodes/generate_options_config.h b/include/nodes/generate_options_config.h index c81c3be2..8c07eaf1 100644 --- a/include/nodes/generate_options_config.h +++ b/include/nodes/generate_options_config.h @@ -9,6 +9,7 @@ #include "contracts/config_schema.h" #include "contracts/inference_payloads.h" +#include "nodes/parameter_binding.h" namespace llm_edgeflow { @@ -104,4 +105,12 @@ inline bool ParseGenerateOptions(const nlohmann::json& config, } } +// 参数只有生成选项的 LLM Node 使用:生成参数由节点配置提供。 +inline Parameters GenerateParameters(int default_max_tokens) { + Parameters params; + params.WithParser(NodeConfigParser( + GenerateOptionsFields(default_max_tokens), ParseGenerateOptions)); + return params; +} + } // namespace llm_edgeflow diff --git a/include/nodes/traceable_algorithms.h b/include/nodes/traceable_algorithms.h index 155c29e6..6f705129 100644 --- a/include/nodes/traceable_algorithms.h +++ b/include/nodes/traceable_algorithms.h @@ -33,7 +33,14 @@ auto MapPayloads(const std::vector>& input, MapFn&& fn) { for (const auto& item : input) { auto res = fn(item.data); if (!res.ok()) { - return NodeResult::Failure(std::move(res).ExtractFailure()); + // 诊断指出失败的条目;回调已给出的结构化细节优先。 + auto failure = std::move(res).ExtractFailure(); + if (!failure.batch_detail.has_value()) { + failure.batch_detail = BatchFailureDetail{ + "MapPayloads", BatchFailureReason::kCallbackFailed, + TraceableItemKey{item.req_id, item.sub_id}}; + } + return NodeResult::Failure(std::move(failure)); } outputs.emplace_back(item.req_id, item.sub_id, std::move(res).value()); } diff --git a/scripts/check_governance.sh b/scripts/check_governance.sh index 55ea253e..e0f2a2cc 100755 --- a/scripts/check_governance.sh +++ b/scripts/check_governance.sh @@ -13,9 +13,7 @@ required_skills=( ".agents/skills/pipeline-composer/SKILL.md" ".agents/skills/json-prompt-solution/SKILL.md" ".agents/skills/edgeflow-adapter-developer/SKILL.md" - ".agents/skills/edgeflow-node-map-developer/SKILL.md" - ".agents/skills/edgeflow-node-llm-developer/SKILL.md" - ".agents/skills/edgeflow-node-batch-developer/SKILL.md" + ".agents/skills/edgeflow-node-developer/SKILL.md" ".agents/skills/edgeflow-model-developer/SKILL.md" ".agents/skills/edgeflow-backend-developer/SKILL.md" ".agents/skills/llm-edgeflow-developer-guide/SKILL.md" diff --git a/src/common_nodes/asr_transcribe_node.cpp b/src/common_nodes/asr_transcribe_node.cpp index 826a1386..4b69873c 100644 --- a/src/common_nodes/asr_transcribe_node.cpp +++ b/src/common_nodes/asr_transcribe_node.cpp @@ -9,25 +9,23 @@ struct Models { AsrCall transcriber; }; -NodeResult Transcribe(const Inputs& inputs, const NoParameters&, - const Models& models) { +NodeResult Run(const Inputs& inputs, const Models& models) { return models.transcriber.Transcribe(*inputs.audio); } -auto AsrTranscribeSpec() { - return MakeBatchSpec( +auto Spec() { + return MakeNodeSpec( InputsOf{Required("audio", &Inputs::audio)}, PreservedOutput("text", "audio"), - Parameters{}, ModelsOf{Model( "transcriber", "bind_model", &Models::transcriber, "引用 models[].model_id;所选模型必须提供 asr 转写能力。")}, - &Transcribe) + &Run) .Category("common") .ParallelSafe(true) .Description("Audio speech recognition (ASR) transcription node"); } -REGISTER_FUNCTION_NODE(AsrTranscribeNode, AsrTranscribeSpec()); +REGISTER_FUNCTION_NODE(AsrTranscribeNode, Spec()); } // namespace } // namespace llm_edgeflow diff --git a/src/common_nodes/llm_generate_node.cpp b/src/common_nodes/llm_generate_node.cpp index 7458f8d5..224a5b36 100644 --- a/src/common_nodes/llm_generate_node.cpp +++ b/src/common_nodes/llm_generate_node.cpp @@ -11,30 +11,26 @@ struct Models { LlmCall generator; }; -NodeResult Generate(const Inputs& inputs, - const GenerateOptions& params, - const Models& models) { +NodeResult Run(const Inputs& inputs, const GenerateOptions& params, + const Models& models) { return models.generator.Generate(*inputs.prompt, params); } -auto LlmGenerateSpec() { - NodeConfigParser parser(GenerateOptionsFields(128), - ParseGenerateOptions); - return MakeBatchSpec( - InputsOf{Required("prompt", &Inputs::prompt)}, - PreservedOutput("text", "prompt"), - Parameters{}.WithParser(std::move(parser)), - ModelsOf{Model("generator", "bind_model", - &Models::generator, - "引用 models[].model_id;所选模型必须提供 " - "llm 文本生成能力。")}, - &Generate) +auto Spec() { + return MakeNodeSpec(InputsOf{Required("prompt", &Inputs::prompt)}, + PreservedOutput("text", "prompt"), + GenerateParameters(128), + ModelsOf{ + Model("generator", "bind_model", &Models::generator, + "引用 models[].model_id;所选模型必须提供 " + "llm 文本生成能力。")}, + &Run) .Category("common") .ParallelSafe(true) .Description("LLM generate text inference node"); } -REGISTER_FUNCTION_NODE(LlmGenerateNode, LlmGenerateSpec()); +REGISTER_FUNCTION_NODE(LlmGenerateNode, Spec()); } // namespace } // namespace llm_edgeflow diff --git a/src/common_nodes/ocr_detect_node.cpp b/src/common_nodes/ocr_detect_node.cpp index 5616da52..14473367 100644 --- a/src/common_nodes/ocr_detect_node.cpp +++ b/src/common_nodes/ocr_detect_node.cpp @@ -13,8 +13,7 @@ struct Models { OcrCall detector; }; -NodeResult Recognize(const Inputs& inputs, const NoParameters&, - const Models& models) { +NodeResult Run(const Inputs& inputs, const Models& models) { auto result = models.detector.Recognize(*inputs.images); if (!result.ok()) return NodeResult::Failure(result.failure()); Outputs outputs; @@ -27,22 +26,21 @@ NodeResult Recognize(const Inputs& inputs, const NoParameters&, return NodeResult::Success(std::move(outputs)); } -auto OcrDetectSpec() { - return MakeBatchSpec( +auto Spec() { + return MakeNodeSpec( InputsOf{Required("images", &Inputs::images)}, OutputsOf( {Produced("document", &Outputs::document, "images"), Produced("text", &Outputs::text, "images")}), - Parameters{}, ModelsOf{Model("detector", "bind_model", &Models::detector, "引用 models[].model_id;所选模型必须提供 " "ocr 文档识别能力。")}, - &Recognize) + &Run) .Category("common") .ParallelSafe(true) .Description("OCR visual document detection and text recognition node"); } -REGISTER_FUNCTION_NODE(OcrDetectNode, OcrDetectSpec()); +REGISTER_FUNCTION_NODE(OcrDetectNode, Spec()); } // namespace } // namespace llm_edgeflow diff --git a/src/common_nodes/structured_json_parse_node.cpp b/src/common_nodes/structured_json_parse_node.cpp index 43c32958..66a73acb 100644 --- a/src/common_nodes/structured_json_parse_node.cpp +++ b/src/common_nodes/structured_json_parse_node.cpp @@ -72,7 +72,7 @@ const std::vector& StructuredJsonParseConfigFields() { /** * @brief 结构化 JSON 解析与文本提取受控算子 (StructuredJsonParseNode) */ -struct StructuredJsonOptions { +struct Params { bool Load(const nlohmann::json& config) { const auto& normalized = config; fallback_json_ = @@ -166,13 +166,12 @@ struct StructuredJsonOptions { }; namespace { -struct StructuredInputs { +struct Inputs { const TextBatch* text = nullptr; }; -bool ParseOrExtractJson(const StructuredJsonOptions& options, - const std::string& input, std::string* out_json, - nlohmann::json* out_structured, +bool ParseOrExtractJson(const Params& options, const std::string& input, + std::string* out_json, nlohmann::json* out_structured, JsonParseStatus* out_status, std::string* out_diag) { if (input.empty()) { *out_diag = "Empty input string"; @@ -248,8 +247,8 @@ bool ParseOrExtractJson(const StructuredJsonOptions& options, return false; } -NodeResult ParseStructuredJson( - const StructuredInputs& inputs, const StructuredJsonOptions& options) { +NodeResult Run(const Inputs& inputs, + const Params& options) { const auto* text_items = inputs.text; StructuredDocumentBatch output_docs; output_docs.reserve(text_items->size()); @@ -299,26 +298,24 @@ NodeResult ParseStructuredJson( return NodeResult::Success(std::move(output_docs)); } -auto StructuredJsonParseSpec() { - auto params = Parameters{}.WithParser( - NodeConfigParser( - StructuredJsonParseConfigFields(), - [](const nlohmann::json& config, StructuredJsonOptions* options, - std::string* diagnostic) { - const bool ok = options->Load(config); - if (!ok && diagnostic) - *diagnostic = "Invalid structured JSON fields, types or fallback"; - return ok; - })); - return MakeBatchSpec( - InputsOf( - {Required("text", &StructuredInputs::text)}), +auto Spec() { + auto params = Parameters{}.WithParser(NodeConfigParser( + StructuredJsonParseConfigFields(), + [](const nlohmann::json& config, Params* options, + std::string* diagnostic) { + const bool ok = options->Load(config); + if (!ok && diagnostic) + *diagnostic = "Invalid structured JSON fields, types or fallback"; + return ok; + })); + return MakeNodeSpec( + InputsOf({Required("text", &Inputs::text)}), PreservedOutput("document", "text"), - std::move(params), &ParseStructuredJson) + std::move(params), &Run) .Category("common") .Description("Complete JSON parsing and block extraction without repair") .ParallelSafe(true); } } // namespace -REGISTER_FUNCTION_NODE(StructuredJsonParseNode, StructuredJsonParseSpec()); +REGISTER_FUNCTION_NODE(StructuredJsonParseNode, Spec()); } // namespace llm_edgeflow diff --git a/src/common_nodes/text_chunk_node.cpp b/src/common_nodes/text_chunk_node.cpp index eca81b26..650983de 100644 --- a/src/common_nodes/text_chunk_node.cpp +++ b/src/common_nodes/text_chunk_node.cpp @@ -8,14 +8,14 @@ namespace llm_edgeflow { namespace { -struct ChunkInputs { +struct Inputs { const TextBatch* text = nullptr; }; -struct ChunkParams { +struct Params { int64_t chunk_size{}; int64_t overlap{}; }; -struct ChunkOutputs { +struct Outputs { TextBatch chunks; Int32Batch counts; }; @@ -49,47 +49,46 @@ NodeResult> SplitText(const std::string& str, return NodeResult>::Success(std::move(chunks)); } -NodeResult ChunkText(const ChunkInputs& inputs, - const ChunkParams& params) { +NodeResult Run(const Inputs& inputs, const Params& params) { auto result = SplitPayloads(*inputs.text, [&](const std::string& text) { return SplitText(text, static_cast(params.chunk_size), static_cast(params.overlap)); }); - if (!result.ok()) return NodeResult::Failure(result.failure()); + if (!result.ok()) return NodeResult::Failure(result.failure()); auto split = std::move(result).value(); - return NodeResult::Success( + return NodeResult::Success( {std::move(split.children), std::move(split.counts)}); } -auto TextChunkSpec() { - auto params = Parameters( - {Field("chunk_size", &ChunkParams::chunk_size) +auto Spec() { + auto params = Parameters( + {Field("chunk_size", &Params::chunk_size) .Default(100) .Range(1, 1000000) .Description("每块最多包含的 Unicode 码点数;按 UTF-8 " "字符边界切分,不是字节数或模型 token 数。"), - Field("overlap", &ChunkParams::overlap) + Field("overlap", &Params::overlap) .Default(0) .Range(0, 100000) .Description("相邻块重叠的 Unicode 码点数,必须小于 chunk_size;0 " "表示无重叠。")}); - params.Validate([](const ChunkParams& value, std::string* diagnostic) { + params.Validate([](const Params& value, std::string* diagnostic) { if (value.overlap < value.chunk_size) return true; if (diagnostic) *diagnostic = "overlap must be smaller than chunk_size"; return false; }); - return MakeBatchSpec( - InputsOf({Required("text", &ChunkInputs::text)}), - OutputsOf( - {Produced("chunks", &ChunkOutputs::chunks, + return MakeNodeSpec( + InputsOf({Required("text", &Inputs::text)}), + OutputsOf( + {Produced("chunks", &Outputs::chunks, PortFlow{"1:N", "generate_sub_id", "request"}), - Produced("chunk_counts", &ChunkOutputs::counts, "text")}), - std::move(params), &ChunkText) + Produced("chunk_counts", &Outputs::counts, "text")}), + std::move(params), &Run) .Category("common") .Description( "UTF-8 code-point-safe text chunking with overlap and provenance") .ParallelSafe(true); } } // namespace -REGISTER_FUNCTION_NODE(TextChunkNode, TextChunkSpec()); +REGISTER_FUNCTION_NODE(TextChunkNode, Spec()); } // namespace llm_edgeflow diff --git a/src/common_nodes/text_corpus_source_node.cpp b/src/common_nodes/text_corpus_source_node.cpp index aeee9c21..b90862e5 100644 --- a/src/common_nodes/text_corpus_source_node.cpp +++ b/src/common_nodes/text_corpus_source_node.cpp @@ -36,15 +36,14 @@ bool ValidateCorpusEntries(const nlohmann::json& config, return true; } -struct CorpusInputs { +struct Inputs { const TextBatch* trigger = nullptr; }; -struct CorpusParams { +struct Params { std::vector corpus; }; -NodeResult ProduceCorpus(const CorpusInputs&, - const CorpusParams& params) { +NodeResult Run(const Inputs&, const Params& params) { TextBatch output; output.reserve(params.corpus.size()); for (size_t i = 0; i < params.corpus.size(); ++i) { @@ -53,28 +52,24 @@ NodeResult ProduceCorpus(const CorpusInputs&, return NodeResult::Success(std::move(output)); } -auto TextCorpusSourceSpec() { - auto params = - Parameters{}.WithParser(NodeConfigParser( - TextCorpusSourceConfigFields(), - [](const nlohmann::json& config, CorpusParams* value, - std::string* diagnostic) { - if (!ValidateCorpusEntries(config, diagnostic)) return false; - if (config.contains("corpus")) - value->corpus = - config.at("corpus").get>(); - return true; - })); - return MakeBatchSpec( - InputsOf( - {OptionalValue("trigger", &CorpusInputs::trigger)}), +auto Spec() { + auto params = Parameters{}.WithParser(NodeConfigParser( + TextCorpusSourceConfigFields(), + [](const nlohmann::json& config, Params* value, std::string* diagnostic) { + if (!ValidateCorpusEntries(config, diagnostic)) return false; + if (config.contains("corpus")) + value->corpus = config.at("corpus").get>(); + return true; + })); + return MakeNodeSpec( + InputsOf({OptionalValue("trigger", &Inputs::trigger)}), ProducedBatch( "corpus", PortFlow{"1:N", "generate_sub_id", "session"}), - std::move(params), &ProduceCorpus) + std::move(params), &Run) .Category("common") .Description("Static text corpus and knowledge database source node") .ParallelSafe(true); } } // namespace -REGISTER_FUNCTION_NODE(TextCorpusSourceNode, TextCorpusSourceSpec()); +REGISTER_FUNCTION_NODE(TextCorpusSourceNode, Spec()); } // namespace llm_edgeflow diff --git a/src/common_nodes/text_embedding_node.cpp b/src/common_nodes/text_embedding_node.cpp index 6b2a1dfb..2e184ed5 100644 --- a/src/common_nodes/text_embedding_node.cpp +++ b/src/common_nodes/text_embedding_node.cpp @@ -8,14 +8,14 @@ namespace llm_edgeflow { namespace { -struct EmbeddingInputs { +struct Inputs { const TextBatch* text = nullptr; }; -struct EmbeddingParams { +struct Params { bool normalize{}; std::string lifetime; }; -struct EmbeddingModels { +struct Models { EmbeddingCall encoder; }; @@ -45,10 +45,9 @@ std::string ConstructSessionCacheKey(const std::string& model_id, return key; } -NodeResult EmbedText(const EmbeddingInputs& inputs, - const EmbeddingParams& params, - const EmbeddingModels& models, - const SessionResources& resources) { +NodeResult Run(const Inputs& inputs, const Params& params, + const Models& models, + const SessionResources& resources) { const auto& text = *inputs.text; if (text.empty()) return NodeResult::Success({}); EmbeddingOptions options; @@ -74,31 +73,30 @@ NodeResult EmbedText(const EmbeddingInputs& inputs, return NodeResult::Success(*cached.value()); } -auto TextEmbeddingSpec() { +auto Spec() { const PortFlow flow{"1:1", "preserve", "request", "lifetime"}; - return MakeBatchSpec( - InputsOf( - {Required("text", &EmbeddingInputs::text, flow)}), + return MakeNodeSpec( + InputsOf({Required("text", &Inputs::text, flow)}), PreservedOutput("embedding", "text", flow), - Parameters( - {Field("normalize", &EmbeddingParams::normalize) + Parameters( + {Field("normalize", &Params::normalize) .Default(true) .Description("要求模型对输出向量做 L2 归一化。"), - Field("lifetime", &EmbeddingParams::lifetime) + Field("lifetime", &Params::lifetime) .Default("request") .Enum({"request", "session"}) .Description("request 每次请求计算;session " "按模型版本、归一化选项和输入缓存向量,输入" "须满足 session 生命周期契约。")}), - ModelsOf( - {Model("encoder", "bind_model", &EmbeddingModels::encoder, + ModelsOf( + {Model("encoder", "bind_model", &Models::encoder, "引用 models[].model_id;所选模型必须提供 embedding " "文本向量能力。")}), - &EmbedText) + &Run) .Category("common") .Description("Text embedding extraction node") .ParallelSafe(true); } } // namespace -REGISTER_FUNCTION_NODE(TextEmbeddingNode, TextEmbeddingSpec()); +REGISTER_FUNCTION_NODE(TextEmbeddingNode, Spec()); } // namespace llm_edgeflow diff --git a/src/common_nodes/text_rerank_node.cpp b/src/common_nodes/text_rerank_node.cpp index 246c21ed..140fcb42 100644 --- a/src/common_nodes/text_rerank_node.cpp +++ b/src/common_nodes/text_rerank_node.cpp @@ -10,22 +10,21 @@ namespace llm_edgeflow { namespace { -struct RerankInputs { +struct Inputs { const TextBatch* queries = nullptr; const RankedTextBatch* candidates = nullptr; const TextBatch* candidate_texts = nullptr; const QueryCandidatesBatch* pairs = nullptr; }; -struct RerankParams { +struct Params { int top_k{}; }; -struct RerankModels { +struct Models { RerankCall reranker; }; -NodeResult RerankText(const RerankInputs& inputs, - const RerankParams& params, - const RerankModels& models) { +NodeResult Run(const Inputs& inputs, const Params& params, + const Models& models) { const auto* queries = inputs.queries; const auto* candidates = inputs.candidates; const auto* candidate_texts = inputs.candidate_texts; @@ -133,30 +132,29 @@ NodeResult RerankText(const RerankInputs& inputs, return NodeResult::Success(std::move(refined_batch)); } -auto TextRerankSpec() { - return MakeBatchSpec( - InputsOf( - {OptionalValue("queries", &RerankInputs::queries), - OptionalValue("candidates", &RerankInputs::candidates, +auto Spec() { + return MakeNodeSpec( + InputsOf( + {OptionalValue("queries", &Inputs::queries), + OptionalValue("candidates", &Inputs::candidates, PortFlow{"N:1", "preserve", "request"}), - OptionalValue("candidate_texts", - &RerankInputs::candidate_texts, + OptionalValue("candidate_texts", &Inputs::candidate_texts, PortFlow{"N:1", "preserve", "request"}), - OptionalValue("pairs", &RerankInputs::pairs)}), + OptionalValue("pairs", &Inputs::pairs)}), ProducedBatch( "ranked", PortFlow{"1:N", "generate_sub_id", "request"}), - Parameters( - {Field("top_k", &RerankParams::top_k) + Parameters( + {Field("top_k", &Params::top_k) .Default(1) .Range(1, 1000) .Description( "按 req_id " "分组,用重排模型分数降序保留的候选条数上限。")}), - ModelsOf( - {Model("reranker", "bind_model", &RerankModels::reranker, + ModelsOf( + {Model("reranker", "bind_model", &Models::reranker, "引用 models[].model_id;所选模型必须提供 rerank " "查询与候选评分能力。")}), - &RerankText) + &Run) .PortConstraints({PortGroupConstraint::Groups( PortConstraintKind::kExactOneGroupOf, {{"pairs"}, @@ -169,5 +167,5 @@ auto TextRerankSpec() { .ParallelSafe(true); } } // namespace -REGISTER_FUNCTION_NODE(TextRerankNode, TextRerankSpec()); +REGISTER_FUNCTION_NODE(TextRerankNode, Spec()); } // namespace llm_edgeflow diff --git a/src/common_nodes/text_rule_match_node.cpp b/src/common_nodes/text_rule_match_node.cpp index bb3433d3..309c2add 100644 --- a/src/common_nodes/text_rule_match_node.cpp +++ b/src/common_nodes/text_rule_match_node.cpp @@ -103,7 +103,7 @@ struct RuleSpec { std::shared_ptr compiled_regex; }; -struct RuleMatchState { +struct Params { CategoryList category_keywords_list; std::vector rules_list; std::string default_category; @@ -174,7 +174,7 @@ bool BuildRules(const nlohmann::json& rules_json, return true; } -bool BuildRuleMatchState(const nlohmann::json& config, RuleMatchState* state, +bool BuildRuleMatchState(const nlohmann::json& config, Params* state, std::string* diagnostic) { if (diagnostic) diagnostic->clear(); CategoryList categories; @@ -194,12 +194,11 @@ bool BuildRuleMatchState(const nlohmann::json& config, RuleMatchState* state, return true; } -struct RuleInputs { +struct Inputs { const TextBatch* text = nullptr; }; -NodeResult MatchRules(const RuleInputs& inputs, - const RuleMatchState& state) { +NodeResult Run(const Inputs& inputs, const Params& state) { const auto* text_items = inputs.text; RuleMatchBatch output_matches; output_matches.reserve(text_items->size()); @@ -283,39 +282,35 @@ NodeResult MatchRules(const RuleInputs& inputs, return NodeResult::Success(std::move(output_matches)); } -NodeResult UpdateRules(const RuleMatchState& current, - const nlohmann::json& root, - const BindingFacts&) { +NodeResult UpdateRules(const Params& current, + const nlohmann::json& root, + const BindingFacts&) { std::string error; - RuleMatchState next = current; + Params next = current; if (!BuildRuleMatchState(root, &next, &error)) { - return NodeResult::Failure( - NodeErrorKind::kBusinessError, error, - node_error::control::kInvalidRequest); + return NodeResult::Failure(NodeErrorKind::kBusinessError, error, + node_error::control::kInvalidRequest); } - return NodeResult::Success(std::move(next)); + return NodeResult::Success(std::move(next)); } -auto MakeTextRuleMatchSpec() { - auto parameters = - Parameters{}.WithParser(NodeConfigParser( - TextRuleMatchConfigFields(), - [](const nlohmann::json& config, RuleMatchState* state, - std::string* diagnostic) { - state->default_category = - config.at("default_category").get(); - state->default_score = config.at("default_score").get(); - return BuildRuleMatchState(config, state, diagnostic); - })); +auto Spec() { + auto parameters = Parameters{}.WithParser(NodeConfigParser( + TextRuleMatchConfigFields(), + [](const nlohmann::json& config, Params* state, std::string* diagnostic) { + state->default_category = + config.at("default_category").get(); + state->default_score = config.at("default_score").get(); + return BuildRuleMatchState(config, state, diagnostic); + })); auto control = ControlCommandDefinition( kControlCmdUpdateRules, "update_rules", "Update matching rules and categories dynamically", RuleControlSchema(), true); control.shared_id = true; - return MakeBatchSpec( - InputsOf{Required("text", &RuleInputs::text)}, - PreservedOutput("matches", "text"), - std::move(parameters), MatchRules) + return MakeNodeSpec(InputsOf{Required("text", &Inputs::text)}, + PreservedOutput("matches", "text"), + std::move(parameters), Run) .Category("common") .Description( "Keyword and Unicode regex matching with lookbehind and named " @@ -325,6 +320,6 @@ auto MakeTextRuleMatchSpec() { } } // namespace -REGISTER_FUNCTION_NODE(TextRuleMatchNode, MakeTextRuleMatchSpec()); +REGISTER_FUNCTION_NODE(TextRuleMatchNode, Spec()); } // namespace llm_edgeflow diff --git a/src/common_nodes/text_template_node.cpp b/src/common_nodes/text_template_node.cpp index 6fbc37ab..e534a17a 100644 --- a/src/common_nodes/text_template_node.cpp +++ b/src/common_nodes/text_template_node.cpp @@ -141,7 +141,7 @@ const nlohmann::json& TemplateControlSchema() { {{"type", "string"}, {"enum", {"fail", "empty", "preserve"}}}}}}}; return schema; } -struct TemplateState { +struct Params { std::string template_str = kDefaultTemplate; std::string separator = kDefaultSeparator; size_t max_length = kDefaultMaxLength; @@ -188,7 +188,7 @@ inline bool CompileTemplate( return ok; } -bool BuildTemplateState(TemplateState* state, +bool BuildTemplateState(Params* state, const std::unordered_set* connected_inputs, std::string* diagnostic) { std::vector tokens; @@ -205,10 +205,10 @@ bool BuildTemplateState(TemplateState* state, return true; } -inline NodeResult BuildNextTemplate( - const TemplateState& current, const TemplateUpdate& update, - const BindingFacts& bindings) { - TemplateState next = current; +inline NodeResult BuildNextTemplate(const Params& current, + const TemplateUpdate& update, + const BindingFacts& bindings) { + Params next = current; if (update.template_str) next.template_str = *update.template_str; if (update.prompt_id) next.prompt_id = *update.prompt_id; if (update.missing_variable_policy) { @@ -227,17 +227,17 @@ inline NodeResult BuildNextTemplate( if (!BuildTemplateState( &next, bindings.has_bindings ? &bindings.connected_inputs : nullptr, &diagnostic)) { - return NodeResult::Failure( + return NodeResult::Failure( NodeErrorKind::kBusinessError, diagnostic.empty() ? "Invalid template placeholders or syntax in Control" : diagnostic, node_error::control::kInvalidRequest); } - return NodeResult::Success(std::move(next)); + return NodeResult::Success(std::move(next)); } -struct TemplateInputs { +struct Inputs { const TextBatch* primary = nullptr; const RankedTextBatch* context = nullptr; const TextBatch* context_text = nullptr; @@ -247,8 +247,7 @@ struct TemplateInputs { const TextAttributesBatch* attributes = nullptr; }; -NodeResult RenderTemplate(const TemplateInputs& inputs, - const TemplateState& state) { +NodeResult Run(const Inputs& inputs, const Params& state) { const auto* primary_items = inputs.primary; const auto* context_items = inputs.context; const auto* context_text_items = inputs.context_text; @@ -460,9 +459,9 @@ NodeResult RenderTemplate(const TemplateInputs& inputs, return NodeResult::Success(std::move(output_batch)); } -NodeResult UpdateTemplate(const TemplateState& current, - const nlohmann::json& root, - const BindingFacts& bindings) { +NodeResult UpdateTemplate(const Params& current, + const nlohmann::json& root, + const BindingFacts& bindings) { TemplateUpdate update; if (root.contains("template")) update.template_str = root["template"].get(); @@ -486,13 +485,12 @@ NodeResult UpdateTemplate(const TemplateState& current, return BuildNextTemplate(current, update, bindings); } -auto MakeTextTemplateSpec() { +auto Spec() { auto parameters = - Parameters{} - .WithParser(NodeConfigParser( + Parameters{} + .WithParser(NodeConfigParser( TextTemplateConfigFields(), - [](const nlohmann::json& config, TemplateState* state, - std::string*) { + [](const nlohmann::json& config, Params* state, std::string*) { state->template_str = config.at("template").get(); state->separator = config.at("separator").get(); state->max_length = config.at("max_length").get(); @@ -507,7 +505,7 @@ auto MakeTextTemplateSpec() { config.at("values").getstatic_values)>(); return true; })) - .Prepare([](TemplateState* state, const BindingFacts& bindings, + .Prepare([](Params* state, const BindingFacts& bindings, std::string* diagnostic) { state->allow_dynamic_attrs = state->allow_dynamic_attrs || bindings.IsConnected("attributes"); @@ -518,22 +516,22 @@ auto MakeTextTemplateSpec() { kControlCmdUpdatePrompt, "update_prompt", "Update template string dynamically", TemplateControlSchema(), true); control.shared_id = true; - return MakeBatchSpec( - InputsOf{ - OptionalValue("primary", &TemplateInputs::primary), - OptionalValue("context", &TemplateInputs::context, + return MakeNodeSpec( + InputsOf{ + OptionalValue("primary", &Inputs::primary), + OptionalValue("context", &Inputs::context, InputFlow::AggregateByRequest), - OptionalValue("context_text", &TemplateInputs::context_text, + OptionalValue("context_text", &Inputs::context_text, InputFlow::AggregateByRequest), - OptionalValue("matches", &TemplateInputs::matches, + OptionalValue("matches", &Inputs::matches, InputFlow::AggregateByRequest), - OptionalValue("document", &TemplateInputs::document, + OptionalValue("document", &Inputs::document, InputFlow::AggregateByRequest), - OptionalValue("document_text", &TemplateInputs::document_text, + OptionalValue("document_text", &Inputs::document_text, InputFlow::AggregateByRequest), - OptionalValue("attributes", &TemplateInputs::attributes)}, + OptionalValue("attributes", &Inputs::attributes)}, ProducedBatch("text", PortFlow{}), - std::move(parameters), RenderTemplate) + std::move(parameters), Run) .Category("common") .Description( "Text template rendering: {{name}} substitutes variables; " @@ -549,6 +547,6 @@ auto MakeTextTemplateSpec() { } } // namespace -REGISTER_FUNCTION_NODE(TextTemplateNode, MakeTextTemplateSpec()); +REGISTER_FUNCTION_NODE(TextTemplateNode, Spec()); } // namespace llm_edgeflow diff --git a/src/common_nodes/vector_top_k_node.cpp b/src/common_nodes/vector_top_k_node.cpp index 7d772ea8..bc2c7cd9 100644 --- a/src/common_nodes/vector_top_k_node.cpp +++ b/src/common_nodes/vector_top_k_node.cpp @@ -10,12 +10,12 @@ namespace llm_edgeflow { namespace { -struct VectorInputs { +struct Inputs { const EmbeddingBatch* queries = nullptr; const EmbeddingBatch* candidates = nullptr; const TextBatch* candidate_texts = nullptr; }; -struct VectorParams { +struct Params { std::string candidate_scope; int top_k{}; float min_score{}; @@ -43,8 +43,7 @@ float DotProduct(const std::vector& v1, const std::vector& v2) { return dot; } -NodeResult RankVectors(const VectorInputs& inputs, - const VectorParams& params) { +NodeResult Run(const Inputs& inputs, const Params& params) { const auto* queries = inputs.queries; const auto* candidates = inputs.candidates; const auto* candidate_texts = inputs.candidate_texts; @@ -160,43 +159,42 @@ NodeResult RankVectors(const VectorInputs& inputs, return NodeResult::Success(std::move(ranked_batch)); } -auto VectorTopKSpec() { - return MakeBatchSpec( - InputsOf( - {Required("queries", &VectorInputs::queries), - Required("candidates", &VectorInputs::candidates, +auto Spec() { + return MakeNodeSpec( + InputsOf( + {Required("queries", &Inputs::queries), + Required("candidates", &Inputs::candidates, PortFlow{"N:1", "preserve", "request"}), - OptionalValue("candidate_texts", - &VectorInputs::candidate_texts, + OptionalValue("candidate_texts", &Inputs::candidate_texts, PortFlow{"N:1", "preserve", "request"})}), ProducedBatch( "ranked", PortFlow{"1:N", "generate_sub_id", "request"}), - Parameters( - {Field("candidate_scope", &VectorParams::candidate_scope) + Parameters( + {Field("candidate_scope", &Params::candidate_scope) .Default("request") .Enum({"request", "shared"}) .Description("request 只检索相同 req_id 的候选;shared " "使用所有 req_id=0 的共享候选。"), - Field("top_k", &VectorParams::top_k) + Field("top_k", &Params::top_k) .Default(1) .Range(1, 1000) .Description("每条查询按向量相似度降序返回的候选条数上限" ",在 min_score 过滤之后应用。"), - Field("min_score", &VectorParams::min_score) + Field("min_score", &Params::min_score) .Default(0.0f) .Range(-100, 100) .Description("保留相似度大于等于此值的候选;分数含义随 " "metric 变化。"), - Field("metric", &VectorParams::metric) + Field("metric", &Params::metric) .Default("cosine") .Enum({"cosine", "dot_product"}) .Description("cosine 使用余弦相似度;dot_product " "使用原始向量点积,分数受向量模长影响。")}), - &RankVectors) + &Run) .Category("common") .Description("Vector Top-K search and ranking node") .ParallelSafe(true); } } // namespace -REGISTER_FUNCTION_NODE(VectorTopKNode, VectorTopKSpec()); +REGISTER_FUNCTION_NODE(VectorTopKNode, Spec()); } // namespace llm_edgeflow diff --git a/src/core/pipeline_catalog.cpp b/src/core/pipeline_catalog.cpp index 47f258f5..38659d0d 100644 --- a/src/core/pipeline_catalog.cpp +++ b/src/core/pipeline_catalog.cpp @@ -238,8 +238,7 @@ nlohmann::json PipelineCatalog::NodeToJson(const NodeDefinition& definition) { {"port_constraints", std::move(constraints)}, {"control_commands", std::move(commands)}, {"config_fields", std::move(fields)}, - {"model_dependencies", std::move(model_deps)}, - {"biz_names", definition.biz_names}}; + {"model_dependencies", std::move(model_deps)}}; } nlohmann::json PipelineCatalog::ModelToJson(const ModelDefinition& definition) { @@ -281,11 +280,6 @@ nlohmann::json PipelineCatalog::ToJson(const PipelineCatalogSnapshot& snapshot, const std::string& biz_filter) { nlohmann::json nodes = nlohmann::json::array(); for (const auto& item : snapshot.nodes) { - if (!biz_filter.empty() && !item.biz_names.empty() && - std::find(item.biz_names.begin(), item.biz_names.end(), biz_filter) == - item.biz_names.end()) { - continue; - } nodes.push_back(NodeToJson(item)); } diff --git a/src/core/pipeline_validator.cpp b/src/core/pipeline_validator.cpp index 83f4535c..f9233d08 100644 --- a/src/core/pipeline_validator.cpp +++ b/src/core/pipeline_validator.cpp @@ -555,14 +555,6 @@ ValidatedPipelinePlan ValidateAndPlanInternal( } def_by_id[node.id] = definition; - if (biz && !definition->biz_names.empty() && - std::find(definition->biz_names.begin(), definition->biz_names.end(), - parsed.biz_name) == definition->biz_names.end()) { - Add(&report, DiagnosticCode::kNodeBizMismatch, - "/pipeline/" + std::to_string(node.source_index) + "/node_type", - "Node type is not declared for biz: " + parsed.biz_name, node.id); - } - bool node_fields_valid = false; auto normalized_config = ValidateConfigFields( definition->config_fields, node.config, diff --git a/src/custom_nodes/README.md b/src/custom_nodes/README.md index 392bdae9..8dc75f79 100644 --- a/src/custom_nodes/README.md +++ b/src/custom_nodes/README.md @@ -9,8 +9,9 @@ [第一个自定义 Node](../../doc/dev_guide/first_custom_node.md) 完成生成、修改、编译和运行。 [按需参考](../../doc/dev_guide/custom_node_concepts.md)解释端口、来源、模型、Catalog 和并发。 -所有生产 Node 使用 `REGISTER_FUNCTION_NODE` 从 Spec 生成绑定、Definition 和执行包装。 -Map、Batch、LLM 是同一契约的便利组合;`NodeBase` 是框架内部运行机制,不是业务作者的另一个入口。 +所有 Node 使用同一结构(`Inputs`、可选的 `Params` 与 `Models`、`Run`、`Spec`),通过 +`REGISTER_FUNCTION_NODE` 从 Spec 生成绑定、Definition 和执行包装。`NodeBase` 是框架内部运行机制, +不是业务作者的另一个入口。 ## 通用开发步骤速查 diff --git a/src/custom_nodes/prompt_guided_llm_node.cpp b/src/custom_nodes/prompt_guided_llm_node.cpp index 40771110..1bc12a86 100644 --- a/src/custom_nodes/prompt_guided_llm_node.cpp +++ b/src/custom_nodes/prompt_guided_llm_node.cpp @@ -15,7 +15,7 @@ namespace custom_nodes { namespace { // 初始化后供处理阶段使用的普通自有配置。 -struct PromptConfig { +struct Params { std::vector prompt_parts; bool uses_context = false; std::string prompt_prefix; @@ -25,7 +25,7 @@ struct PromptConfig { // 字段已校验并填充默认值。此处只保留本 Node 的语义转换; // 请求值从不进入配置解析。 -bool ParsePromptConfig(const nlohmann::json& config, PromptConfig* parameters, +bool ParsePromptConfig(const nlohmann::json& config, Params* parameters, std::string* error) { auto reject = [&](const std::string& message) { if (error) *error = message; @@ -90,9 +90,9 @@ std::vector PromptConfigFields() { return fields; } -const NodeConfigParser& PromptConfiguration() { - static const NodeConfigParser parser(PromptConfigFields(), - ParsePromptConfig); +const NodeConfigParser& PromptConfiguration() { + static const NodeConfigParser parser(PromptConfigFields(), + ParsePromptConfig); return parser; } @@ -148,9 +148,8 @@ struct Models { LlmCall generator; }; -NodeResult GeneratePrompt(const Inputs& inputs, - const PromptConfig& params, - const Models& models) { +NodeResult Run(const Inputs& inputs, const Params& params, + const Models& models) { if (inputs.input->empty()) return NodeResult::Success({}); if (params.uses_context && !inputs.context) { return NodeResult::Failure( @@ -181,9 +180,9 @@ NodeResult GeneratePrompt(const Inputs& inputs, return result; } -auto PromptGuidedSpec() { - auto params = Parameters{}.WithParser(PromptConfiguration()); - params.ValidateBindings([](const PromptConfig& config, +auto Spec() { + auto params = Parameters{}.WithParser(PromptConfiguration()); + params.ValidateBindings([](const Params& config, const std::unordered_set& inputs, std::string* error) { if (config.uses_context && inputs.count("context") == 0) { @@ -193,7 +192,7 @@ auto PromptGuidedSpec() { } return true; }); - return MakeBatchSpec( + return MakeNodeSpec( InputsOf{Required("input", &Inputs::input), OptionalValue("context", &Inputs::context, InputFlow::AggregateByRequest)}, @@ -202,7 +201,7 @@ auto PromptGuidedSpec() { &Models::generator, "引用 models[].model_id;所选模型必须提供 " "llm 文本生成能力。")}, - &GeneratePrompt) + &Run) .Category("custom") .ParallelSafe(true) .Description( @@ -210,7 +209,7 @@ auto PromptGuidedSpec() { "and response post-processing using {{input}}/{{context}} templates"); } -REGISTER_FUNCTION_NODE(PromptGuidedLlmNode, PromptGuidedSpec()); +REGISTER_FUNCTION_NODE(PromptGuidedLlmNode, Spec()); } // namespace } // namespace custom_nodes diff --git a/tests/contract/architecture/test_layer_header_views.cmake b/tests/contract/architecture/test_layer_header_views.cmake index 366dcf29..dd491617 100644 --- a/tests/contract/architecture/test_layer_header_views.cmake +++ b/tests/contract/architecture/test_layer_header_views.cmake @@ -158,28 +158,25 @@ function(check_authoring_snippet case_name body expected_diagnostic) endif() endfunction() -check_authoring_snippet(valid_map [=[ +check_authoring_snippet(valid_node [=[ +struct Inputs { const TextBatch* input = nullptr; }; std::string Clean(const std::string& input) { return input; } +NodeResult Run(const Inputs& inputs) { + return MapPayloads(*inputs.input, &Clean); +} auto Spec() { - return MakeMapSpec(Input("input"), Output("output"), &Clean); + return MakeNodeSpec(InputsOf{Required("input", &Inputs::input)}, + PreservedOutput("output", "input"), &Run); } -REGISTER_FUNCTION_NODE(HeaderOnlyMapNode, Spec()); +REGISTER_FUNCTION_NODE(HeaderOnlyNode, Spec()); ]=] "") -check_authoring_snippet(wrong_map_signature [=[ -std::string Wrong(int input) { return std::to_string(input); } -auto spec = MakeMapSpec(Input("input"), Output("output"), &Wrong); -]=] "Map function must accept either") - -check_authoring_snippet(unknown_batch [=[ -struct UnknownBatch {}; -Input input("input"); -]=] "Input batch type must be a vector of TraceableItem") - -check_authoring_snippet(wrong_model_member [=[ -struct Models { EmbeddingCall generator; }; -auto slot = Llm("generator", "bind_model", &Models::generator); -]=] "no matching function" "LlmCall") +check_authoring_snippet(wrong_run_signature [=[ +struct Inputs { const TextBatch* input = nullptr; }; +NodeResult Run(int input) { return TextBatch{}; } +auto spec = MakeNodeSpec(InputsOf{Required("input", &Inputs::input)}, + PreservedOutput("output", "input"), &Run); +]=] "Node Run must be callable as") # 通过增量、无依赖的夹具验证实际的 CMake 模块。 # 复用常规生成器,包括父项目使用 multi-config 的情况。 diff --git a/tests/contract/authoring/test_spec_signature_diagnostics.cmake b/tests/contract/authoring/test_spec_signature_diagnostics.cmake index d8bcfd9d..e5e768c9 100644 --- a/tests/contract/authoring/test_spec_signature_diagnostics.cmake +++ b/tests/contract/authoring/test_spec_signature_diagnostics.cmake @@ -52,13 +52,12 @@ endforeach() # 本脚本自身不维护重复的签名列表。 file(READ "${FUNCTION_NODE_HEADER}" contract_text) string(REGEX REPLACE "\"[ \t\r\n]+\"" "" contract_text "${contract_text}") -string(REGEX MATCHALL - "(NodeResult Run|std::string (BuildPrompt|FormatAnswer))\\([^)]*\\)" +string(REGEX MATCHALL "NodeResult Run\\([^)]*\\)" signatures "${contract_text}") list(REMOVE_DUPLICATES signatures) list(LENGTH signatures signature_count) -if(signature_count LESS 7) - message(FATAL_ERROR "Extracted only ${signature_count} canonical signatures; require at least 7") +if(signature_count LESS 1) + message(FATAL_ERROR "No canonical Run signature found in ${FUNCTION_NODE_HEADER}") endif() file(READ "${CONCEPTS_DOC}" concepts_text) foreach(signature IN LISTS signatures) diff --git a/tests/fixtures/spec_signatures/batch_invalid_member.cpp b/tests/fixtures/spec_signatures/batch_invalid_member.cpp deleted file mode 100644 index 33d08037..00000000 --- a/tests/fixtures/spec_signatures/batch_invalid_member.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// expect-error: Batch Run must be callable as one of -#include "signature_fixture.h" -struct Logic { - NodeResult Run(Inputs&, const Options&) const { - return TextBatch{}; - } -}; -auto Spec() { return Batch(&Logic::Run); } -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/batch_missing_noparameters.cpp b/tests/fixtures/spec_signatures/batch_missing_noparameters.cpp deleted file mode 100644 index e69735a5..00000000 --- a/tests/fixtures/spec_signatures/batch_missing_noparameters.cpp +++ /dev/null @@ -1,9 +0,0 @@ -// expect-error: Batch Run must be callable as one of -#include "signature_fixture.h" -NodeResult Run(const Inputs&, const Models&) { return TextBatch{}; } -auto Spec() { - return MakeBatchSpec(InputsContract(), - PreservedOutput("output", "input"), - ModelContract(), &Run); -} -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/llm_build_prompt_nonconst.cpp b/tests/fixtures/spec_signatures/llm_build_prompt_nonconst.cpp deleted file mode 100644 index 0d884a91..00000000 --- a/tests/fixtures/spec_signatures/llm_build_prompt_nonconst.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// expect-error: BuildPrompt must be callable as -#include "signature_fixture.h" -std::string Wrong(std::string&) { return {}; } -auto Spec() { return MakeLlmTextSpec(&Wrong, &Text); } -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/llm_build_prompt_returns_int.cpp b/tests/fixtures/spec_signatures/llm_build_prompt_returns_int.cpp deleted file mode 100644 index 9f1c424e..00000000 --- a/tests/fixtures/spec_signatures/llm_build_prompt_returns_int.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// expect-error: BuildPrompt must return -#include "signature_fixture.h" -int Wrong(const std::string&) { return 42; } -auto Spec() { return MakeLlmTextSpec(&Wrong, &Text); } -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/llm_build_prompt_returns_result_int.cpp b/tests/fixtures/spec_signatures/llm_build_prompt_returns_result_int.cpp deleted file mode 100644 index 1aa30aa0..00000000 --- a/tests/fixtures/spec_signatures/llm_build_prompt_returns_result_int.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// expect-error: BuildPrompt must return -#include "signature_fixture.h" -NodeResult Wrong(const std::string&) { return 42; } -auto Spec() { return MakeLlmTextSpec(&Wrong, &Text); } -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/llm_format_answer_nonconst.cpp b/tests/fixtures/spec_signatures/llm_format_answer_nonconst.cpp deleted file mode 100644 index e077e616..00000000 --- a/tests/fixtures/spec_signatures/llm_format_answer_nonconst.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// expect-error: FormatAnswer must be callable as -#include "signature_fixture.h" -std::string Wrong(std::string&) { return {}; } -auto Spec() { return MakeLlmTextSpec(&Text, &Wrong); } -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/llm_format_answer_returns_char.cpp b/tests/fixtures/spec_signatures/llm_format_answer_returns_char.cpp deleted file mode 100644 index a34d7caf..00000000 --- a/tests/fixtures/spec_signatures/llm_format_answer_returns_char.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// expect-error: FormatAnswer must return -#include "signature_fixture.h" -char Wrong(const std::string&) { return 'x'; } -auto Spec() { return MakeLlmTextSpec(&Text, &Wrong); } -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/llm_format_answer_returns_int.cpp b/tests/fixtures/spec_signatures/llm_format_answer_returns_int.cpp deleted file mode 100644 index 5d7e7ece..00000000 --- a/tests/fixtures/spec_signatures/llm_format_answer_returns_int.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// expect-error: FormatAnswer must return -#include "signature_fixture.h" -int Wrong(const std::string&) { return 42; } -auto Spec() { return MakeLlmTextSpec(&Text, &Wrong); } -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/llm_format_answer_returns_result_char.cpp b/tests/fixtures/spec_signatures/llm_format_answer_returns_result_char.cpp deleted file mode 100644 index 66548b44..00000000 --- a/tests/fixtures/spec_signatures/llm_format_answer_returns_result_char.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// expect-error: FormatAnswer must return -#include "signature_fixture.h" -NodeResult Wrong(const std::string&) { return 'x'; } -auto Spec() { return MakeLlmTextSpec(&Text, &Wrong); } -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/llm_mutable_build_prompt.cpp b/tests/fixtures/spec_signatures/llm_mutable_build_prompt.cpp deleted file mode 100644 index 3541b661..00000000 --- a/tests/fixtures/spec_signatures/llm_mutable_build_prompt.cpp +++ /dev/null @@ -1,12 +0,0 @@ -// expect-error: BuildPrompt must be callable as -#include "signature_fixture.h" - -auto Spec() { - return MakeLlmTextSpec( - [count = 0](const std::string& text) mutable { - ++count; - return text; - }, - &Text); -} -REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/run_member_function.cpp b/tests/fixtures/spec_signatures/run_member_function.cpp new file mode 100644 index 00000000..5025729a --- /dev/null +++ b/tests/fixtures/spec_signatures/run_member_function.cpp @@ -0,0 +1,10 @@ +// expect-error: Node Run must be callable as +#include "signature_fixture.h" +struct Logic { + NodeResult Run(const Inputs&, const Options&, + const Models&) const { + return TextBatch{}; + } +}; +auto Spec() { return ParamsAndModels(&Logic::Run); } +REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/run_missing_models.cpp b/tests/fixtures/spec_signatures/run_missing_models.cpp new file mode 100644 index 00000000..dacab911 --- /dev/null +++ b/tests/fixtures/spec_signatures/run_missing_models.cpp @@ -0,0 +1,5 @@ +// expect-error: Node Run must be callable as +#include "signature_fixture.h" +NodeResult Run(const Inputs&, const Options&) { return TextBatch{}; } +auto Spec() { return ParamsAndModels(&Run); } +REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/batch_nonconst_inputs.cpp b/tests/fixtures/spec_signatures/run_nonconst_inputs.cpp similarity index 65% rename from tests/fixtures/spec_signatures/batch_nonconst_inputs.cpp rename to tests/fixtures/spec_signatures/run_nonconst_inputs.cpp index 6b790c10..17040af5 100644 --- a/tests/fixtures/spec_signatures/batch_nonconst_inputs.cpp +++ b/tests/fixtures/spec_signatures/run_nonconst_inputs.cpp @@ -1,7 +1,7 @@ -// expect-error: Batch Run must be callable as one of +// expect-error: Node Run must be callable as #include "signature_fixture.h" NodeResult Run(Inputs&, const Options&, const Models&) { return TextBatch{}; } -auto Spec() { return Batch(&Run); } +auto Spec() { return ParamsAndModels(&Run); } REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/batch_params_swapped.cpp b/tests/fixtures/spec_signatures/run_params_swapped.cpp similarity index 66% rename from tests/fixtures/spec_signatures/batch_params_swapped.cpp rename to tests/fixtures/spec_signatures/run_params_swapped.cpp index e7d08fb7..eee0d935 100644 --- a/tests/fixtures/spec_signatures/batch_params_swapped.cpp +++ b/tests/fixtures/spec_signatures/run_params_swapped.cpp @@ -1,7 +1,7 @@ -// expect-error: Batch Run must be callable as one of +// expect-error: Node Run must be callable as #include "signature_fixture.h" NodeResult Run(const Inputs&, const Models&, const Options&) { return TextBatch{}; } -auto Spec() { return Batch(&Run); } +auto Spec() { return ParamsAndModels(&Run); } REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/batch_returns_plain_batch.cpp b/tests/fixtures/spec_signatures/run_returns_plain_batch.cpp similarity index 59% rename from tests/fixtures/spec_signatures/batch_returns_plain_batch.cpp rename to tests/fixtures/spec_signatures/run_returns_plain_batch.cpp index 3825079a..c9517fdb 100644 --- a/tests/fixtures/spec_signatures/batch_returns_plain_batch.cpp +++ b/tests/fixtures/spec_signatures/run_returns_plain_batch.cpp @@ -1,5 +1,5 @@ -// expect-error: Batch Run must return NodeResult +// expect-error: Node Run must return NodeResult #include "signature_fixture.h" TextBatch Run(const Inputs&, const Options&, const Models&) { return {}; } -auto Spec() { return Batch(&Run); } +auto Spec() { return ParamsAndModels(&Run); } REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/batch_returns_wrong_result.cpp b/tests/fixtures/spec_signatures/run_returns_wrong_result.cpp similarity index 62% rename from tests/fixtures/spec_signatures/batch_returns_wrong_result.cpp rename to tests/fixtures/spec_signatures/run_returns_wrong_result.cpp index 2bf8e965..1741d502 100644 --- a/tests/fixtures/spec_signatures/batch_returns_wrong_result.cpp +++ b/tests/fixtures/spec_signatures/run_returns_wrong_result.cpp @@ -1,7 +1,7 @@ -// expect-error: Batch Run must return NodeResult +// expect-error: Node Run must return NodeResult #include "signature_fixture.h" NodeResult Run(const Inputs&, const Options&, const Models&) { return Int32Batch{}; } -auto Spec() { return Batch(&Run); } +auto Spec() { return ParamsAndModels(&Run); } REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/run_writes_nomodels.cpp b/tests/fixtures/spec_signatures/run_writes_nomodels.cpp new file mode 100644 index 00000000..2bc52222 --- /dev/null +++ b/tests/fixtures/spec_signatures/run_writes_nomodels.cpp @@ -0,0 +1,11 @@ +// expect-error: Node Run must be callable as +#include "signature_fixture.h" +NodeResult Run(const Inputs&, const Options&, const NoModels&) { + return TextBatch{}; +} +auto Spec() { + return MakeNodeSpec(InputsContract(), + PreservedOutput("output", "input"), + Parameters{}, &Run); +} +REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/run_writes_noparameters.cpp b/tests/fixtures/spec_signatures/run_writes_noparameters.cpp new file mode 100644 index 00000000..61bcbbb9 --- /dev/null +++ b/tests/fixtures/spec_signatures/run_writes_noparameters.cpp @@ -0,0 +1,11 @@ +// expect-error: Node Run must be callable as +#include "signature_fixture.h" +NodeResult Run(const Inputs&, const NoParameters&, const Models&) { + return TextBatch{}; +} +auto Spec() { + return MakeNodeSpec(InputsContract(), + PreservedOutput("output", "input"), + ModelContract(), &Run); +} +REGISTER_FUNCTION_NODE(SignatureProbeNode, Spec()); diff --git a/tests/fixtures/spec_signatures/signature_fixture.h b/tests/fixtures/spec_signatures/signature_fixture.h index f57c1464..7d4c98fa 100644 --- a/tests/fixtures/spec_signatures/signature_fixture.h +++ b/tests/fixtures/spec_signatures/signature_fixture.h @@ -15,14 +15,14 @@ inline auto InputsContract() { return InputsOf({Required("input", &Inputs::input)}); } inline auto ModelContract() { - return ModelsOf({Llm("generator", "bind_model", &Models::generator)}); + return ModelsOf( + {Model("generator", "bind_model", &Models::generator)}); } template -auto Batch(Fn fn) { - return MakeBatchSpec(InputsContract(), - PreservedOutput("output", "input"), - Parameters{}, ModelContract(), fn); +auto ParamsAndModels(Fn fn) { + return MakeNodeSpec(InputsContract(), + PreservedOutput("output", "input"), + Parameters{}, ModelContract(), fn); } -inline std::string Text(const std::string& text) { return text; } } // namespace signature_fixture using namespace signature_fixture; diff --git a/tests/fixtures/spec_signatures/valid_signatures.cpp b/tests/fixtures/spec_signatures/valid_signatures.cpp index 1616b2af..72979f50 100644 --- a/tests/fixtures/spec_signatures/valid_signatures.cpp +++ b/tests/fixtures/spec_signatures/valid_signatures.cpp @@ -1,193 +1,69 @@ // expect-ok -#include - #include "signature_fixture.h" -struct ImplicitText { - operator std::string() const { return "text"; } -}; struct Outputs { TextBatch texts; Int32Batch scores; }; -NodeResult Run2(const Inputs& inputs, const NoParameters&) { +auto Ports() { + return std::make_pair(InputsContract(), + PreservedOutput("output", "input")); +} +NodeResult RunInputs(const Inputs& inputs) { return *inputs.input; } +NodeResult RunSession(const Inputs& inputs, + const SessionResources&) { return *inputs.input; } -NodeResult Run3(const Inputs& inputs, const NoParameters&, - const NoModels&) { +NodeResult RunParams(const Inputs& inputs, const Options&) { return *inputs.input; } -NodeResult Run4(const Inputs& inputs, const NoParameters&, - const NoModels&, const SessionResources&) { +NodeResult RunModels(const Inputs& inputs, const Models&) { return *inputs.input; } -struct Logic { - NodeResult Run2(const Inputs& i, const NoParameters&) { - return *i.input; - } - NodeResult Run3(const Inputs& i, const NoParameters&, - const NoModels&) { - return *i.input; - } - NodeResult Run4(const Inputs& i, const NoParameters&, - const NoModels&, const SessionResources&) { - return *i.input; - } - NodeResult Const2(const Inputs& i, const NoParameters&) const { - return *i.input; - } - NodeResult Const3(const Inputs& i, const NoParameters&, - const NoModels&) const { - return *i.input; - } - NodeResult Const4(const Inputs& i, const NoParameters&, - const NoModels&, const SessionResources&) const { - return *i.input; - } -}; -template -auto NoParamsBatch(Fn fn) { - return MakeBatchSpec(InputsContract(), - PreservedOutput("output", "input"), fn); +NodeResult RunAll(const Inputs& inputs, const Options&, + const Models&) { + return *inputs.input; } -REGISTER_FUNCTION_NODE(ValidFree2Node, NoParamsBatch(&Run2)); -REGISTER_FUNCTION_NODE(ValidFree3Node, NoParamsBatch(&Run3)); -REGISTER_FUNCTION_NODE(ValidFree4Node, NoParamsBatch(&Run4)); -REGISTER_FUNCTION_NODE(ValidMember2Node, NoParamsBatch(&Logic::Run2)); -REGISTER_FUNCTION_NODE(ValidMember3Node, NoParamsBatch(&Logic::Run3)); -REGISTER_FUNCTION_NODE(ValidMember4Node, NoParamsBatch(&Logic::Run4)); -REGISTER_FUNCTION_NODE(ValidConstMember2Node, NoParamsBatch(&Logic::Const2)); -REGISTER_FUNCTION_NODE(ValidConstMember3Node, NoParamsBatch(&Logic::Const3)); -REGISTER_FUNCTION_NODE(ValidConstMember4Node, NoParamsBatch(&Logic::Const4)); -auto ValidMapSpec() { - return MakeMapSpec(Input("input"), Output("output"), - &Text); +NodeResult RunAllSession(const Inputs& inputs, const Options&, + const Models&, const SessionResources&) { + return *inputs.input; } -REGISTER_FUNCTION_NODE(ValidMapNode, ValidMapSpec()); +auto InputsSpec() { + auto [inputs, output] = Ports(); + return MakeNodeSpec(std::move(inputs), std::move(output), &RunInputs); +} +REGISTER_FUNCTION_NODE(ValidInputsNode, InputsSpec()); +auto SessionSpec() { + auto [inputs, output] = Ports(); + return MakeNodeSpec(std::move(inputs), std::move(output), &RunSession); +} +REGISTER_FUNCTION_NODE(ValidSessionNode, SessionSpec()); +auto ParamsSpec() { + auto [inputs, output] = Ports(); + return MakeNodeSpec(std::move(inputs), std::move(output), + Parameters{}, &RunParams); +} +REGISTER_FUNCTION_NODE(ValidParamsNode, ParamsSpec()); +auto ModelsSpec() { + auto [inputs, output] = Ports(); + return MakeNodeSpec(std::move(inputs), std::move(output), ModelContract(), + &RunModels); +} +REGISTER_FUNCTION_NODE(ValidModelsNode, ModelsSpec()); +REGISTER_FUNCTION_NODE(ValidAllNode, ParamsAndModels(&RunAll)); +REGISTER_FUNCTION_NODE(ValidAllSessionNode, ParamsAndModels(&RunAllSession)); +auto LambdaSpec() { + return ParamsAndModels([](const Inputs& inputs, const Options&, + const Models&) -> NodeResult { + return MapPayloads(*inputs.input, + [](const std::string& text) { return text; }); + }); +} +REGISTER_FUNCTION_NODE(ValidLambdaNode, LambdaSpec()); auto ValidMultiOutputSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsContract(), OutputsOf({Produced("texts", &Outputs::texts), Produced("scores", &Outputs::scores)}), - [](const Inputs&, const NoParameters&) -> NodeResult { - return Outputs{}; - }); + [](const Inputs&) -> NodeResult { return Outputs{}; }); } REGISTER_FUNCTION_NODE(ValidMultiOutputNode, ValidMultiOutputSpec()); -std::string HookString(const std::string& text) { return text; } -std::string ParameterHookString(const std::string& text, const Options&) { - return text; -} -auto LlmStringSpec() { return MakeLlmTextSpec(&HookString, &HookString); } -REGISTER_FUNCTION_NODE(ValidLlmStringNode, LlmStringSpec()); -auto ParameterLlmStringSpec() { - return MakeLlmTextSpec(Parameters{}, &ParameterHookString, - &ParameterHookString); -} -REGISTER_FUNCTION_NODE(ValidParameterLlmStringNode, ParameterLlmStringSpec()); -const char* HookPointer(const std::string& text) { return "text"; } -const char* ParameterHookPointer(const std::string& text, const Options&) { - return "text"; -} -auto LlmPointerSpec() { return MakeLlmTextSpec(&HookPointer, &HookPointer); } -REGISTER_FUNCTION_NODE(ValidLlmPointerNode, LlmPointerSpec()); -auto ParameterLlmPointerSpec() { - return MakeLlmTextSpec(Parameters{}, &ParameterHookPointer, - &ParameterHookPointer); -} -REGISTER_FUNCTION_NODE(ValidParameterLlmPointerNode, ParameterLlmPointerSpec()); -std::string_view HookView(const std::string& text) { return text; } -std::string_view ParameterHookView(const std::string& text, const Options&) { - return text; -} -auto LlmViewSpec() { return MakeLlmTextSpec(&HookView, &HookView); } -REGISTER_FUNCTION_NODE(ValidLlmViewNode, LlmViewSpec()); -auto ParameterLlmViewSpec() { - return MakeLlmTextSpec(Parameters{}, &ParameterHookView, - &ParameterHookView); -} -REGISTER_FUNCTION_NODE(ValidParameterLlmViewNode, ParameterLlmViewSpec()); -NodeResult HookResultString(const std::string& text) { - return text; -} -NodeResult ParameterHookResultString(const std::string& text, - const Options&) { - return text; -} -auto LlmResultStringSpec() { - return MakeLlmTextSpec(&HookResultString, &HookResultString); -} -REGISTER_FUNCTION_NODE(ValidLlmResultStringNode, LlmResultStringSpec()); -auto ParameterLlmResultStringSpec() { - return MakeLlmTextSpec(Parameters{}, &ParameterHookResultString, - &ParameterHookResultString); -} -REGISTER_FUNCTION_NODE(ValidParameterLlmResultStringNode, - ParameterLlmResultStringSpec()); -NodeResult HookResultPointer(const std::string& text) { - return "text"; -} -NodeResult ParameterHookResultPointer(const std::string& text, - const Options&) { - return "text"; -} -auto LlmResultPointerSpec() { - return MakeLlmTextSpec(&HookResultPointer, &HookResultPointer); -} -REGISTER_FUNCTION_NODE(ValidLlmResultPointerNode, LlmResultPointerSpec()); -auto ParameterLlmResultPointerSpec() { - return MakeLlmTextSpec(Parameters{}, &ParameterHookResultPointer, - &ParameterHookResultPointer); -} -REGISTER_FUNCTION_NODE(ValidParameterLlmResultPointerNode, - ParameterLlmResultPointerSpec()); -NodeResult HookResultView(const std::string& text) { - return std::string_view(text); -} -NodeResult ParameterHookResultView(const std::string& text, - const Options&) { - return std::string_view(text); -} -auto LlmResultViewSpec() { - return MakeLlmTextSpec(&HookResultView, &HookResultView); -} -REGISTER_FUNCTION_NODE(ValidLlmResultViewNode, LlmResultViewSpec()); -auto ParameterLlmResultViewSpec() { - return MakeLlmTextSpec(Parameters{}, &ParameterHookResultView, - &ParameterHookResultView); -} -REGISTER_FUNCTION_NODE(ValidParameterLlmResultViewNode, - ParameterLlmResultViewSpec()); -ImplicitText HookImplicit(const std::string& text) { return {}; } -ImplicitText ParameterHookImplicit(const std::string& text, const Options&) { - return {}; -} -auto LlmImplicitSpec() { return MakeLlmTextSpec(&HookImplicit, &HookImplicit); } -REGISTER_FUNCTION_NODE(ValidLlmImplicitNode, LlmImplicitSpec()); -auto ParameterLlmImplicitSpec() { - return MakeLlmTextSpec(Parameters{}, &ParameterHookImplicit, - &ParameterHookImplicit); -} -REGISTER_FUNCTION_NODE(ValidParameterLlmImplicitNode, - ParameterLlmImplicitSpec()); -NodeResult HookResultImplicit(const std::string& text) { - return ImplicitText{}; -} -NodeResult ParameterHookResultImplicit(const std::string& text, - const Options&) { - return ImplicitText{}; -} -auto LlmResultImplicitSpec() { - return MakeLlmTextSpec(&HookResultImplicit, &HookResultImplicit); -} -REGISTER_FUNCTION_NODE(ValidLlmResultImplicitNode, LlmResultImplicitSpec()); -auto ParameterLlmResultImplicitSpec() { - return MakeLlmTextSpec(Parameters{}, &ParameterHookResultImplicit, - &ParameterHookResultImplicit); -} -REGISTER_FUNCTION_NODE(ValidParameterLlmResultImplicitNode, - ParameterLlmResultImplicitSpec()); -auto ValidByValueSpec() { - return MakeLlmTextSpec([](std::string text) { return text; }, - [](std::string text) { return text; }); -} -REGISTER_FUNCTION_NODE(ValidByValueNode, ValidByValueSpec()); diff --git a/tests/integration/pipeline/test_pipeline_catalog_validator.cpp b/tests/integration/pipeline/test_pipeline_catalog_validator.cpp index 9729c89b..a769cddc 100644 --- a/tests/integration/pipeline/test_pipeline_catalog_validator.cpp +++ b/tests/integration/pipeline/test_pipeline_catalog_validator.cpp @@ -45,7 +45,6 @@ NodeDefinition StudioCatalogProbeDefinition() { definition.node_type = StudioCatalogProbeNode::kNodeType; definition.category = "test"; definition.description = "Catalog auto-discovery probe"; - definition.biz_names = {"keyword_match"}; return definition; } diff --git a/tests/tooling/test_scaffold_custom_node.py b/tests/tooling/test_scaffold_custom_node.py index 35a09388..e0e0dbbc 100755 --- a/tests/tooling/test_scaffold_custom_node.py +++ b/tests/tooling/test_scaffold_custom_node.py @@ -84,14 +84,14 @@ def test_llm_starter_is_the_actual_generator_template(self): "StarterLlmNode", "LLM authoring starter", "llm", ("input", "TextBatch", "1:1", "preserve"), ("output", "TextBatch", "1:1", "preserve")) - self.assertEqual(generated, source.replace("StarterLlmSpec", "StarterLlmNodeSpec")) + self.assertEqual(generated, source) result = self.run_cli("SwappedNode", "--kind", "model", "-m", "llm", "--in-port", "output:TextBatch", "--out-port", "input:TextBatch", "--description", 'StarterLlmNode "input"\n', "--dry-run") self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn('Input("output")', result.stdout) - self.assertIn('Output("input")', result.stdout) - self.assertIn('MakeLlmTextSpec', result.stdout) + self.assertIn('Required("output", &Inputs::input)', result.stdout) + self.assertIn('PreservedOutput("input", "output")', result.stdout) + self.assertIn('GenerateParameters(', result.stdout) self.assertIn('REGISTER_FUNCTION_NODE(SwappedNode', result.stdout) def test_control_starter_and_business_test_generation(self): @@ -144,8 +144,8 @@ def test_write_test_creates_source_and_test_file(self): source_file = Path(temp) / "src" / "custom_nodes" / "awesome_feature_node.cpp" self.assertTrue(source_file.exists()) source_content = source_file.read_text(encoding="utf-8") - self.assertIn("REGISTER_FUNCTION_NODE(AwesomeFeatureNode, AwesomeFeatureNodeSpec());", source_content) - self.assertIn("MakeMapSpec", source_content) + self.assertIn("REGISTER_FUNCTION_NODE(AwesomeFeatureNode, Spec());", source_content) + self.assertIn("MapPayloads(*inputs.input, &Transform)", source_content) # 检查在 tests/unit/nodes/test_awesome_feature_node.cpp 生成的测试文件 test_file = Path(temp) / "tests" / "unit" / "nodes" / "test_awesome_feature_node.cpp" @@ -179,7 +179,7 @@ def test_write_test_dry_run_does_not_create_files(self): source_file = Path(temp) / "src" / "custom_nodes" / "dry_run_node.cpp" test_file = Path(temp) / "tests" / "unit" / "nodes" / "test_dry_run_node.cpp" self.assertIn(f"--- {source_file} (new file) ---", result.stdout) - self.assertIn("REGISTER_FUNCTION_NODE(DryRunNode, DryRunNodeSpec());", result.stdout) + self.assertIn("REGISTER_FUNCTION_NODE(DryRunNode, Spec());", result.stdout) self.assertIn(f"--- {test_file} (new test file) ---", result.stdout) self.assertIn("TEST(CustomNodeCatalogTest, DryRunNode_", result.stdout) self.assertIn("Tests in tests/unit/nodes/test_*.cpp are discovered automatically.", result.stdout) @@ -335,7 +335,7 @@ def edit_before_second_capture(source, destination, *args, **kwargs): self.assertEqual(second.read_text(), "second user edit") def test_all_model_capabilities_use_function_contracts(self): - for capability, call in (("llm", "MakeLlmTextSpec"), + for capability, call in (("llm", "LlmCall"), ("embedding", "EmbeddingCall"), ("asr", "AsrCall"), ("ocr", "OcrCall"), ("rerank", "RerankCall")): @@ -344,16 +344,17 @@ def test_all_model_capabilities_use_function_contracts(self): capability, "--dry-run", "--write-test") self.assertEqual(result.returncode, 0, result.stderr) self.assertIn(call, result.stdout) + self.assertIn("MakeNodeSpec", result.stdout) self.assertIn("REGISTER_FUNCTION_NODE", result.stdout) self.assertNotIn("ProcessNode", result.stdout) self.assertNotIn("ModelBoundNode", result.stdout) - def test_conversion_and_flow_contracts_use_batch_spec(self): + def test_conversion_and_flow_contracts_use_node_spec(self): for options in (["--out-port", "output:Int32Batch"], ["--in-port", "input:TextBatch:1:N:generate_sub_id"]): result = self.run_cli("SelectedNode", *options, "--dry-run", "--write-test") self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("MakeBatchSpec", result.stdout) + self.assertIn("MakeNodeSpec", result.stdout) self.assertIn("domain transformation is not implemented", result.stdout) self.assertIn("UnimplementedDomainLogicFailsCleanly", result.stdout) self.assertNotIn("ProcessNode", result.stdout) @@ -365,7 +366,7 @@ def test_embedding_and_control_use_existing_function_contracts(self): self.assertEqual(result.returncode, 0, result.stderr) for expected in ['Required("texts", &Inputs::texts)', 'PreservedOutput("vectors", "texts")', - 'Embedding("encoder", "bind_model", &Models::encoder)', + 'Model("encoder", "bind_model", &Models::encoder)', 'models.encoder.Embed(*input.texts)', 'EncoderNode_ControlledExecutionAndModelFailure']: self.assertIn(expected, result.stdout) @@ -377,14 +378,15 @@ def test_embedding_and_control_use_existing_function_contracts(self): def test_generates_function_nodes(self): result = self.run_cli("BasicMapNode", "--dry-run") self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("MakeMapSpec", result.stdout) - self.assertIn("REGISTER_FUNCTION_NODE(BasicMapNode, BasicMapNodeSpec());", result.stdout) + self.assertIn("MakeNodeSpec", result.stdout) + self.assertIn("REGISTER_FUNCTION_NODE(BasicMapNode, Spec());", result.stdout) self.assertIn("Transform(const std::string& input)", result.stdout) + self.assertIn("NodeResult Run(const Inputs& inputs)", result.stdout) result = self.run_cli("BasicLlmNode", "--kind", "model", "-m", "llm", "--dry-run") self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("MakeLlmTextSpec", result.stdout) - self.assertIn("REGISTER_FUNCTION_NODE(BasicLlmNode, BasicLlmNodeSpec());", result.stdout) + self.assertIn("MakeNodeSpec", result.stdout) + self.assertIn("REGISTER_FUNCTION_NODE(BasicLlmNode, Spec());", result.stdout) self.assertIn("BuildPrompt", result.stdout) self.assertIn("FormatAnswer", result.stdout) diff --git a/tests/unit/core/test_node_base_contracts.cpp b/tests/unit/core/test_node_base_contracts.cpp index ce7e991a..1c244ce4 100644 --- a/tests/unit/core/test_node_base_contracts.cpp +++ b/tests/unit/core/test_node_base_contracts.cpp @@ -537,14 +537,13 @@ struct MockAsrModels { }; auto MockAsrSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf{ Required(kTestAudioInputs.name, &MockAsrInputs::audio)}, PreservedOutput(kTestTranscripts.name, kTestAudioInputs.name), ModelsOf{ Model("transcriber", "bind_model", &MockAsrModels::transcriber)}, - [](const MockAsrInputs& inputs, const NoParameters&, - const MockAsrModels& models) { + [](const MockAsrInputs& inputs, const MockAsrModels& models) { return models.transcriber.Transcribe(*inputs.audio); }); } diff --git a/tests/unit/core/test_validated_pipeline_plan.cpp b/tests/unit/core/test_validated_pipeline_plan.cpp index 25759286..ca171660 100644 --- a/tests/unit/core/test_validated_pipeline_plan.cpp +++ b/tests/unit/core/test_validated_pipeline_plan.cpp @@ -313,7 +313,6 @@ TEST_F(ValidatedPipelinePlanTest, DiagnosticCodeNameTableDriven) { {DiagnosticCode::kConfigFieldRange, "CONFIG_FIELD_RANGE"}, {DiagnosticCode::kConfigFieldEnum, "CONFIG_FIELD_ENUM"}, {DiagnosticCode::kUnknownModelReference, "UNKNOWN_MODEL_REFERENCE"}, - {DiagnosticCode::kNodeBizMismatch, "NODE_BIZ_MISMATCH"}, {DiagnosticCode::kMissingInputProducer, "MISSING_INPUT_PRODUCER"}, {DiagnosticCode::kDuplicatePortProducer, "DUPLICATE_PORT_PRODUCER"}, {DiagnosticCode::kMissingBizOutput, "MISSING_BIZ_OUTPUT"}, @@ -329,7 +328,7 @@ TEST_F(ValidatedPipelinePlanTest, DiagnosticCodeNameTableDriven) { {DiagnosticCode::kInvalidBuildState, "INVALID_BUILD_STATE"}, }; - EXPECT_EQ(cases.size(), 42u); + EXPECT_EQ(cases.size(), 41u); std::unordered_set names; for (const auto& item : cases) { std::string name = DiagnosticCodeName(item.code); @@ -865,46 +864,6 @@ TEST_F(ValidatedPipelinePlanTest, EXPECT_TRUE(PipelineValidator::Validate(pipeline_json).ok); } -class RestrictedBusinessNode : public INode { - public: - inline static constexpr char kNodeType[] = "RestrictedBusinessNode"; - bool Init(const NodeInitContext&) override { return true; } - int Process(AlgContext*) override { return 0; } - const std::string& Name() const override { - static const std::string n = kNodeType; - return n; - } -}; - -inline NodeDefinition MakeRestrictedNodeDef() { - NodeDefinition def; - def.node_type = RestrictedBusinessNode::kNodeType; - def.category = "biz"; - def.biz_names = {"restricted_only_biz"}; - def.description = "Restricted test node"; - return def; -} -REGISTER_NODE_WITH_DEFINITION(RestrictedBusinessNode, MakeRestrictedNodeDef()); - -TEST_F(ValidatedPipelinePlanTest, RejectsNodeFromDifferentBusiness) { - nlohmann::json pipeline_json = { - {"biz_name", "doc_qa"}, - {"models", nlohmann::json::array()}, - {"pipeline", - nlohmann::json::array({{{"id", "wrong_business_node"}, - {"node_type", "RestrictedBusinessNode"}, - {"depends_on", nlohmann::json::array()}}})}}; - - auto plan = PipelineValidator::ValidateAndPlan(pipeline_json); - EXPECT_FALSE(plan.report.ok); - EXPECT_NE(std::find_if(plan.report.diagnostics.begin(), - plan.report.diagnostics.end(), - [](const auto& item) { - return item.code == DiagnosticCode::kNodeBizMismatch; - }), - plan.report.diagnostics.end()); -} - TEST_F(ValidatedPipelinePlanTest, DeterministicLexicalModelPathValidationWithoutDeploymentContext) { if (!BackendRegistry::Instance().Find("mock_path_backend").has_value()) { diff --git a/tests/unit/nodes/test_common_nodes.cpp b/tests/unit/nodes/test_common_nodes.cpp index 3403a68d..30e7f2bf 100644 --- a/tests/unit/nodes/test_common_nodes.cpp +++ b/tests/unit/nodes/test_common_nodes.cpp @@ -9,6 +9,7 @@ #include #include +#include "contracts/config_schema_validation.h" #include "core/alg_context.h" #include "core/common_contracts.h" #include "core/node_registry.h" @@ -870,18 +871,21 @@ void CheckScaffoldExecution(const std::string& name, const std::string& model, ASSERT_NE(node, nullptr); // 解析后的键与逻辑名不同:同时覆盖类型化绑定。 ValidatedNodePlan plan; - plan.normalized_config = {{"bind_model", model}}; plan.ports = {{"input", "source", BlackboardTypeTraits::TypeName(), "1:1", "preserve", "request", PortDirection::kInput}, {"output", "result", BlackboardTypeTraits::TypeName(), "1:1", "preserve", "request", PortDirection::kOutput}}; const auto def = PipelineCatalog::FindNode(name); - if (def) { - for (const auto& dep : def->model_dependencies) { - plan.model_bindings.push_back( - {dep.name, dep.capability, dep.config_field, model}); - } + ASSERT_TRUE(def.has_value()); + nlohmann::json config = nlohmann::json::object(); + for (const auto& dep : def->model_dependencies) { + config[dep.config_field] = model; + plan.model_bindings.push_back( + {dep.name, dep.capability, dep.config_field, model}); } + // 与 Validator 相同:按 Definition 字段填入默认值。 + ASSERT_TRUE(ValidateAndNormalizeFields(def->config_fields, config, + &plan.normalized_config, nullptr)); ASSERT_TRUE(node->Init({&plan, session})); Input input; for (const auto& id : @@ -1261,6 +1265,30 @@ TEST_F(CommonNodesTest, PromptAndGeneratedLlmNodesFailWithoutPublishing) { } } +TEST_F(CommonNodesTest, GeneratedLlmNodeReadsGenerationOptionsFromConfig) { + auto model = std::make_shared(); + ASSERT_TRUE(RegisterTestModel(session_ctx_->GetModelManager(), + "prompt_contract", model, "v1")); + // 未配置时使用默认生成参数;配置后原样传给模型。 + const std::vector> cases = { + {nlohmann::json{{"bind_model", "prompt_contract"}}, 128, 0.7f}, + {nlohmann::json{{"bind_model", "prompt_contract"}, + {"max_tokens", 2048}, + {"temperature", 0.0}}, + 2048, 0.0f}}; + for (const auto& [config, max_tokens, temperature] : cases) { + SCOPED_TRACE(config.dump()); + auto node = NodeRegistry::Instance().Create("ScaffoldModelLlmNode"); + ASSERT_NE(node, nullptr); + ASSERT_TRUE(InitNodeForTest(*node, config, session_ctx_.get())); + AlgContext ctx; + ctx.Publish("input", TextBatch{{17, 4, "a"}}); + ASSERT_EQ(node->Process(&ctx), 0) << ctx.GetErrorMessage(); + EXPECT_EQ(model->last_options.max_tokens, max_tokens); + EXPECT_FLOAT_EQ(model->last_options.temperature, temperature); + } +} + TEST_F(CommonNodesTest, PromptContextIsExplicitAndRequiredWhenUsed) { auto node = NodeRegistry::Instance().Create("PromptGuidedLlmNode"); auto model = std::make_shared(); diff --git a/tests/unit/nodes/test_function_node.cpp b/tests/unit/nodes/test_function_node.cpp index e2e0d654..3686fb2d 100644 --- a/tests/unit/nodes/test_function_node.cpp +++ b/tests/unit/nodes/test_function_node.cpp @@ -29,6 +29,34 @@ namespace llm_edgeflow { namespace { +struct TextInputs { + const TextBatch* input = nullptr; +}; + +// 逐条文本转换夹具:与作者用 MapPayloads 编写的 Run 相同。 +template +auto TextMapSpec(Fn fn) { + return MakeNodeSpec( + InputsOf({Required("input", &TextInputs::input)}), + PreservedOutput("output", "input"), + [fn = std::move(fn)](const TextInputs& inputs) -> NodeResult { + return MapPayloads(*inputs.input, fn); + }); +} + +template +auto TextMapSpec(Parameters params, Fn fn) { + return MakeNodeSpec( + InputsOf({Required("input", &TextInputs::input)}), + PreservedOutput("output", "input"), std::move(params), + [fn = std::move(fn)](const TextInputs& inputs, + const ParamsT& params) -> NodeResult { + return MapPayloads(*inputs.input, [&](const std::string& text) { + return fn(text, params); + }); + }); +} + struct ComplexParams { std::string prefix; int limit = 0; @@ -77,13 +105,12 @@ struct ComplexInputs { }; auto ComplexSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({Required("input", &ComplexInputs::input), Optional("context", &ComplexInputs::context)}), PreservedOutput("output", "input"), ComplexConfig(), - ModelsOf{}, - [](const ComplexInputs& input, const ComplexParams& params, - const NoModels&) -> NodeResult { + [](const ComplexInputs& input, + const ComplexParams& params) -> NodeResult { return MapPayloads(*input.input, [&](const std::string& text) { return params.prefix + text; }); @@ -91,30 +118,6 @@ auto ComplexSpec() { } REGISTER_FUNCTION_NODE(ComplexParserNode, ComplexSpec()); -int local_logic_constructions = 0; -class RequestLocalLogic { - public: - RequestLocalLogic() { ++local_logic_constructions; } - NodeResult Run(const ComplexInputs& inputs, const NoParameters&, - const NoModels&) { - return MapPayloads(*inputs.input, [&](const std::string& text) { - accumulated_ += text; - return accumulated_; - }); - } - - private: - std::string accumulated_; -}; - -auto RequestLocalSpec() { - return MakeBatchSpec( - InputsOf({Required("input", &ComplexInputs::input)}), - PreservedOutput("output", "input"), Parameters{}, - ModelsOf{}, &RequestLocalLogic::Run); -} -REGISTER_FUNCTION_NODE(RequestLocalNode, RequestLocalSpec()); - struct CleanParams { std::string prefix; }; @@ -136,8 +139,7 @@ std::string CleanTextFn(const std::string& in, const CleanParams& params) { } auto CleanSpec() { - return MakeMapSpec(Input("input"), Output("output"), - CleanConfig(), &CleanTextFn) + return TextMapSpec(CleanConfig(), &CleanTextFn) .Description("Clean text map node"); } @@ -153,23 +155,20 @@ NodeResult FailableCleanFn(const std::string& in) { } auto FailableSpec() { - return MakeMapSpec(Input("input"), Output("output"), - &FailableCleanFn) - .Description("Failable map node"); + return TextMapSpec(&FailableCleanFn).Description("Failable map node"); } REGISTER_FUNCTION_NODE(FailableMapNode, FailableSpec()); // 失败时不带自身消息,由框架给出 Map 函数名。 auto SilentFailureSpec() { - return MakeMapSpec(Input("input"), Output("output"), - [](const std::string& in) { - if (in == "FAIL") { - return NodeResult::Failure( - NodeErrorKind::kBusinessError, ""); - } - return NodeResult::Success(in); - }); + return TextMapSpec([](const std::string& in) { + if (in == "FAIL") { + return NodeResult::Failure(NodeErrorKind::kBusinessError, + ""); + } + return NodeResult::Success(in); + }); } REGISTER_FUNCTION_NODE(SilentFailureMapNode, SilentFailureSpec()); @@ -182,9 +181,7 @@ std::string UpperFn(const std::string& in) { } auto UpperSpec() { - return MakeMapSpec(Input("input"), Output("output"), - &UpperFn) - .Description("Uppercase map node"); + return TextMapSpec(&UpperFn).Description("Uppercase map node"); } REGISTER_FUNCTION_NODE(UpperMapNode, UpperSpec()); @@ -202,8 +199,7 @@ auto MoveOnlyMapSpec() { return params.prefix + *owned + input; }; static_assert(!std::is_copy_constructible_v); - return MakeMapSpec(Input("input"), Output("output"), - CleanConfig(), std::move(transform)) + return TextMapSpec(CleanConfig(), std::move(transform)) .WithControls({ReplaceFields(3005, "set_prefix", {"prefix"})}); } REGISTER_FUNCTION_NODE(MoveOnlyMapNode, MoveOnlyMapSpec()); @@ -218,8 +214,7 @@ auto MoveOnlyResultMapSpec() { return NodeResult::Success(*owned + input); }; static_assert(!std::is_copy_constructible_v); - return MakeMapSpec(Input("input"), Output("output"), - std::move(transform)); + return TextMapSpec(std::move(transform)); } REGISTER_FUNCTION_NODE(MoveOnlyResultMapNode, MoveOnlyResultMapSpec()); @@ -324,17 +319,17 @@ NodeResult AnswerBatchFn(const AnswerInputs& inputs, } auto AnswerBatchSpec() { - return MakeBatchSpec(InputsOf({ - Required("questions", &AnswerInputs::questions), - Optional("context", &AnswerInputs::context, - InputFlow::AggregateByRequest), - }), - PreservedOutput("output", "questions"), - AnswerConfig(), - ModelsOf({ - Llm("generator", "bind_model", &AnswerModels::llm), - }), - &AnswerBatchFn) + return MakeNodeSpec(InputsOf({ + Required("questions", &AnswerInputs::questions), + Optional("context", &AnswerInputs::context, + InputFlow::AggregateByRequest), + }), + PreservedOutput("output", "questions"), + AnswerConfig(), + ModelsOf({ + Model("generator", "bind_model", &AnswerModels::llm), + }), + &AnswerBatchFn) .Description("Batch answering node with retry"); } @@ -348,15 +343,13 @@ struct OptionalValueInputs { std::atomic optional_value_run_count{0}; auto OptionalValueSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({ Required("input", &OptionalValueInputs::input), OptionalValue("context", &OptionalValueInputs::context), }), - PreservedOutput("output", "input"), Parameters{}, - ModelsOf{}, - [](const OptionalValueInputs& inputs, const NoParameters&, - const NoModels&) -> NodeResult { + PreservedOutput("output", "input"), + [](const OptionalValueInputs& inputs) -> NodeResult { ++optional_value_run_count; std::string prefix = "[no-context] "; if (inputs.context) { @@ -377,14 +370,14 @@ struct TwoStageModels { }; auto TwoStageSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({Required("questions", &AnswerInputs::questions)}), PreservedOutput("output", "questions"), ModelsOf({ - Llm("draft", "draft_model", &TwoStageModels::draft), - Llm("revise", "revise_model", &TwoStageModels::revise), + Model("draft", "draft_model", &TwoStageModels::draft), + Model("revise", "revise_model", &TwoStageModels::revise), }), - [](const AnswerInputs& inputs, const NoParameters&, + [](const AnswerInputs& inputs, const TwoStageModels& models) -> NodeResult { auto draft = models.draft.Generate(*inputs.questions); if (!draft.ok()) return draft; @@ -404,166 +397,17 @@ NodeDefinition TwoStageDefinition() { REGISTER_NODE_WITH_DEFINITION(TwoStageHarnessNode, TwoStageDefinition()); -// 对象逻辑 Batch Node -struct LogicAnswerParams { - std::string tag = "logic"; -}; - -auto LogicAnswerConfig() { - return Parameters({ - Field("tag", &LogicAnswerParams::tag).Default("logic"), - }); -} - -class AnswerLogic { - public: - NodeResult Run(const AnswerInputs& inputs, - const LogicAnswerParams& params, - const AnswerModels& models) const { - if (!inputs.questions || inputs.questions->empty()) { - return NodeResult::Success(TextBatch{}); - } - auto prompts = MapPayloads(*inputs.questions, [&](const std::string& text) { - return params.tag + ":" + text; - }); - return models.llm.Generate(prompts); - } -}; - -auto LogicBatchSpec() { - return MakeBatchSpec(InputsOf({ - Required("questions", &AnswerInputs::questions), - }), - PreservedOutput("output", "questions"), - LogicAnswerConfig(), - ModelsOf({ - Llm("generator", "bind_model", &AnswerModels::llm), - }), - &AnswerLogic::Run) - .Description("Logic class batch node"); -} - -REGISTER_FUNCTION_NODE(LogicBatchNode, LogicBatchSpec()); - -// LLM 文本快捷 Node -struct ShortcutParams { - std::string suffix = "!"; -}; - -auto ShortcutConfig() { - return Parameters({ - Field("suffix", &ShortcutParams::suffix).Default("!"), - }); -} - -auto ShortcutSpec() { - return MakeLlmTextSpec( - Input("prompt"), Output("text"), ShortcutConfig(), - [](const std::string& in, const ShortcutParams&) { - return "prompt:" + in; - }, - [](const std::string& out, const ShortcutParams& p) { - return out + p.suffix; - }); -} - -REGISTER_FUNCTION_NODE(LlmShortcutNode, ShortcutSpec()); - -int formatting_calls = 0; -NodeResult FormatUntilSecond(const std::string& text) { - ++formatting_calls; - if (text == "ans:second") { - return NodeResult::Failure(NodeErrorKind::kBusinessError, - "second answer rejected", -9876); - } - return "formatted:" + text; -} - -struct ConstOnlyFormatter { - NodeResult operator()(const std::string& text) const { - return FormatUntilSecond(text); - } - NodeResult operator()(std::string&) const = delete; -}; - -struct ConstOnlyParameterFormatter { - NodeResult operator()(const std::string& text, - const ShortcutParams&) const { - return FormatUntilSecond(text); - } - NodeResult operator()(std::string&, - const ShortcutParams&) const = delete; -}; - -auto FailingFormatSpec() { - return MakeLlmTextSpec( - Input("input"), Output("output"), - [](const std::string& text) { return text; }, ConstOnlyFormatter{}); -} -REGISTER_FUNCTION_NODE(FailingFormatNode, FailingFormatSpec()); - -auto FailingParameterFormatSpec() { - return MakeLlmTextSpec( - Input("input"), Output("output"), ShortcutConfig(), - [](const std::string& text) { return text; }, - ConstOnlyParameterFormatter{}); -} -REGISTER_FUNCTION_NODE(FailingParameterFormatNode, - FailingParameterFormatSpec()); - -static std::string StarterBuildPrompt(const std::string& text) { - return "prompt:" + text; -} -static std::string StarterFormatAnswer(const std::string& text) { - return "formatted:" + text; -} - -auto StarterLlmTestSpec() { - return MakeLlmTextSpec(Input("input"), Output("output"), - &StarterBuildPrompt, &StarterFormatAnswer); -} -REGISTER_FUNCTION_NODE(StarterLlmTestNode, StarterLlmTestSpec()); - -auto ViewHooksSpec() { - return MakeLlmTextSpec( - Input("input"), Output("output"), - [](const std::string& text) { return std::string_view(text).substr(1); }, - [](const std::string& text) -> NodeResult { - return std::string_view(text).substr(4); - }); -} -REGISTER_FUNCTION_NODE(ViewHooksNode, ViewHooksSpec()); - -auto ResultViewHooksSpec() { - return MakeLlmTextSpec( - Input("input"), Output("output"), ShortcutConfig(), - [](const std::string& text, - const ShortcutParams&) -> NodeResult { - if (text == "REJECT") { - return NodeResult::Failure( - NodeErrorKind::kBusinessError, "prompt rejected", -8765); - } - return std::string_view(text).substr(1); - }, - [](const std::string& text, const ShortcutParams&) { - return std::string_view(text).substr(4); - }); -} -REGISTER_FUNCTION_NODE(ResultViewHooksNode, ResultViewHooksSpec()); - struct TextToScoreInputs { const TextBatch* texts = nullptr; }; auto TextToScoreSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({ Required("texts", &TextToScoreInputs::texts), }), PreservedOutput("scores", "texts"), - Parameters{}, ModelsOf({}), - [](const TextToScoreInputs& in, const NoParameters&, - const NoModels&) -> NodeResult { + [](const TextToScoreInputs& in) -> NodeResult { ScoreBatch scores; if (!in.texts) return NodeResult::Success(scores); scores.reserve(in.texts->size()); @@ -580,16 +424,15 @@ struct ContextPollutionInputs { }; auto FailingAfterPublishSpec() { - return MakeBatchSpec(InputsOf({ - Required("input", &ContextPollutionInputs::input), - }), - PreservedOutput("output", "input"), - Parameters{}, ModelsOf({}), - [](const ContextPollutionInputs&, const NoParameters&, - const NoModels&) -> NodeResult { - return NodeResult::Failure( - NodeErrorKind::kBusinessError, "forced error"); - }); + return MakeNodeSpec( + InputsOf({ + Required("input", &ContextPollutionInputs::input), + }), + PreservedOutput("output", "input"), + [](const ContextPollutionInputs&) -> NodeResult { + return NodeResult::Failure(NodeErrorKind::kBusinessError, + "forced error"); + }); } REGISTER_FUNCTION_NODE(FailingAfterPublishNode, FailingAfterPublishSpec()); @@ -603,7 +446,7 @@ struct BindingTestInputs { }; inline auto BindingTestSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({ Required("texts", &BindingTestInputs::texts), Optional("mask", &BindingTestInputs::mask), @@ -623,9 +466,8 @@ inline auto BindingTestSpec() { } return true; }), - ModelsOf{}, - [](const BindingTestInputs& in, const BindingTestParams&, - const NoModels&) -> NodeResult { + [](const BindingTestInputs& in, + const BindingTestParams&) -> NodeResult { return NodeResult::Success(*in.texts); }); } @@ -636,8 +478,7 @@ struct BindingMapParams { }; inline auto BindingMapSpec() { - return MakeMapSpec( - Input("input"), Output("output"), + return TextMapSpec( Parameters( { Field("require_extra", &BindingMapParams::require_extra) @@ -676,8 +517,7 @@ inline std::string ControlledMapFn(const std::string& in, } inline auto ControlledMapSpec() { - return MakeMapSpec( - Input("input"), Output("output"), + return TextMapSpec( Parameters( { Field("prefix", &ControlledMapParams::prefix).Default(""), @@ -712,8 +552,7 @@ struct NonCopyableMapParams { }; inline auto NonCopyableMapSpec() { - return MakeMapSpec( - Input("input"), Output("output"), + return TextMapSpec( Parameters( { Field("prefix", &NonCopyableMapParams::prefix).Default("nc:"), @@ -740,7 +579,7 @@ struct NonCopyableBatchParams { }; inline auto NonCopyableBatchSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({ Required("texts", &NonCopyableBatchInputs::texts), }), @@ -801,7 +640,7 @@ inline NodeResult ControlledBatchFn( } inline auto ControlledBatchSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf({ Required("texts", &ControlledBatchInputs::texts), }), @@ -840,7 +679,7 @@ struct ImageSummaryParams { std::string fault; }; auto ImageSummarySpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf{ Required("images", &ImageSummaryInputs::images)}, OutputsOf{ @@ -848,8 +687,8 @@ auto ImageSummarySpec() { Produced("lengths", &ImageSummaryOutputs::lengths, "images")}, Parameters{ Field("fault", &ImageSummaryParams::fault).Default("")}, - [](const ImageSummaryInputs& inputs, const ImageSummaryParams& params, - const NoModels&) -> NodeResult { + [](const ImageSummaryInputs& inputs, + const ImageSummaryParams& params) -> NodeResult { ImageSummaryOutputs output; for (const auto& image : *inputs.images) { output.names.emplace_back(image.req_id, image.sub_id, image.data); @@ -869,11 +708,10 @@ REGISTER_FUNCTION_NODE(ImageSummaryAuthorNode, ImageSummarySpec()); struct SourceInputs {}; auto SourceSpec() { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf{}, ProducedBatch("chunks", {"1:N", "generate_sub_id", "session"}), - [](const SourceInputs&, const NoParameters&, - const NoModels&) -> NodeResult { + [](const SourceInputs&) -> NodeResult { return TextBatch{{41, 0, "first"}, {41, 1, "second"}, {41, 2, "third"}}; }); } @@ -891,13 +729,13 @@ auto ComplexStateControlSpec() { {"required", {"prefix"}}, {"additionalProperties", false}, {"properties", {{"prefix", {{"type", "string"}}}}}}}}}}; - return MakeBatchSpec( + return MakeNodeSpec( InputsOf{Required("input", &ComplexInputs::input)}, PreservedOutput("output", "input"), Parameters{ Field("prefix", &CleanParams::prefix).Default("old:")}, - [](const ComplexInputs& input, const CleanParams& params, - const NoModels&) -> NodeResult { + [](const ComplexInputs& input, + const CleanParams& params) -> NodeResult { return MapPayloads(*input.input, [&](const std::string& value) { return params.prefix + value; }); @@ -1086,7 +924,7 @@ TEST(FunctionNodeTest, IntermediateFailureProducesNoOutput) { EXPECT_EQ(out, nullptr); } -TEST(FunctionNodeTest, MapItemFailureNamesNodeAndItem) { +TEST(FunctionNodeTest, MapPayloadsFailureNamesItem) { NodeHarness harness("FailableMapNode"); harness.TextInput("input", {"ok1", "FAIL", "ok3"}); auto result = harness.Run(); @@ -1095,7 +933,7 @@ TEST(FunctionNodeTest, MapItemFailureNamesNodeAndItem) { EXPECT_NE(result.diagnostic().find("Forced failure on keyword FAIL"), std::string::npos) << result.diagnostic(); - EXPECT_NE(result.diagnostic().find("FailableMapNode"), std::string::npos) + EXPECT_NE(result.diagnostic().find("MapPayloads"), std::string::npos) << result.diagnostic(); EXPECT_NE(result.diagnostic().find("sub_id=0"), std::string::npos) << result.diagnostic(); @@ -1106,9 +944,11 @@ TEST(FunctionNodeTest, MapItemFailureNamesNodeAndItem) { ASSERT_FALSE(silent_result.ok()); EXPECT_EQ(silent_result.process_code(), node_error::author_node::kBusinessError); - EXPECT_NE(silent_result.diagnostic().find( - "SilentFailureMapNode map function failed"), - std::string::npos) + EXPECT_NE( + silent_result.diagnostic().find("SilentFailureMapNode process failed"), + std::string::npos) + << silent_result.diagnostic(); + EXPECT_NE(silent_result.diagnostic().find("MapPayloads"), std::string::npos) << silent_result.diagnostic(); EXPECT_EQ(silent_result.Output("output"), nullptr); @@ -1474,115 +1314,6 @@ TEST(FunctionNodeTest, BatchModelCallRetrySucceedsWithoutPollutingContext) { EXPECT_TRUE(result.Context()->IsOk()); } -// 普通逻辑对象的 Run -TEST(FunctionNodeTest, BatchLogicClassExecutesPerRequest) { - auto mock_model = std::make_shared(); - NodeHarness harness("LogicBatchNode"); - harness.Config({{"bind_model", "test_llm"}, {"tag", "my_tag"}}); - harness.BindModel("test_llm", mock_model); - harness.TextInput("questions", {"hello"}); - - auto result = harness.Run(); - ASSERT_TRUE(result.ok()) << result.diagnostic(); - EXPECT_EQ(result.TextValues("output"), - (std::vector{"ans:my_tag:hello"})); -} - -// MakeLlmTextSpec 快捷方式 -TEST(FunctionNodeTest, - LlmFormattingFailureDoesNotPublishPartiallyFormattedBatch) { - for (const char* name : {"FailingFormatNode", "FailingParameterFormatNode"}) { - SCOPED_TRACE(name); - formatting_calls = 0; - auto model = std::make_shared(); - NodeHarness harness(name); - harness.Config({{"bind_model", "formatter_llm"}}); - harness.BindModel("formatter_llm", model); - harness.TextInput("input", {"first", "second", "third"}); - auto result = harness.Run(); - EXPECT_FALSE(result.ok()); - EXPECT_FALSE(result.init_failed()) << result.diagnostic(); - EXPECT_EQ(result.process_code(), -9876); - EXPECT_EQ(result.Output("output"), nullptr); - EXPECT_EQ(formatting_calls, - 2); // 第一个成功;第二个中止了批次。 - EXPECT_EQ(model->call_count, 1); - } -} - -TEST(FunctionNodeTest, LlmShortcutNodeExecutesPipeline) { - auto mock_model = std::make_shared(); - NodeHarness harness("LlmShortcutNode"); - harness.Config({{"bind_model", "test_llm"}, {"suffix", "!!!"}}); - harness.BindModel("test_llm", mock_model); - harness.TextInput("prompt", {"world"}); - - auto result = harness.Run(); - ASSERT_TRUE(result.ok()) << result.diagnostic(); - // BuildPrompt: prompt:world -> Llm: ans:prompt:world -> FormatAnswer: - // ans:prompt:world!!! - EXPECT_EQ(result.TextValues("text"), - (std::vector{"ans:prompt:world!!!"})); -} - -TEST(FunctionNodeTest, FourArgMakeLlmTextSpecExecutesProperly) { - auto mock_model = std::make_shared(); - NodeHarness harness("StarterLlmTestNode"); - harness.Config({{"bind_model", "test_llm"}}); - harness.BindModel("test_llm", mock_model); - harness.TextInput("input", {"hello"}); - - auto result = harness.Run(); - ASSERT_TRUE(result.ok()) << result.diagnostic(); - EXPECT_EQ(result.TextValues("output"), - (std::vector{"formatted:ans:prompt:hello"})); -} - -TEST(FunctionNodeTest, LlmViewHooksCopyAliasedTextAndPreserveProvenance) { - const TextBatch input{{17, 3, std::string("xhello\0world", 12)}, {8, 5, "x"}}; - for (const auto* node_type : {"ViewHooksNode", "ResultViewHooksNode"}) { - SCOPED_TRACE(node_type); - auto model = std::make_shared(); - NodeHarness harness(node_type); - harness.Config({{"bind_model", "test_llm"}}); - harness.BindModel("test_llm", model); - harness.CustomInput("input", input); - auto result = harness.Run(); - ASSERT_TRUE(result.ok()) << result.diagnostic(); - const auto* output = result.Output("output"); - ASSERT_NE(output, nullptr); - ASSERT_EQ(output->size(), input.size()); - ASSERT_EQ(model->last_prompts.size(), input.size()); - ASSERT_NE(result.Context(), nullptr); - const auto* snapshot = result.Context()->Read("bk_in_input"); - ASSERT_NE(snapshot, nullptr); - ASSERT_EQ(snapshot->size(), input.size()); - for (size_t i = 0; i < input.size(); ++i) { - EXPECT_EQ(output->at(i).data, input[i].data.substr(1)); - EXPECT_EQ(model->last_prompts[i].data, input[i].data.substr(1)); - EXPECT_EQ(output->at(i).req_id, input[i].req_id); - EXPECT_EQ(output->at(i).sub_id, input[i].sub_id); - EXPECT_EQ(snapshot->at(i).data, input[i].data); - EXPECT_EQ(snapshot->at(i).req_id, input[i].req_id); - EXPECT_EQ(snapshot->at(i).sub_id, input[i].sub_id); - } - } -} - -TEST(FunctionNodeTest, LlmResultViewPromptFailureStopsBeforeModelCall) { - auto model = std::make_shared(); - NodeHarness harness("ResultViewHooksNode"); - harness.Config({{"bind_model", "test_llm"}}); - harness.BindModel("test_llm", model); - harness.TextInput("input", {"xfirst", "REJECT"}); - auto result = harness.Run(); - EXPECT_FALSE(result.ok()); - EXPECT_FALSE(result.init_failed()) << result.diagnostic(); - EXPECT_EQ(result.process_code(), -8765); - EXPECT_EQ(result.Output("output"), nullptr); - EXPECT_EQ(model->call_count, 0); -} - TEST(FunctionNodeTest, BatchCrossTypeOutputAlignmentSucceeds) { NodeHarness harness("TextToScoreNode"); harness.TextInput("texts", {"query1", "query2"}); @@ -1597,7 +1328,7 @@ TEST(FunctionNodeTest, BatchCrossTypeOutputAlignmentSucceeds) { } TEST(FunctionNodeTest, NodeHarnessFailsInitOnInvalidConfig) { - NodeHarness harness("LlmShortcutNode"); + NodeHarness harness("AnswerBatchNode"); harness.Config({{"bind_model", "test_llm"}, {"unknown_field", 123}}); auto result = harness.Run(); EXPECT_FALSE(result.ok()); @@ -1858,24 +1589,6 @@ TEST(FunctionNodeTest, ComplexParserMatchesPreflightInitAndOwnsConfiguration) { } } -TEST(FunctionNodeTest, LogicObjectIsRecreatedForEachProcessOnSameNode) { - auto node = NodeRegistry::Instance().Create("RequestLocalNode"); - ASSERT_NE(node, nullptr); - SessionContext session; - ASSERT_TRUE(InitNodeForTest(*node, nlohmann::json::object(), &session)); - const int before = local_logic_constructions; - for (const std::string text : {"first", "second", "third"}) { - AlgContext context; - context.Publish("input", TextBatch{{7, 2, text}}); - ASSERT_EQ(node->Process(&context), 0); - const auto* output = context.Read("output"); - ASSERT_NE(output, nullptr); - ASSERT_EQ(output->size(), 1u); - EXPECT_EQ((*output)[0].data, text); - } - EXPECT_EQ(local_logic_constructions, before + 3); -} - // --------------------------------------------------------------------------- // ConfigurationSnapshot 与直接并发 // --------------------------------------------------------------------------- @@ -2100,7 +1813,7 @@ TEST(ConfigurationSnapshotTest, MoveOnlyStateHandled) { // 函数式 Spec 的 WithControls 与 NodeHarness 测试 // --------------------------------------------------------------------------- -TEST(FunctionNodeTest, FunctionalMapSpecWithControls) { +TEST(FunctionNodeTest, ItemwiseNodeWithFieldControls) { NodeHarness harness("ControlledMapNode"); harness.Config( {{"prefix", "init_p:"}, {"suffix", ":init_s"}, {"multiplier", 1}}); @@ -2163,7 +1876,7 @@ TEST(FunctionNodeTest, FunctionalMapSpecWithControls) { EXPECT_EQ(unk.status, NodeControlStatus::kUnsupported); } -TEST(FunctionNodeTest, FunctionalBatchSpecWithControlsAndValidation) { +TEST(FunctionNodeTest, BatchNodeWithControlsAndValidation) { NodeHarness harness("ControlledBatchNode"); harness.Config({{"header", "H:"}, {"uppercase", false}}); harness.TextInput("texts", {"abc", "def"}); @@ -2269,7 +1982,7 @@ TEST(FunctionNodeTest, WholeBatchProcessConsistencyDuringControl) { } } -TEST(FunctionNodeTest, WholeBatchProcessConsistencyDuringControlForBatchSpec) { +TEST(FunctionNodeTest, WholeBatchProcessConsistencyDuringControlForBatchNode) { NodeHarness harness("ControlledBatchNode"); harness.Config({{"header", "old:"}, {"uppercase", false}}); ASSERT_TRUE(harness.EnsureInitialized()); @@ -2479,11 +2192,11 @@ TEST(FunctionNodeTest, OutputDeclarationsStillRejectDuplicatePortNames) { TEST(FunctionNodeTest, MixedControlDeclarationsRejectDuplicateIdInEitherOrder) { auto make_spec = [] { - return MakeBatchSpec( + return MakeNodeSpec( InputsOf{Required("input", &ComplexInputs::input)}, PreservedOutput("output", "input"), CleanConfig(), - [](const ComplexInputs& inputs, const CleanParams&, - const NoModels&) -> NodeResult { return *inputs.input; }); + [](const ComplexInputs& inputs, const CleanParams&) + -> NodeResult { return *inputs.input; }); }; auto update = [](const CleanParams& current, const nlohmann::json&, const BindingFacts&) -> NodeResult { @@ -2571,8 +2284,7 @@ struct BindingFactsProbeParams { }; inline auto BindingFactsProbeSpec() { - return MakeMapSpec( - Input("input"), Output("output"), + return TextMapSpec( Parameters( { Field("mode", &BindingFactsProbeParams::mode).Default("base"), diff --git a/tests/unit/nodes/test_traceable_batch_operations.cpp b/tests/unit/nodes/test_traceable_batch_operations.cpp index 66dc3815..52691cbc 100644 --- a/tests/unit/nodes/test_traceable_batch_operations.cpp +++ b/tests/unit/nodes/test_traceable_batch_operations.cpp @@ -1277,12 +1277,11 @@ NodeResult RunDirectSubBatch(const DirectSubBatchInputs& in, } auto DirectSubBatchSpec() { - return MakeBatchSpec(InputsOf({ - Required("input", &DirectSubBatchInputs::input), - }), - PreservedOutput("output", "input"), - Parameters({}), - &RunDirectSubBatch) + return MakeNodeSpec(InputsOf({ + Required("input", &DirectSubBatchInputs::input), + }), + PreservedOutput("output", "input"), + Parameters({}), &RunDirectSubBatch) .Description("Test fixture for unscattered sub-batch rejection"); } @@ -1515,12 +1514,12 @@ NodeResult RunBatchSelectFail(const BatchSelectFailInputs& in, } auto BatchSelectFailSpec() { - return MakeBatchSpec(InputsOf({ - Required("input", &BatchSelectFailInputs::input), - }), - PreservedOutput("output", "input"), - Parameters({}), - &RunBatchSelectFail) + return MakeNodeSpec(InputsOf({ + Required("input", &BatchSelectFailInputs::input), + }), + PreservedOutput("output", "input"), + Parameters({}), + &RunBatchSelectFail) .Description( "Test fixture for SelectBatch failure diagnostic formatting"); } @@ -1586,12 +1585,11 @@ NodeResult RunBatchSplitFail(const BatchSplitFailInputs& in, } auto BatchSplitFailSpec() { - return MakeBatchSpec(InputsOf({ - Required("input", &BatchSplitFailInputs::input), - }), - PreservedOutput("output", "input"), - Parameters({}), - &RunBatchSplitFail) + return MakeNodeSpec(InputsOf({ + Required("input", &BatchSplitFailInputs::input), + }), + PreservedOutput("output", "input"), + Parameters({}), &RunBatchSplitFail) .Description( "Test fixture for SplitPayloads failure diagnostic formatting"); } @@ -1623,16 +1621,28 @@ inline NodeResult RunMapItemFail(const std::string& s) { return NodeResult::Success(s); } +struct MapItemFailInputs { + const TextBatch* input = nullptr; +}; + +inline NodeResult RunMapItemFailBatch(const MapItemFailInputs& in) { + return MapPayloads(*in.input, &RunMapItemFail); +} + inline auto MapItemFailSpec() { - return MakeMapSpec(Input("input"), Output("output"), - &RunMapItemFail) - .Description("Test fixture for MapSpec failure diagnostic formatting"); + return MakeNodeSpec(InputsOf({ + Required("input", &MapItemFailInputs::input), + }), + PreservedOutput("output", "input"), + &RunMapItemFailBatch) + .Description( + "Test fixture for MapPayloads failure diagnostic formatting"); } REGISTER_FUNCTION_NODE(MapItemFailTestNode, MapItemFailSpec()); TEST_F(TraceableBatchOperationsTest, - AuthorNodeFormatsMapSpecFailureDiagnostic) { + AuthorNodeFormatsMapPayloadsFailureDiagnostic) { NodeHarness harness("MapItemFailTestNode"); TextBatch batch = {{1, 0, "ok"}, {7, 3, "trigger_map_failure"}}; harness.TextInputWithBatch("input", std::move(batch)); @@ -1642,7 +1652,7 @@ TEST_F(TraceableBatchOperationsTest, EXPECT_EQ(result.process_code(), -5544); const auto& diag = result.diagnostic(); EXPECT_NE(diag.find("Process returned -5544"), std::string::npos); - EXPECT_NE(diag.find("MapItemFailTestNode"), std::string::npos); + EXPECT_NE(diag.find("MapPayloads"), std::string::npos); EXPECT_NE(diag.find("map item failed"), std::string::npos); EXPECT_NE(diag.find("req_id=7"), std::string::npos); EXPECT_NE(diag.find("sub_id=3"), std::string::npos); diff --git a/tools/scaffold_custom_node.py b/tools/scaffold_custom_node.py index 61e6523b..26282c00 100755 --- a/tools/scaffold_custom_node.py +++ b/tools/scaffold_custom_node.py @@ -74,7 +74,6 @@ def get_item_type_for_batch(batch): def render_map_node(name, description, in_port, out_port): in_name, in_type, in_card, in_prov = in_port out_name, out_type, out_card, out_prov = out_port - spec_func = f"{name}Spec" return f"""#include #include "nodes/authoring.h" @@ -82,21 +81,29 @@ def render_map_node(name, description, in_port, out_port): namespace llm_edgeflow {{ namespace custom_nodes {{ namespace {{ -// Map 入门模板:逐条独立转换输入,并保留来源信息。 +// 逐条转换入门模板:每条输入得到一条输出,来源编号由框架保留。 static std::string Transform(const std::string& input) {{ // TODO: 替换为你的领域逻辑。 return input; }} -auto {spec_func}() {{ - return MakeMapSpec( - Input<{in_type}>({cpp_string(in_name)}), - Output<{out_type}>({cpp_string(out_name)}), - &Transform) +struct Inputs {{ + const {in_type}* input = nullptr; +}}; + +NodeResult<{out_type}> Run(const Inputs& inputs) {{ + return MapPayloads(*inputs.input, &Transform); +}} + +auto Spec() {{ + return MakeNodeSpec( + InputsOf{{Required({cpp_string(in_name)}, &Inputs::input)}}, + PreservedOutput<{out_type}>({cpp_string(out_name)}, {cpp_string(in_name)}), + &Run) .Description({cpp_string(description)}); }} -REGISTER_FUNCTION_NODE({name}, {spec_func}()); +REGISTER_FUNCTION_NODE({name}, Spec()); }} // namespace }} // namespace custom_nodes @@ -107,7 +114,6 @@ def render_map_node(name, description, in_port, out_port): def render_llm_starter(name, description, in_name, out_name): source = STARTER_LLM_TEMPLATE.read_text(encoding="utf-8") source = source.replace("StarterLlmNode", name) - source = source.replace("StarterLlmSpec", f"{name}Spec") literals = { '"input"': cpp_string(in_name), '"output"': cpp_string(out_name), @@ -131,22 +137,21 @@ def render_embedding_node(name, description, in_name, out_name): EmbeddingCall encoder; }}; -static NodeResult Run(const Inputs& input, const NoParameters&, - const Models& models) {{ +static NodeResult Run(const Inputs& input, const Models& models) {{ // TODO: 按需添加领域预处理或后处理。 return models.encoder.Embed(*input.texts); }} -auto {name}Spec() {{ - return MakeBatchSpec( +auto Spec() {{ + return MakeNodeSpec( InputsOf({{Required({cpp_string(in_name)}, &Inputs::texts)}}), PreservedOutput({cpp_string(out_name)}, {cpp_string(in_name)}), - ModelsOf({{Embedding("encoder", "bind_model", &Models::encoder)}}), + ModelsOf({{Model("encoder", "bind_model", &Models::encoder)}}), &Run) .Description({cpp_string(description)}); }} -REGISTER_FUNCTION_NODE({name}, {name}Spec()); +REGISTER_FUNCTION_NODE({name}, Spec()); }} // namespace }} // namespace custom_nodes @@ -194,13 +199,13 @@ def render_node(name, description, kind, capability, in_port, out_port, control_ if preserved else f"ProducedBatch<{out_type}>({cpp_string(out_name)}, PortFlow{{{cpp_string(out_card)}, {cpp_string(out_prov)}}})") models = "" - model_binding = "ModelsOf{}" - model_type = "NoModels" + run_models = "" + model_binding = "" if kind == "model": call, _, _, method = CAPABILITY_MAP[capability] models = f"struct Models {{ {call} model; }};\n" - model_type = "Models" - model_binding = 'ModelsOf{Model("model", "bind_model", &Models::model)}' + run_models = ", const Models& models" + model_binding = '\n ModelsOf{Model("model", "bind_model", &Models::model)},' processing = f" return models.model.{method}(*inputs.items);" elif preserved and in_type == out_type: processing = f" return NodeResult<{out_type}>::Success(*inputs.items);" @@ -217,20 +222,19 @@ def render_node(name, description, kind, capability, in_port, out_port, control_ namespace {{ struct Inputs {{ const {in_type}* items = nullptr; }}; {models} -static NodeResult<{out_type}> Run(const Inputs& inputs, const NoParameters&, - const {model_type}&{ " models" if kind == "model" else ""}) {{ +static NodeResult<{out_type}> Run(const Inputs& inputs{run_models}) {{ {processing} }} -auto {name}Spec() {{ - return MakeBatchSpec( +auto Spec() {{ + return MakeNodeSpec( InputsOf{{Required({cpp_string(in_name)}, &Inputs::items{input_flow})}}, - {output}, - {model_binding}, &Run) + {output},{model_binding} + &Run) .Description({cpp_string(description)}); }} -REGISTER_FUNCTION_NODE({name}, {name}Spec()); +REGISTER_FUNCTION_NODE({name}, Spec()); }} // namespace }} // namespace custom_nodes }} // namespace llm_edgeflow