From 1e092b638a773ba2d83da72b6923eb32439a234d Mon Sep 17 00:00:00 2001 From: chamsechan Date: Mon, 5 Oct 2026 22:18:16 +0800 Subject: [PATCH] refactor(nodes): keep one Node authoring structure Every Node now uses MakeNodeSpec with Inputs, optional Params and Models, one Run and a Spec. MakeMapSpec, MakeLlmTextSpec, member function Run, the Llm()/Embedding() aliases and Node biz_names are removed. Run takes only the parts the Spec declares. MapPayloads names the failing item, and the LLM starter reads generation options from node configuration through GenerateParameters. The three Node skills are merged into edgeflow-node-developer. Co-Authored-By: Claude Opus 5.5 --- .../edgeflow-node-batch-developer/SKILL.md | 65 -- .../skills/edgeflow-node-developer/SKILL.md | 75 ++ .../edgeflow-node-llm-developer/SKILL.md | 60 -- .../edgeflow-node-map-developer/SKILL.md | 53 -- .../skills/edgeflow-solution-planner/SKILL.md | 4 +- .agents/skills/json-prompt-solution/SKILL.md | 5 +- .../llm-edgeflow-developer-guide/SKILL.md | 8 +- .../references/capability-nodes.md | 34 +- AGENTS.md | 8 +- .../node_authoring/benchmark/README.md | 3 +- .../node_authoring/benchmark/probe.cpp | 25 +- .../starter_batch_group_node.cpp | 26 +- .../starter_batch_join_node.cpp | 24 +- .../node_authoring/starter_batch_node.cpp | 14 +- .../starter_batch_select_scatter_node.cpp | 16 +- .../node_authoring/starter_control_node.cpp | 31 +- .../node_authoring/starter_llm_node.cpp | 32 +- .../starter_multi_model_node.cpp | 9 +- doc/CHANGELOG.md | 9 + doc/README.md | 8 +- doc/dev_guide/custom_node_concepts.md | 82 ++- doc/dev_guide/first_control.md | 4 +- doc/dev_guide/first_custom_node.md | 21 +- doc/dev_guide/recipe_text_llm_node.md | 4 +- doc/developer_guide.md | 16 +- include/core/diagnostic_code.h | 1 - include/core/node_definition.h | 1 - include/nodes/authoring.h | 1 + include/nodes/function_node.h | 683 +++--------------- include/nodes/generate_options_config.h | 9 + include/nodes/traceable_algorithms.h | 9 +- scripts/check_governance.sh | 4 +- src/common_nodes/asr_transcribe_node.cpp | 12 +- src/common_nodes/llm_generate_node.cpp | 28 +- src/common_nodes/ocr_detect_node.cpp | 12 +- .../structured_json_parse_node.cpp | 43 +- src/common_nodes/text_chunk_node.cpp | 37 +- src/common_nodes/text_corpus_source_node.cpp | 37 +- src/common_nodes/text_embedding_node.cpp | 34 +- src/common_nodes/text_rerank_node.cpp | 38 +- src/common_nodes/text_rule_match_node.cpp | 53 +- src/common_nodes/text_template_node.cpp | 60 +- src/common_nodes/vector_top_k_node.cpp | 34 +- src/core/pipeline_catalog.cpp | 8 +- src/core/pipeline_validator.cpp | 8 - src/custom_nodes/README.md | 5 +- src/custom_nodes/prompt_guided_llm_node.cpp | 27 +- .../test_layer_header_views.cmake | 31 +- .../test_spec_signature_diagnostics.cmake | 7 +- .../spec_signatures/batch_invalid_member.cpp | 9 - .../batch_missing_noparameters.cpp | 9 - .../llm_build_prompt_nonconst.cpp | 5 - .../llm_build_prompt_returns_int.cpp | 5 - .../llm_build_prompt_returns_result_int.cpp | 5 - .../llm_format_answer_nonconst.cpp | 5 - .../llm_format_answer_returns_char.cpp | 5 - .../llm_format_answer_returns_int.cpp | 5 - .../llm_format_answer_returns_result_char.cpp | 5 - .../llm_mutable_build_prompt.cpp | 12 - .../spec_signatures/run_member_function.cpp | 10 + .../spec_signatures/run_missing_models.cpp | 5 + ...nst_inputs.cpp => run_nonconst_inputs.cpp} | 4 +- ...ams_swapped.cpp => run_params_swapped.cpp} | 4 +- ..._batch.cpp => run_returns_plain_batch.cpp} | 4 +- ...esult.cpp => run_returns_wrong_result.cpp} | 4 +- .../spec_signatures/run_writes_nomodels.cpp | 11 + .../run_writes_noparameters.cpp | 11 + .../spec_signatures/signature_fixture.h | 12 +- .../spec_signatures/valid_signatures.cpp | 222 ++---- .../test_pipeline_catalog_validator.cpp | 1 - tests/tooling/test_scaffold_custom_node.py | 32 +- tests/unit/core/test_node_base_contracts.cpp | 5 +- .../core/test_validated_pipeline_plan.cpp | 43 +- tests/unit/nodes/test_common_nodes.cpp | 40 +- tests/unit/nodes/test_function_node.cpp | 494 +++---------- .../nodes/test_traceable_batch_operations.cpp | 56 +- tools/scaffold_custom_node.py | 56 +- 77 files changed, 930 insertions(+), 1967 deletions(-) delete mode 100644 .agents/skills/edgeflow-node-batch-developer/SKILL.md create mode 100644 .agents/skills/edgeflow-node-developer/SKILL.md delete mode 100644 .agents/skills/edgeflow-node-llm-developer/SKILL.md delete mode 100644 .agents/skills/edgeflow-node-map-developer/SKILL.md delete mode 100644 tests/fixtures/spec_signatures/batch_invalid_member.cpp delete mode 100644 tests/fixtures/spec_signatures/batch_missing_noparameters.cpp delete mode 100644 tests/fixtures/spec_signatures/llm_build_prompt_nonconst.cpp delete mode 100644 tests/fixtures/spec_signatures/llm_build_prompt_returns_int.cpp delete mode 100644 tests/fixtures/spec_signatures/llm_build_prompt_returns_result_int.cpp delete mode 100644 tests/fixtures/spec_signatures/llm_format_answer_nonconst.cpp delete mode 100644 tests/fixtures/spec_signatures/llm_format_answer_returns_char.cpp delete mode 100644 tests/fixtures/spec_signatures/llm_format_answer_returns_int.cpp delete mode 100644 tests/fixtures/spec_signatures/llm_format_answer_returns_result_char.cpp delete mode 100644 tests/fixtures/spec_signatures/llm_mutable_build_prompt.cpp create mode 100644 tests/fixtures/spec_signatures/run_member_function.cpp create mode 100644 tests/fixtures/spec_signatures/run_missing_models.cpp rename tests/fixtures/spec_signatures/{batch_nonconst_inputs.cpp => run_nonconst_inputs.cpp} (65%) rename tests/fixtures/spec_signatures/{batch_params_swapped.cpp => run_params_swapped.cpp} (66%) rename tests/fixtures/spec_signatures/{batch_returns_plain_batch.cpp => run_returns_plain_batch.cpp} (59%) rename tests/fixtures/spec_signatures/{batch_returns_wrong_result.cpp => run_returns_wrong_result.cpp} (62%) create mode 100644 tests/fixtures/spec_signatures/run_writes_nomodels.cpp create mode 100644 tests/fixtures/spec_signatures/run_writes_noparameters.cpp 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