diff --git a/.agents/skills/pipeline-composer/SKILL.md b/.agents/skills/pipeline-composer/SKILL.md index 10519daf..a6bfcc0c 100644 --- a/.agents/skills/pipeline-composer/SKILL.md +++ b/.agents/skills/pipeline-composer/SKILL.md @@ -79,11 +79,10 @@ See [the I/O boundary](../../../doc/dev_guide/business_onboarding.md#输入输 A new Profile is optional; explicit `--biz`, `--config` and `--dataset` also work. Use the original Profile alone only when its configuration already points to the intended Pipeline. - Demo uses the selected Pipeline defaults. The compatibility flag `--no-default-control` - is unnecessary and conflicts with `--example-control`. Use `--example-control` only for - the built-in update demonstration, and provide a - Control file only when it is part of the requested scenario. Verify - request IDs, status and expected output fields in `results.jsonl` and `summary.json`. + Demo uses the selected Pipeline defaults (by default, no example Control is sent). Use + `--example-control` only for the built-in update demonstration, and provide a Control file + only when it is part of the requested scenario. Verify request IDs, status and expected + output fields in `results.jsonl` and `summary.json`. For human composition, use `./show --web` or `./show --web`. For AI and automation, use `alg_pipeline_tool` and consume its versioned JSON output. diff --git a/CMakeLists.txt b/CMakeLists.txt index fb9f5eaa..8e713d7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,9 +54,6 @@ endif() set(LLM_EDGEFLOW_GENERATED_INCLUDE_DIR "${PROJECT_BINARY_DIR}/generated/include") file(MAKE_DIRECTORY "${LLM_EDGEFLOW_GENERATED_INCLUDE_DIR}/edgeflow") -# An earlier build generated this name; it must not shadow the compatibility -# forwarding header after the canonical version changes. -file(REMOVE "${LLM_EDGEFLOW_GENERATED_INCLUDE_DIR}/company_alg_version.h") configure_file( "${PROJECT_SOURCE_DIR}/cmake_ext/edgeflow_version.h.in" "${LLM_EDGEFLOW_GENERATED_INCLUDE_DIR}/edgeflow/version.h" diff --git a/README.md b/README.md index 8b51cd5c..75d1d896 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ cmake --build build --target alg_sdk alg_demo alg_pipeline_tool alg_show --paral - `results/quickstart/keyword_match_rules/results.jsonl`:逐条请求的状态与匹配结果。 - `results/quickstart/keyword_match_rules/summary.json`:样本数、成功数、失败数与耗时。 -`keyword_match_rules` 是 Demo 预设名称,这个方案使用真实规则节点。Demo 默认使用 Pipeline 中的规则;`--no-default-control` 保留为兼容选项。需要体验内置规则热更新时显式添加 `--example-control`。 +`keyword_match_rules` 是 Demo 预设名称,这个方案使用真实规则节点。Demo 默认使用 Pipeline 中的规则;需要体验内置规则热更新时显式添加 `--example-control`。 ### 3. 查看与编辑流程 @@ -104,7 +104,7 @@ flowchart TD | 职责 | 何时扩展 | 主要入口 | | :--- | :--- | :--- | -| 接入适配层 | 宿主程序增加新的输入输出结构或调用约定 | `include/adapter/`、`include/operator/`、`src/adapter/` | +| 接入适配层 | 宿主程序增加新的输入输出结构或调用约定 | `include/adapter/`、`include/edgeflow/operator/`、`src/adapter/` | | 流程编排层 | 现有校验、调度或上下文机制无法满足需求 | `include/core/`、`src/core/` | | 能力节点层 | 增加领域算法、数据处理或模型调用组合 | `src/custom_nodes/`;通用操作位于 `src/common_nodes/` | | 模型执行层 | 增加模型语义或接入新的推理运行时 | `src/engine/models/`、`src/engine/backends/` | diff --git a/cmake_ext/LayerHeaderViews.cmake b/cmake_ext/LayerHeaderViews.cmake index 6ccb9f4e..d3c968d7 100644 --- a/cmake_ext/LayerHeaderViews.cmake +++ b/cmake_ext/LayerHeaderViews.cmake @@ -42,10 +42,6 @@ set(public_headers include/edgeflow/c_api.h include/edgeflow/c_api.hpp include/edgeflow/export.h include/edgeflow/log.h include/edgeflow/operator/interface.h include/edgeflow/operator/types.h - include/company_alg_interface.h include/company_alg_cpp.hpp - include/company_alg_export.h include/company_alg_log.h - include/company_alg_version.h - include/operator/operator_interface.h include/operator/company_operator_types.h ${platform_mock_headers}) edgeflow_header_view(public ${public_headers}) diff --git a/configs/pipeline_audio_asr_cpu.conf b/configs/pipeline_audio_asr_cpu.conf index 4b6c8c68..6a3f8c7a 100644 --- a/configs/pipeline_audio_asr_cpu.conf +++ b/configs/pipeline_audio_asr_cpu.conf @@ -4,13 +4,15 @@ "model_paths": { "asr_model_v1": "models/ggml-base.bin" }, - "mem_que": { - "type": "audio_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "transcribed_text": 511, - "intent_slot_json": 1023 + "outputs": { + "audio_out": { + "type": "audio_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "transcribed_text": 511, + "intent_slot_json": 1023 + } } } } diff --git a/configs/pipeline_cross_rerank_cpu.conf b/configs/pipeline_cross_rerank_cpu.conf index a39de4ed..8aa88f0f 100644 --- a/configs/pipeline_cross_rerank_cpu.conf +++ b/configs/pipeline_cross_rerank_cpu.conf @@ -4,11 +4,13 @@ "model_paths": { "rerank_model_v1": "./models/ms_marco_tinybert_l2_v2_quantized.onnx" }, - "mem_que": { - "type": "rerank_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": {} + "outputs": { + "rerank_out": { + "type": "rerank_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": {} + } } } } diff --git a/configs/pipeline_dialogue_audit_default.conf b/configs/pipeline_dialogue_audit_default.conf index 07247c58..e51afa2f 100644 --- a/configs/pipeline_dialogue_audit_default.conf +++ b/configs/pipeline_dialogue_audit_default.conf @@ -6,14 +6,16 @@ "rerank_model_v1": "./models/ms_marco_tinybert_l2_v2_quantized.onnx", "audit_llm_v1": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "audit_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "risk_level": 31, - "matched_policy_clause": 255, - "audit_verdict_json": 1023 + "outputs": { + "audit_out": { + "type": "audit_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "risk_level": 31, + "matched_policy_clause": 255, + "audit_verdict_json": 1023 + } } } } diff --git a/configs/pipeline_dialogue_audit_kite.conf b/configs/pipeline_dialogue_audit_kite.conf index a09b9041..befd14d8 100644 --- a/configs/pipeline_dialogue_audit_kite.conf +++ b/configs/pipeline_dialogue_audit_kite.conf @@ -6,14 +6,16 @@ "rerank_model_v1": "models/ms_marco_tinybert_l2_v2_quantized.onnx", "audit_llm_v1": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "audit_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "risk_level": 31, - "matched_policy_clause": 255, - "audit_verdict_json": 1023 + "outputs": { + "audit_out": { + "type": "audit_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "risk_level": 31, + "matched_policy_clause": 255, + "audit_verdict_json": 1023 + } } } } diff --git a/configs/pipeline_doc_qa_cpu.conf b/configs/pipeline_doc_qa_cpu.conf index f9010a90..55962ba3 100644 --- a/configs/pipeline_doc_qa_cpu.conf +++ b/configs/pipeline_doc_qa_cpu.conf @@ -5,13 +5,15 @@ "embed_model_onnx": "./models/bge_base_zh_v1.5.onnx", "llm_model_llamacpp": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/configs/pipeline_doc_qa_default.conf b/configs/pipeline_doc_qa_default.conf index dcc7f178..02cacdda 100644 --- a/configs/pipeline_doc_qa_default.conf +++ b/configs/pipeline_doc_qa_default.conf @@ -5,13 +5,15 @@ "embed_model_v1": "./models/bge_base_zh_v1.5.onnx", "llm_model_v1": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/configs/pipeline_doc_qa_kite.conf b/configs/pipeline_doc_qa_kite.conf index 0399dfe7..39294d50 100644 --- a/configs/pipeline_doc_qa_kite.conf +++ b/configs/pipeline_doc_qa_kite.conf @@ -5,13 +5,15 @@ "embed_model_v1": "models/bge_base_zh_v1.5.onnx", "llm_model_v1": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/configs/pipeline_doc_qa_kite_generated_embeddings.conf b/configs/pipeline_doc_qa_kite_generated_embeddings.conf index ace2f5b5..9f664844 100644 --- a/configs/pipeline_doc_qa_kite_generated_embeddings.conf +++ b/configs/pipeline_doc_qa_kite_generated_embeddings.conf @@ -5,13 +5,15 @@ "embed_model_v1": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf", "llm_model_v1": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/configs/pipeline_doc_qa_rerank_cpu.conf b/configs/pipeline_doc_qa_rerank_cpu.conf index 8808e500..916de12d 100644 --- a/configs/pipeline_doc_qa_rerank_cpu.conf +++ b/configs/pipeline_doc_qa_rerank_cpu.conf @@ -6,13 +6,15 @@ "rerank_model_onnx": "./models/ms_marco_tinybert_l2_v2_quantized.onnx", "llm_model_llamacpp": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/configs/pipeline_doc_qa_rerank_default.conf b/configs/pipeline_doc_qa_rerank_default.conf index 91a89392..ab550a16 100644 --- a/configs/pipeline_doc_qa_rerank_default.conf +++ b/configs/pipeline_doc_qa_rerank_default.conf @@ -6,13 +6,15 @@ "rerank_model_v1": "./models/ms_marco_tinybert_l2_v2_quantized.onnx", "llm_model_v1": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/configs/pipeline_doc_qa_rerank_kite.conf b/configs/pipeline_doc_qa_rerank_kite.conf index f8c036d4..d7f1ea5f 100644 --- a/configs/pipeline_doc_qa_rerank_kite.conf +++ b/configs/pipeline_doc_qa_rerank_kite.conf @@ -6,13 +6,15 @@ "rerank_model_onnx": "models/ms_marco_tinybert_l2_v2_quantized.onnx", "llm_model_llamacpp": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/configs/pipeline_entity_extract_cpu.conf b/configs/pipeline_entity_extract_cpu.conf index eca23f5f..180869a8 100644 --- a/configs/pipeline_entity_extract_cpu.conf +++ b/configs/pipeline_entity_extract_cpu.conf @@ -4,12 +4,14 @@ "model_paths": { "entity_llm": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "entity_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "entities_json": 2047 + "outputs": { + "entity_out": { + "type": "entity_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "entities_json": 2047 + } } } } diff --git a/configs/pipeline_entity_extract_default.conf b/configs/pipeline_entity_extract_default.conf index f8a1603a..9769d5d5 100644 --- a/configs/pipeline_entity_extract_default.conf +++ b/configs/pipeline_entity_extract_default.conf @@ -4,12 +4,14 @@ "model_paths": { "entity_llm": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "entity_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "entities_json": 2047 + "outputs": { + "entity_out": { + "type": "entity_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "entities_json": 2047 + } } } } diff --git a/configs/pipeline_entity_extract_kite.conf b/configs/pipeline_entity_extract_kite.conf index fa429b06..ebe6f45c 100644 --- a/configs/pipeline_entity_extract_kite.conf +++ b/configs/pipeline_entity_extract_kite.conf @@ -4,12 +4,14 @@ "model_paths": { "entity_llm": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "entity_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "entities_json": 2047 + "outputs": { + "entity_out": { + "type": "entity_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "entities_json": 2047 + } } } } diff --git a/configs/pipeline_keyword_match_rules.conf b/configs/pipeline_keyword_match_rules.conf index 418c8e1c..4b7435f1 100644 --- a/configs/pipeline_keyword_match_rules.conf +++ b/configs/pipeline_keyword_match_rules.conf @@ -1,12 +1,14 @@ { "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "match_result_json": 2047 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "match_result_json": 2047 + } } } } diff --git a/configs/pipeline_ocr_doc_qa_kite.conf b/configs/pipeline_ocr_doc_qa_kite.conf index 1fc10063..3b98f689 100644 --- a/configs/pipeline_ocr_doc_qa_kite.conf +++ b/configs/pipeline_ocr_doc_qa_kite.conf @@ -5,12 +5,14 @@ "ocr_model_v1": "models/SmolVLM-256M-Instruct-Q8_0.gguf", "llm_model_v1": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "od_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "result_json": 2047 + "outputs": { + "od_out": { + "type": "od_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "result_json": 2047 + } } } } diff --git a/configs/pipeline_translate_cpu.conf b/configs/pipeline_translate_cpu.conf index 27b3d02b..53be9f6c 100644 --- a/configs/pipeline_translate_cpu.conf +++ b/configs/pipeline_translate_cpu.conf @@ -4,12 +4,14 @@ "model_paths": { "translate_llm": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, - "mem_que": { - "type": "entity_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "entities_json": 8191 + "outputs": { + "entity_out": { + "type": "entity_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "entities_json": 8191 + } } } } diff --git a/demo/common/demo_options.cpp b/demo/common/demo_options.cpp index f6847fda..7e70b1a5 100644 --- a/demo/common/demo_options.cpp +++ b/demo/common/demo_options.cpp @@ -234,8 +234,6 @@ int ParseCommandLine(int argc, char* argv[], DemoOptions* out_options, out_options->has_control_file = true; } else if (arg == "--example-control") { out_options->example_control = true; - } else if (arg == "--no-default-control") { - out_options->no_default_control = true; } else if (arg == "--append") { out_options->append = true; } else if (arg == "--allow-fallback-sample") { @@ -246,12 +244,6 @@ int ParseCommandLine(int argc, char* argv[], DemoOptions* out_options, } } - if (out_options->example_control && out_options->no_default_control) { - if (error_msg) - *error_msg = "--example-control conflicts with --no-default-control"; - return 2; - } - return 0; } @@ -592,8 +584,6 @@ void PrintHelp(const char* program_name) { "1)\n" << " --example-control Apply the built-in Demo example update " "(keyword_match)\n" - << " --no-default-control Compatibility flag: keep configured " - "defaults (now the default)\n" << " --control-file Runtime control parameters JSON file\n" << " --control-cmd Node command ID for --control-file\n" << " --append Append output to existing results file " diff --git a/demo/common/demo_options.h b/demo/common/demo_options.h index 23524dd5..0875515c 100644 --- a/demo/common/demo_options.h +++ b/demo/common/demo_options.h @@ -39,9 +39,7 @@ struct DemoOptions { std::optional control_cmd; // 节点命令 ID;必须配合 control_file std::string suite; // 执行套件 ("smoke", "real", "all") bool example_control = false; // Explicitly apply the Demo example update. - bool no_default_control = - false; // Compatibility flag; configured defaults remain active. - bool append = false; // 结果文件是否追加模式 + bool append = false; // 结果文件是否追加模式 bool allow_fallback_sample = false; // 测试集缺失时是否允许使用内置样例 bool list_only = false; // 是否仅列出可用 Business 和 Profile bool show_help = false; // 是否显示帮助信息 diff --git a/demo/common/operator_runner.h b/demo/common/operator_runner.h index dda3f525..62bd6830 100644 --- a/demo/common/operator_runner.h +++ b/demo/common/operator_runner.h @@ -179,8 +179,7 @@ inline int ApplyOperatorControl( << e.what() << std::endl; return 3; } - } else if (options.example_control && !options.no_default_control && - default_ctrl_json != nullptr) { + } else if (options.example_control && default_ctrl_json != nullptr) { control_payload = default_ctrl_json; } diff --git a/demo/fixtures/mock/pipeline_audio_asr_intent.conf b/demo/fixtures/mock/pipeline_audio_asr_intent.conf index af215b43..90b0f0ee 100644 --- a/demo/fixtures/mock/pipeline_audio_asr_intent.conf +++ b/demo/fixtures/mock/pipeline_audio_asr_intent.conf @@ -4,13 +4,15 @@ "model_paths": { "asr_model_v1": "./models/paraformer_asr_npu.bin" }, - "mem_que": { - "type": "audio_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "transcribed_text": 511, - "intent_slot_json": 1023 + "outputs": { + "audio_out": { + "type": "audio_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "transcribed_text": 511, + "intent_slot_json": 1023 + } } } } diff --git a/demo/fixtures/mock/pipeline_dialogue_audit.conf b/demo/fixtures/mock/pipeline_dialogue_audit.conf index 195b3571..bcd04d30 100644 --- a/demo/fixtures/mock/pipeline_dialogue_audit.conf +++ b/demo/fixtures/mock/pipeline_dialogue_audit.conf @@ -6,14 +6,16 @@ "rerank_model_v1": "./models/bge_reranker_large_npu.bin", "audit_llm_v1": "./models/qwen_1_8b_npu.bin" }, - "mem_que": { - "type": "audit_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "risk_level": 31, - "matched_policy_clause": 255, - "audit_verdict_json": 1023 + "outputs": { + "audit_out": { + "type": "audit_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "risk_level": 31, + "matched_policy_clause": 255, + "audit_verdict_json": 1023 + } } } } diff --git a/demo/fixtures/mock/pipeline_doc_qa.conf b/demo/fixtures/mock/pipeline_doc_qa.conf index b96302e0..4390e142 100644 --- a/demo/fixtures/mock/pipeline_doc_qa.conf +++ b/demo/fixtures/mock/pipeline_doc_qa.conf @@ -5,13 +5,15 @@ "embed_model_v1": "./models/bge_base_npu.bin", "llm_model_v1": "./models/qwen_1.5b_npu.bin" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/demo/fixtures/mock/pipeline_doc_qa_custom.conf b/demo/fixtures/mock/pipeline_doc_qa_custom.conf index 07db4660..29d6be4f 100644 --- a/demo/fixtures/mock/pipeline_doc_qa_custom.conf +++ b/demo/fixtures/mock/pipeline_doc_qa_custom.conf @@ -4,13 +4,15 @@ "model_paths": { "llm_model_v1": "demo/fixtures/mock/artifacts/neutral-llm.fixture" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/demo/fixtures/mock/pipeline_doc_qa_custom.json b/demo/fixtures/mock/pipeline_doc_qa_custom.json index bc2f759a..ff8ade3f 100644 --- a/demo/fixtures/mock/pipeline_doc_qa_custom.json +++ b/demo/fixtures/mock/pipeline_doc_qa_custom.json @@ -87,7 +87,6 @@ }, "config": { "bind_model": "llm_model_v1", - "template_syntax": "standard", "prompt_template": "【参考片段】: {{context}}\n【用户提问】: {{input}}\n请基于参考片段给出精确解答:", "strip_markdown": true, "temperature": 0.3, diff --git a/demo/fixtures/mock/pipeline_doc_qa_rerank.conf b/demo/fixtures/mock/pipeline_doc_qa_rerank.conf index d048b8bb..eacaa060 100644 --- a/demo/fixtures/mock/pipeline_doc_qa_rerank.conf +++ b/demo/fixtures/mock/pipeline_doc_qa_rerank.conf @@ -6,13 +6,15 @@ "rerank_model_v1": "./models/bge_reranker_large_npu.bin", "llm_model_v1": "./models/qwen_1.5b_npu.bin" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "intent_name": 63, - "answer_text": 1023 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "intent_name": 63, + "answer_text": 1023 + } } } } diff --git a/demo/fixtures/mock/pipeline_entity_extract.conf b/demo/fixtures/mock/pipeline_entity_extract.conf index ee018f84..165faebf 100644 --- a/demo/fixtures/mock/pipeline_entity_extract.conf +++ b/demo/fixtures/mock/pipeline_entity_extract.conf @@ -4,12 +4,14 @@ "model_paths": { "entity_llm": "./models/qwen_0_6b_npu.bin" }, - "mem_que": { - "type": "entity_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "entities_json": 2047 + "outputs": { + "entity_out": { + "type": "entity_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "entities_json": 2047 + } } } } diff --git a/demo/fixtures/mock/pipeline_entity_extract_custom.conf b/demo/fixtures/mock/pipeline_entity_extract_custom.conf index b89b4100..ea9245e7 100644 --- a/demo/fixtures/mock/pipeline_entity_extract_custom.conf +++ b/demo/fixtures/mock/pipeline_entity_extract_custom.conf @@ -4,12 +4,14 @@ "model_paths": { "entity_llm": "demo/fixtures/mock/artifacts/neutral-llm.fixture" }, - "mem_que": { - "type": "entity_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "entities_json": 2047 + "outputs": { + "entity_out": { + "type": "entity_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "entities_json": 2047 + } } } } diff --git a/demo/fixtures/mock/pipeline_entity_extract_custom.json b/demo/fixtures/mock/pipeline_entity_extract_custom.json index 325ad154..369c9122 100644 --- a/demo/fixtures/mock/pipeline_entity_extract_custom.json +++ b/demo/fixtures/mock/pipeline_entity_extract_custom.json @@ -29,7 +29,6 @@ }, "config": { "bind_model": "entity_llm", - "template_syntax": "standard", "prompt_template": "你是一个中文实体与名词抽取助手。请从以下句子中提取出所有名词与实体,并仅以JSON列表形式返回:\n输入文本:{{input}}\n提取结果:", "strip_markdown": true, "temperature": 0.1, diff --git a/demo/fixtures/mock/pipeline_ocr_doc_qa.conf b/demo/fixtures/mock/pipeline_ocr_doc_qa.conf index 98de822e..3c2295cf 100644 --- a/demo/fixtures/mock/pipeline_ocr_doc_qa.conf +++ b/demo/fixtures/mock/pipeline_ocr_doc_qa.conf @@ -5,12 +5,14 @@ "ocr_model_v1": "./models/ch_ppocr_v4_det_rec_npu.bin", "llm_model_v1": "./models/qwen_1.5b_npu.bin" }, - "mem_que": { - "type": "od_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": { - "result_json": 2047 + "outputs": { + "od_out": { + "type": "od_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "result_json": 2047 + } } } } diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 879e768b..e6ff1132 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 2026-09-14 投产前框架兼容入口与配置收口(第一批瘦身落地,RFC-0056) + +- **旧入口与兼容别名清理**: + - 彻底删除 7 个历史转发公共头(`company_alg_interface.h`、`company_alg_cpp.hpp`、`company_alg_export.h`、`company_alg_log.h`、`company_alg_version.h`、`operator/operator_interface.h`、`operator/company_operator_types.h`),公共头入口唯一收敛至规范的 `edgeflow/` 路径。 + - 删除 `NodeRegistry` 中的 `NodeFactory` 历史源码兼容别名。 +- **模板解析器收敛与占位符统一**: + - 移除 `PromptGuidedLlmNode` 私有模板解析器及 `template_syntax` 配置字段(`auto`/`standard`/`legacy`),全仓统一调用共享 `ParseTextTemplate`。 + - 占位符唯一规范为 `{{name}}`,保留单括号 `{...}` 作为字面量(支持普通 JSON 单括号模板),消除语义分流与歧义分支。 +- **Operator 输出配置单路径化**: + - Operator 部署配置统一收敛为按逻辑槽位命名的 `data.outputs`,彻底下线 `data.mem_que` 并移除过渡期迁移提示脚手架,旧字段按未知字段统一 Fail-Closed 拒绝。 + - 仓内所有 25 份 `.conf` 部署配置文件、fixture 以及测试用例一次性迁移至 `data.outputs`。 +- **Bridge 描述符回调与标识收敛**: + - 所有输出转换回调统一收敛至槽位级 `convert_output`,移除顶层 `convert_sample_output` 回调及其执行回退逻辑。 + - `OperatorBizBridgeRegistry` 标识匹配严格要求等于 `AdapterName`,移除降级兼容 `biz_name` 分支;单槽 Helper 显式填充 `key_suffix`,消除隐式回退。 +- **无效果兼容参数与闲置接口清理**: + - 删除 Demo 中的无效果兼容选项 `--no-default-control`,未知参数保持 Fail-Closed。 + - 清理 `Pipeline Studio` 中读取废弃 Catalog 字段(`model_config_field` / `model_capability`)的兼容分支与指纹别名,统一为 `model_dependencies`。 + - 清理 `SessionContext` 中未被消费的便利查询(`GetAllRegistrations`、`GetChipType`、`GetPlatformMaxBatch`、`GetDepthNum`)以及 `function_node.h` 中的闲置纯虚接口 `GetRawBatch`。 +- **架构文档与门禁用例同步**: + - 同步更新开发指南与源码布局文档;补充 legacy 字段显式拒绝负向测试用例;通过 LayerGuard 与全部 97 项测试门禁。 + ## 2026-09-14 批次关联、分组、选择回填与拆分公共工具(RFC-0055) - **可追踪批次公共操作**:在能力节点层引入 `include/nodes/traceable_batch_operations.h`(通过 `include/nodes/authoring.h` 导出),提供普通函数与轻量借用视图: diff --git a/doc/VERIFIABLE_SELECTION.md b/doc/VERIFIABLE_SELECTION.md index 81caa561..aae09cda 100644 --- a/doc/VERIFIABLE_SELECTION.md +++ b/doc/VERIFIABLE_SELECTION.md @@ -13,7 +13,7 @@ 先查询目标构建的 Catalog,选择兼容 Model/Backend,修改 Pipeline 中对应实例的 `model_path`、`model_config` / `backend_config`。需要改绑模型实例时,运行 -`describe-node `,按返回的 `model_config_field` 更新 Node `config` 中的对应字段。 +`describe-node `,按返回的 `model_dependencies` 更新 Node `config` 中的对应字段。 例如 `LlmGenerateNode` 使用 `config.bind_model` 引用 `models[].model_id`;保留模型实例 ID 只更换权重时,无需修改节点绑定。 Node 的检索数、生成预算、模板等业务参数放在 Node `config`,字段说明与默认值通过 @@ -83,7 +83,7 @@ python3 tools/verify_selection.py check \ 组装仍由 Adapter 完成,见[输入输出边界](dev_guide/business_onboarding.md#输入输出以-c-abi-为边界)。 验收器为选定 Pipeline 生成临时 `.conf`,从 `--conf`(默认同名 `.conf`)继承输出池配置,按 `--model-root` 生成模型路径;不会沿用原 `.conf` 中可能覆盖模型选择的 `model_paths`。 -验收固定使用 CPU、device 0、batch 1,并保留兼容选项 `--no-default-control`;Demo 默认使用所选规则/提示词。这个版本的验收目标是配置正确性与选定输出字段的业务效果;目标设备性能验收需要相应环境与后续测试定义。 +验收固定使用 CPU、device 0、batch 1;Demo 默认使用所选规则/提示词。这个版本的验收目标是配置正确性与选定输出字段的业务效果;目标设备性能验收需要相应环境与后续测试定义。 ```bash python3 tools/verify_selection.py evaluate \ diff --git a/doc/architecture.md b/doc/architecture.md index f4ede8a6..3f8f985f 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -157,7 +157,7 @@ C++ Operator API:NamedIoBatch + Operator 镜像 C 结构 ─┘ - C++ Operator API 根据 Key 的最后一个点号解析槽位后缀: `OperatorValueTypeRegistry` 负责“后缀到外部 C 类型”的唯一绑定, `OperatorBizBridgeDescriptor` 负责按业务和方向收集一个或多个槽位,再转换为 - 内部 DTO;输出槽位可通过 `key_suffix` 独立命名,默认沿用类型后缀。 + 内部 DTO;输出槽位须显式指定 `key_suffix`(单槽 Helper 默认填充为规范类型后缀),与逻辑槽及类型解耦,可独立命名;描述符中已不再支持运行时省略或隐式回退。 同一外层类型可以注册多个分配方案;部署配置选择方案与嵌套布局参数。 两种协议不得通过 `reinterpret_cast` 混用布局。 - 组件调用关系:`外部调用方 → Operator / C ABI → Pipeline → Node → Model → Backend → Platform`。 @@ -175,7 +175,7 @@ C++ Operator API:NamedIoBatch + Operator 镜像 C 结构 ─┘ `.conf` 的 `data.outputs` 按逻辑槽位归一化输出类型、分配方案、参数与容量; 最外层的独立配置读取组件按固定枚举提取配置并返回字符串,注册方案在 Create 将自己的参数文本解析为普通 C++ 结构;分配和业务转换共享该不可变结构。 - 单输出也可使用原 `data.mem_que`,两者互斥。每个逻辑输出槽位拥有独立输出池, + 每个逻辑输出槽位拥有独立输出池, 池深只由框架应用;分配实现只处理一份完整输出。见 [输出分配方案](dev_guide/operator_output_allocation.md)。 diff --git a/doc/archive/FRAMEWORK_SLIMMING_IMPLEMENTATION_REVIEW_2026-09-14.md b/doc/archive/FRAMEWORK_SLIMMING_IMPLEMENTATION_REVIEW_2026-09-14.md new file mode 100644 index 00000000..1ffb297a --- /dev/null +++ b/doc/archive/FRAMEWORK_SLIMMING_IMPLEMENTATION_REVIEW_2026-09-14.md @@ -0,0 +1,154 @@ +# 第一批框架瘦身实施复核 + +- 复核日期:2026-09-14。 +- 对照材料:[投产前框架冗余、兼容负担与瘦身审查](FRAMEWORK_SLIMMING_REVIEW_2026-09-14.md)。 +- 当前 HEAD:`09437485aec0704175cdc3acbdf73c8a9b47891e`;实现位于暂存区,未提交。 +- 待审范围:相对 HEAD 的 88 个文件,701 行增加、734 行删除。本报告及归档索引不计入该实施范围。 +- 暂存补丁标识:`git diff --cached --binary` 的 SHA-256 为 `0dc7cbc4e40884388ab1f6e42448bd323f77d1937992e23944f5f7654cbfa8f6`。 +- 本轮仅复核并保存结论,不修改用户的实现、测试或暂存内容。以下源码行号对应本次待审工作区。 + +## 1. 结论 + +**第一批实施方向基本符合预期,主体迁移完成,但尚不能判为全部收尾。确认一处自定义输出槽的行为回归,以及 Catalog 描述、活动指南、回归测试和迁移决策记录的遗漏。** + +统一门禁已经通过:97/97 个 CTest 测试成功。这个结果说明当前门禁没有发现失败,不排除测试未覆盖的扩展场景;本轮输出 key 回归已通过新旧头文件独立探针确认。 + +Changelog 明确将此次实施称为“第一批瘦身落地”。因此,原报告 B1/B2 和 C1–C3 尚未实施属于后续范围,不应把它们列作本批回归,也不能据此宣称原报告全部建议已完成。 + +## 2. 需要修正的发现 + +### F1 [P2] Typed Bridge 自定义逻辑输出槽意外改变外部 map key + +**位置:** [operator_biz_bridge.h](../../include/adapter/operator_biz_bridge.h) 第 125–126、166–167、224–225 行。 + +`MakeSingleSlotBizBridge` 现在把 `key_suffix` 显式设为传入的 `output_slot`。两种 `MakeTypedSingleSlotBizBridge` 随后只将 `type_suffix` 更新为 `HostOutput` 的规范类型,没有同步确定原有外部 key。 + +传入逻辑槽名 `custom_out`、输出宿主类型 `CompanyOperatorEntityOutput` 时,独立探针对比结果如下: + +| 实现 | logical_name | type_suffix | KeySuffix() | +| --- | --- | --- | --- | +| HEAD 旧实现,两种 typed helper | custom_out | entity_out | entity_out | +| 当前实现,两种 typed helper | custom_out | entity_out | custom_out | +| 默认槽名,新旧实现 | entity_out | entity_out | entity_out | + +原有外部 `chan.entity_out` 因而不能匹配当前描述符。[operator_process_binding.cpp](../../src/adapter/operator/operator_process_binding.cpp) 第 135 行按 `KeySuffix()` 比较,第 157–163 行在找不到必需输出 key 时返回 `-4`。 + +原报告 A4 要求 helper 显式填写“同样的输出 key”,并明确逻辑槽名、类型和外部 key 是不同概念。此次变化超出了移除隐式回退的范围。内置桥大多使用默认槽名,所以当前 smoke 和一般业务测试未触发。 + +**建议:** 两种 typed helper 在确定 `HostOutput` 类型后,同时显式设置原先使用的规范输出 key;若调用方需要不同外部 key,继续通过槽位字段明确指定,不恢复运行时回退。扩展现有 [TypedBuilderSupportsCustomInputSlotName](../../tests/unit/operator/test_operator_biz_bridge_registry.cpp) 第 877 行起的测试:它已经传入 `custom_out`,但只断言输入转换,应补两种 helper 的输出 key、输出绑定及显式自定义外部 key 覆盖。 + +**复现记录:** 在 `/tmp/slimming-bridge-probe-f_benlzm/` 保存独立 `probe.cpp` 和通过 `git show HEAD:include/adapter/operator_biz_bridge.h` 提取的旧头。分别用 C++17 编译并运行旧头/当前头探针,两者编译成功,输出如上;未修改项目测试或构建目录。 + +### F2 [P2] TextTemplate 的运行时 Catalog 仍宣称支持单括号变量 + +**位置:** [text_template_node.cpp](../../src/common_nodes/text_template_node.cpp) 第 627–629 行。 + +`NodeDefinition.description` 仍输出: + +```text +Text template rendering: {{name}} and {name} substitute variables; +JSON braces remain literal +``` + +而共享解析器已正确收敛为仅 `{{name}}` 是变量。重建后查询 `alg_pipeline_tool catalog`,上述旧描述仍然存在;独立解析器探针实际返回: + +```text +Hello {name} => ok=1, variables=0 +Hello {{name}} => ok=1, variables=1 +``` + +这不是仅存在于历史文档中的文字。Catalog 是当前开发者和编排工具使用的能力事实来源,按其说明写单括号模板,会得到成功执行但没有变量替换的结果。 + +**建议:** 更新 Definition 描述,明确单括号只是字面文本,并同步 [platform_mock/operator_types.h](../../include/platform_mock/operator_types.h) 第 57 行仍写单括号占位符的注释。现有新语法行为测试保留;重建后核对 `describe-node TextTemplateNode` 或 Catalog 输出。 + +### F3 [P3] 活动指南仍承诺已经删除的用法 + +| 位置 | 遗留说明 | 当前行为及修正方向 | +| --- | --- | --- | +| [Operator 输出分配指南](../dev_guide/operator_output_allocation.md) 第 39 行 | key_suffix “省略时沿用 type_suffix” | Registry 已要求非空。应说明手写描述符必须显式填写、与逻辑槽及类型独立,以及 helper 的默认规则 | +| [pipeline-composer SKILL.md](../../.agents/skills/pipeline-composer/SKILL.md) 第 82–83 行 | --no-default-control 仍是可用兼容参数,且与 --example-control 冲突 | CLI 现在把它作为未知参数拒绝。应删除可用性说明,保留默认不发送示例 Control 和显式 --example-control 的用法 | + +这些都是现行操作指导,按前者编写扩展可能导致注册失败,按后者保留旧参数会导致 Demo 参数解析失败。历史 RFC 和原审查报告里的旧语法无需改写。 + +### F4 [P3] 新增 AdapterName 负向测试没有证明旧 biz_name 回退已被删除 + +**位置:** [test_operator_biz_bridge_registry.cpp](../../tests/unit/operator/test_operator_biz_bridge_registry.cpp) 第 674–689 行。 + +新增 `IsolatedRegistryRejectsAdapterNameMismatch` 使用 `doc_qa_v1` 作为错误标识。但 [doc_qa_adapter.cpp](../../src/adapter/biz/doc_qa_adapter.cpp) 第 15 行声明的实际 biz 是 `smart_doc_qa_v1`,AdapterName 是第 23 行的 `DocQA`。 + +`doc_qa_v1` 在旧实现中同样不能匹配 AdapterName 或声明的 biz,所以该输入不能区分旧回退实现和当前严格实现。当前生产代码确实移除了回退;问题在于这项新增测试无法防止它再次被引入。 + +**建议:** 将待拒绝值设为 Adapter 实际声明的 `smart_doc_qa_v1`,最好从描述符获取,使测试证明“合法 Pipeline biz_name 也不能作为 AdapterName”。保留规范 AdapterName 注册成功的既有覆盖。 + +### F5 迁移决策记录未完成 + +本次实现没有新增或更新 `doc/rfcs/` 下的迁移 RFC。它已经涉及公开源码头入口、模板配置语义、Operator 输出配置和 Bridge 扩展契约,符合 [CONTRIBUTING.md](../../CONTRIBUTING.md) 第 17、32–39 行的 RFC 范围。 + +尤其 [RFC-0046](../rfcs/0046-naming-and-header-boundaries.md) 曾明确保留旧公共头,[RFC-0049](../rfcs/0049-operator-output-allocation-strategies.md) 曾明确保留旧输出配置及回调/key 默认行为。Changelog 记录了发生什么,但尚未明确哪些旧兼容决策被取代。 + +**建议:** 新建本次迁移 RFC,明确第一批范围、规范契约、被取代决策、配置/扩展迁移规则及验证结果;保留历史 RFC 正文。将剩余 B/C 项标为后续或有意保留,不把此次审查当作已批准的全部架构变更。 + +## 3. 对原报告逐项核对 + +| 原编号 | 当前状态 | 依据与剩余范围 | +| --- | --- | --- | +| A1 旧公共头与 NodeFactory | 主体完成 | 七个旧头、别名及构建清单已删;规范 edgeflow/operator/types.h 和 C11/头可见性保护保留 | +| A2 模板语法 | 实现完成,元数据未收尾 | 私有旧解析器和 template_syntax 已删,默认值、fixture、缺变量及 JSON 字面行为已迁移;剩 F2 | +| A3 data.outputs | 完成 | 配置、生成器、Studio 保存/运行及效果工具已迁移;旧字段拒绝和多输出校验保留 | +| A4 Bridge 单路径 | 主体完成,有回归 | 槽位回调、严格 AdapterName、显式 key 均落地;剩 F1、F3、F4 | +| A5 Demo 参数 | 实现完成,skill 未收尾 | 字段、解析、帮助、冲突判断和内部传参已删;剩 F3 | +| A6 Catalog 旧字段 | 完成主体清理 | 前端移除 model_capability/model_config_field,使用 model_dependencies;单依赖选择/改名/删除保护及版本拒绝保留 | +| B1 Core 诊断统一 | 未实施 | 两套枚举及双向映射仍在,应作为第二批独立改动 | +| B2 Node 注册状态统一 | 未实施 | Registry/Catalog 分离存储仍在,保留高风险独立设计和验证要求 | +| B3 Model/Node 闲置接口 | 部分完成 | GetRawBatch 已删;GetMaxBatchSize 仍在,等待明确接口决策 | +| B4 Session 范围 | 部分完成且符合第一步 | 四个零调用便利查询已删;revision 修改、资源读写等有测试/扩展含义的接口仍保留 | +| C1 宽松 Validator 策略 | 保留 | 测试 Harness 仍依赖,不能简单替换为 strict | +| C2 Node 初始化/作者路径 | 保留 | 无计划初始化与 TraceableUnaryInferenceNode/脚手架未误删 | +| C3 双测试装配 | 保留 | 应另行决定是否统一声明或收缩诊断模式 | +| C4 工具和治理 | 部分完成 | 指纹别名及猜测式测试已清理;静态 include 规则集中、历史禁用项表尚未实施 | + +A6 多模型依赖专项测试尚未找到,当前循环实现未见明显错误,可作为验收补强,不列为已证实运行时缺陷。 + +保留 C ABI/Operator 共享运行时、完整业务 Adapter 转换、输出池生命周期、批策略、来源校验和配置快照符合原报告预期。没有发现本次为瘦身而删除这些必要机制。 + +## 4. 实际验证 + +1. `LLM_EDGEFLOW_JOBS=4 ./scripts/run_all_tests.sh > /tmp/framework-slimming-implementation-gate.log 2>&1`:退出码 0,97/97 个 CTest 测试通过,门禁耗时 47 秒。仅执行一次完整门禁。 +2. `git diff HEAD --check`:通过,覆盖暂存实施变更。 +3. 重建后 `./build/alg_pipeline_tool catalog`:退出码 0、schema_version=3、ok=true;12 Nodes、6 Models、8 bizs、14 Profiles,2 Backends(llama_cpp、onnxruntime)。快照位于 `/tmp/framework-slimming-rebuilt-catalog.json`。 +4. 所有 27 份变更 `.conf` 做 JSON 等值对比:只将旧池配置放入 `outputs` 对应槽,模型路径、容量和其余字段全部一致;其中包括原报告关注的 25 份 configs/Demo 文件和额外 2 份测试配置。 +5. 新旧 typed Bridge 头文件独立探针:确认 F1;默认槽名不受影响,自定义逻辑槽名会改变 key。 +6. 当前模板解析器独立探针 `/tmp/slimming-template-probe.cpp` 与重建 Catalog 对照:确认 F2。 +7. 报告及归档索引另做相对文件链接和空白检查;没有修改、重新暂存用户实施内容。 + +门禁默认关闭 Kite/Whisper Backend 和专门的真实模型 E2E 验证,不能将本次结果解释为全部 Backend、真实模型效果或目标设备验收。此次配置等值对比也不替代那些环境中的部署运行。 + +## 5. 收尾建议 + +优先修 F1 并在现有 Operator 测试中补输出 key/绑定断言;同步处理 F2/F3 的当前说明和 F4 的鉴别性回归用例,补 F5 的迁移决策。修改后按项目流程重新执行所需验证和一次统一门禁。 + +第一批通过这些收尾后可以独立验收。B1/B2、剩余 B3/B4、C1–C3 和 C4 静态治理合并应继续按原报告分批决定,不需要为了宣称“全部瘦身完成”而在同一改动中扩大范围。 + +## 6. 修复后复验(2026-09-14) + +本节对应用户根据上述发现完成修复后的工作区;前文保留原审查时的事实与结论。复验暂存补丁 SHA-256 为 `42b678c300585aa13abd76df42e791860053eda6398c4dec444e48e9b267a9b1`。 + +**复验结论:F1–F5 均已解决,本次未发现新的阻断问题;第一批瘦身符合预期,可以按本批范围验收。** + +| 原发现 | 复验结果 | +| --- | --- | +| F1 typed Bridge 输出 key 回归 | 两种 typed helper 都在构造时明确填入规范输出 key;自定义逻辑槽不改变默认外部 key,显式自定义 key 也保留。测试覆盖成功绑定、错误 key 拒绝及第二重载;运行时 KeySuffix 没有恢复回退,Registry 仍拒绝不完整描述符 | +| F2 TextTemplate Catalog 描述 | Definition 和公开模拟头注释已更新;新增描述检查,重建后的 Catalog 明确仅双括号替换,单括号和 JSON 花括号保留字面内容 | +| F3 活动指南残留 | 输出分配指南明确非空 key_suffix;pipeline-composer 已删除旧 CLI 参数可用性说明 | +| F4 AdapterName 回归测试缺乏鉴别性 | 测试从实际 Adapter Definition 取得 smart_doc_qa_v1,并增加全业务检查;能发现旧 biz_name 回退被重新引入 | +| F5 迁移决策缺失 | [RFC-0056](../rfcs/0056-batch-1-framework-slimming.md) 和索引已补齐,明确取代旧兼容决策及第一批边界 | + +验证证据: + +- 独立复核 Bridge 的两个 helper、运行时拒绝规则及新增输出绑定/AdapterName 测试,未发现新的回归。 +- `LLM_EDGEFLOW_JOBS=4 ./scripts/run_all_tests.sh > /tmp/framework-slimming-fixes-gate.log 2>&1`:退出码 0,97/97 个 CTest 测试通过;门禁耗时 35 秒,CTest 耗时 31.19 秒。仅执行一次完整门禁。 +- `git diff HEAD --check` 通过;验证前后暂存补丁 SHA-256 一致。 +- 重建 Catalog(schema_version=3)保存于本机 `/tmp/framework-slimming-fixes-catalog.json`,TextTemplate 描述为 `Text template rendering: {{name}} substitutes variables; single {name} and JSON braces remain literal`。 +- 新 RFC、输出分配指南与 pipeline-composer 的相对文件链接检查通过。本复验记录补录后另做链接与空白检查,未改动或重新暂存用户实现。 + +原报告第二、三批的设计收敛与产品范围选择仍属于后续工作,本次未扩大验收范围;也未新增真实模型、非默认 Backend 或目标硬件验收结论。 diff --git a/doc/archive/FRAMEWORK_SLIMMING_REVIEW_2026-09-14.md b/doc/archive/FRAMEWORK_SLIMMING_REVIEW_2026-09-14.md new file mode 100644 index 00000000..862e7a50 --- /dev/null +++ b/doc/archive/FRAMEWORK_SLIMMING_REVIEW_2026-09-14.md @@ -0,0 +1,266 @@ +# 投产前框架冗余、兼容负担与瘦身审查 + +- 审查日期:2026-09-14 +- 源码基线:`b08c5bd80e8422e8e5f6f44424e299ccd10f3d46` +- 前提:框架尚未正式投入生产,可以一次性迁移仓内消费者并放弃旧接口、旧配置兼容。 +- 本次交付:审查报告;未实施下述删除或重构。建议不代表已经批准的架构决策。 +- 代码行号对应上述基线。本文属于日期型审计,后续实现另行记录,不回写历史结论。 + +## 1. 结论 + +**存在明确的兼容残留,也存在可以收敛的重复机制,当前适合清理。主要收益是减少接口、配置语义和维护分支;现有证据不支持承诺显著降低 SDK 体积或提高推理性能。** + +最值得先做的是:删除旧名称和无效果参数,统一模板语法,统一 Operator 输出配置及回调形式,删除 Studio 的旧 Catalog 消费分支。随后处理 Core 诊断码往返转换、Node 注册状态分散,以及没有生产消费者的查询和便利接口。 + +框架已经完成不少有效收敛:C ABI 与 Operator 共用内部运行时;Pipeline 使用同一个 Validator 和执行计划;Studio 委托 native Validator;Model 与 Backend 职责分离。建议采用有边界的减法,不建议重写四层架构,也不建议仅按文件长度、测试数量或抽象类数量决定删除。 + +这里讨论的兼容负担主要是**新版继续接受旧源码名称、旧配置和旧语法的向后兼容**,而不是要求旧版本理解未来新格式。 + +## 2. 范围、方法与证据限制 + +审查覆盖接入适配层 / Integration、流程编排层 / Orchestration、能力节点层 / Capability Nodes、模型执行层 / Model Execution,以及 Demo、Studio、脚本、CMake、测试、配置和治理文档。按层并行检查后交叉核对调用关系、生成器、测试依赖和已有 RFC;这不是每个函数均已形式化验证的正确性审计。 + +基于 `git ls-files` 的范围盘点如下。行数按所列目录内常见代码、配置、Markdown 和文本后缀统计,含注释与空行,排除生成物和第三方缓存,**不是可删除行数**。 + +| 范围 | 受版本管理文件数 | 文本行数 | +| --- | ---: | ---: | +| `src/` | 104 | 21,686 | +| `include/` | 84 | 12,163 | +| `tests/` | 95 | 39,091 | +| `demo/` | 38 | 4,735 | +| `tools/` | 12 | 3,503 | +| `scripts/` | 19 | 4,123 | +| `cmake_ext/` | 18 | 2,108 | +| `dev_support/` | 26 | 2,750 | +| `configs/` | 35 | 2,789 | +| `doc/` | 113 | 29,712 | + +已实际运行 `./build/alg_pipeline_tool catalog`。现有构建返回 Catalog v3、12 个 Node、6 个 Model、8 个 biz、14 个 Profile,启用的 Backend 为 `llama_cpp` 和 `onnxruntime`。构建缓存为 Release,Kite/Whisper Backend 关闭;Catalog 中出现 Model 不等于当前构建具有可运行它的 Backend 或模型资产。首次查询使用已有二进制,不能单凭它证明所有源码路径均已重新构建。 + +对受版本管理、超过 100 字节的 `.cpp/.h/.hpp/.c/.py/.js/.sh/.cmake/.json/.conf` 文件做内容哈希比对,未发现整文件完全重复组。下文发现的主要是**同一职责的双表示、兼容分流和闲置接口**;整文件哈希无法排除这种逻辑重复。 + +“无调用”均指本基线仓库内符号检索和相关调用链检查,不包含无法访问的仓外扩展。未接入公司内部 SDK,未做目标硬件推理、真实模型效果验收或删除前后性能测量。 + +## 3. 建议总表 + +优先级表示实施顺序:P1 为优先清理,P2 为第二阶段设计收敛,P3 为需要产品范围决策。它不是安全漏洞严重性;风险表示删除或迁移的风险。 + +| 编号 | 建议 | 类型 | 优先级 / 风险 | 主要收益 | +| --- | --- | --- | --- | --- | +| A1 | 删除七个旧公共头入口及 NodeFactory 别名 | 明确历史兼容 | P1 / 低 | 唯一命名,减少公开入口 | +| A2 | 模板只保留共享解析器和一种占位符语法 | 明确历史兼容 | P1 / 中 | 消除多套语义和歧义分支 | +| A3 | Operator 输出配置统一为 data.outputs | 明确历史兼容 | P1 / 中 | 单/多输出统一解析 | +| A4 | Bridge 回调、标识和输出 key 单路径化 | 兼容及隐式默认 | P1 / 中 | 减少注册与执行分流 | +| A5 | 删除 --no-default-control | 无效果兼容参数 | P1 / 低 | 去除参数、冲突判断和内部传递 | +| A6 | Studio 只消费 Catalog v3 当前字段 | 旧格式消费分支 | P1 / 低 | 前端模型依赖单一表示 | +| B1 | Core 统一诊断码,保留必要报告容器 | 结构重复 | P2 / 中 | 删除往返和有损映射 | +| B2 | Node creator 与 Definition 统一存储 | 状态和所有权重复 | P2 / 高 | 减少双表一致性及锁约束 | +| B3 | 清理无消费者的 Model 查询及 Node 内部接口 | 接口范围过宽 | P2 / 低至中 | 减少必实现方法和裸指针暴露 | +| B4 | 清理 Session 便利查询及测试专用变更入口 | 接口范围过宽 | P2 / 低至中 | 缩小会话资源变更面 | +| C1 | 决定是否取消私有扩展宽松校验策略 | 历史策略及测试便利 | P3 / 中 | Validator 只维护一种完整契约 | +| C2 | 收敛 Node 初始化和脚手架作者路径 | 真实双路径 | P3 / 中至高 | 减少绑定和作者模式重复 | +| C3 | 收敛双套测试装配 | 开发设施重复 | P3 / 中 | 减少构建维护矩阵 | +| C4 | 合并静态规则、删工具别名及猜测式测试 | 工具和治理重复 | P2 / 低至中 | 减少规则漂移和测试兜底 | + +## 4. P1:明确兼容残留 + +### A1. 删除旧公共头和旧注册表名称 + +**事实。** 七个旧入口仅转发当前头:`include/company_alg_interface.h`、`company_alg_cpp.hpp`、`company_alg_export.h`、`company_alg_log.h`、`company_alg_version.h`,以及 `include/operator/operator_interface.h`、`company_operator_types.h`。例如 [旧 C ABI 入口](../../include/company_alg_interface.h) 第 3 行明确标注 Compatibility。它们仍列在 [公共头清单](../../cmake_ext/LayerHeaderViews.cmake) 第 45–48 行。[NodeRegistry](../../include/core/node_registry.h) 第 139–140 行也明确保留 `NodeFactory` 源码兼容别名;当前 C++ 引用只有该声明。 + +**建议。** 仓内调用统一使用当前入口,然后删除七个旧头及 `NodeFactory`。同步清理公共头清单、依赖归属表和旧头成功编译测试。根 [CMakeLists.txt](../../CMakeLists.txt) 第 57–59 行为旧生成版本头维护的构建目录清理,可在要求一次干净构建后去掉。 + +**影响与验收。** 破坏旧 include 和旧 C++ 名称,不要求修改六个 C ABI 函数、结构布局或共享库名称。保留新头的 C11 编译与公共/扩展/内部头可见性测试。收益主要是入口数量,转发头本身几乎没有运行时成本。 + +**明确排除。** [edgeflow/operator/types.h](../../include/edgeflow/operator/types.h) 虽然也是转发头,却是 [源码布局指南](../dev_guide/source_layout.md) 第 105 行规定的当前路径,也是 `edgeflow/operator/interface.h` 的依赖。它可以隔离 `platform_mock/` 实现位置,不应与七个旧名称入口一起删除。未来如要改变此门面,应另作公开源码路径决策。 + +### A2. 删除 PromptGuided 的 legacy/auto,并统一占位符拼写 + +**事实。** [prompt_guided_llm_node.cpp](../../src/custom_nodes/prompt_guided_llm_node.cpp) 第 29–70 行维护独立旧模板解析器,91–106 行选择 `auto/standard/legacy` 并处理歧义,153–161 行公开三模式配置。全仓 JSON 搜索仅发现两份内置 PromptGuided 配置,均显式为 `standard`:[DocQA fixture](../../demo/fixtures/mock/pipeline_doc_qa_custom.json) 第 90 行和 [EntityExtract fixture](../../demo/fixtures/mock/pipeline_entity_extract_custom.json) 第 32 行。 + +共享 [text_template.h](../../include/nodes/text_template.h) 第 27 行又声明同时接受 `{{name}}` 与兼容的 `{name}`,第 50–72 行分别处理两种括号。这是另一层兼容,不会随着删除 PromptGuided 私有解析器自动消失。 + +**建议。** 固定调用共享解析器,移除 `template_syntax` 配置字段和私有旧解析器;将占位符统一为 `{{name}}`,普通 JSON 单花括号作为字面内容。一起迁移默认模板、两份 fixture、文档及模板 Control 示例。 + +**影响与验收。** 旧模板测试仍依赖旧语义,不能称无人使用。更新 [test_common_nodes.cpp](../../tests/unit/nodes/test_common_nodes.cpp) 第 1053、1183、1298 行附近,以及 [test_text_template_node.cpp](../../tests/unit/nodes/test_text_template_node.cpp) 第 82、103 行附近测试。保留未知变量、缺失 context 绑定、非法双括号、JSON 字面文本和 Control 更新校验,尤其保留“插入用户文本后不再二次解析”的行为。这里减少的是模板语义数量,不是提示词能力。 + +### A3. Operator 输出配置只保留 data.outputs + +**事实。** [operator_config_resolver.cpp](../../src/adapter/operator/operator_config_resolver.cpp) 第 468 行显式区分 `legacy_output` / `named_outputs`;470 行检查互斥,479 行为旧格式限制单槽,514 行选择对应读取路径。 + +**不是死代码。** `configs/` 与 `demo/fixtures/` 下 25 份 `.conf` 使用 `mem_que`,未发现使用 `outputs` 的 `.conf`。多输出已有 [Operator 集成测试](../../tests/integration/operator/test_operator_api.cpp) 第 2447 行起的实际覆盖,第 2511 行起检查新旧格式不能混用。 + +**建议。** 单输出也统一写入以逻辑槽位为键的 `data.outputs`,一次迁移所有仓内配置、配置生成工具、Demo/Studio 草稿运行路径及测试;随后删除 `mem_que` 成功解析路径和双模式互斥分支。不要为此次迁移再增加常驻兼容开关或运行时转换器。 + +**影响与验收。** 配置契约破坏性变更,应立 RFC;所有官方 `.conf` 需重新 resolve,相关方案需 validate/plan 和 Demo smoke。保留未知输出槽、缺必需槽、容量、分配参数及总内存预算校验。不能只删除解析分支,让 25 份现有配置失效。 + +### A4. Bridge 描述符使用一套回调与标识规则 + +| 现状与证据 | 建议 | +| --- | --- | +| [operator_biz_bridge.h](../../include/adapter/operator_biz_bridge.h) 第 63 行有槽位 convert_output,第 96 行又有顶层 convert_sample_output;[registry](../../src/adapter/operator/operator_biz_bridge_registry.cpp) 第 141 行允许单输出回退;[执行入口](../../src/adapter/operator/operator_adapter.cpp) 第 398 行每次选择其一 | 所有输出回调下沉到槽位,删除顶层回调及比较、校验、执行回退;单槽 helper 保留并生成同一种描述符 | +| registry 第 256–270 行让 adapter_name 同时接受 AdapterName 和 Pipeline biz_name,注释明确称兼容旧 bridge | 只接受 AdapterName;内置桥已使用 DocQA、Translate 等 Adapter 标识,没有找到依赖此回退的内置桥 | +| header 第 61–66 行允许空 key_suffix 回退为 type_suffix,单槽 helper 第 129 行依赖该默认 | 可让 helper 显式填写同样的输出 key,注册时要求非空,删除运行时隐式回退;作为同一改动的低收益尾项 | + +**影响与验收。** 改变 C++ 扩展描述符,迁移内置桥和 [registry 测试](../../tests/unit/operator/test_operator_biz_bridge_registry.cpp) 第 55、315、675 行附近的双形式用例。保留单/多输出端到端结果,以及分配失败、回收、注册冲突和缺回调拒绝测试。 + +**边界。** type_suffix 与输出 map key 是不同概念:多个输出可以共享外层类型但必须有不同 map key。不能把两者合为一个字段,也不应强制 key 等于 logical_name。Adapter 声明多个合法 biz 契约同样不属于这项兼容回退。 + +### A5. 删除无效果的 --no-default-control + +**事实。** [demo_options.h](../../demo/common/demo_options.h) 第 42–43 行明确标为 Compatibility;[demo_options.cpp](../../demo/common/demo_options.cpp) 第 237 行解析它,249 行专门检查其与 `--example-control` 冲突,595 行帮助也称兼容参数。默认已经不发送示例 Control,因此在正常 CLI 路径中该参数没有额外作用。[operator_runner.h](../../demo/common/operator_runner.h) 第 182 行仍检查它。 + +工具仍主动传递这个空操作参数:[verify_selection.py](../../tools/verify_selection.py) 第 236 行、[Studio server](../../tools/pipeline_studio/server.py) 第 562 行。 + +**建议与验收。** 删除字段、解析、冲突分支、帮助和所有内部传参,更新 Demo/Studio 测试和教程。保留默认行为、显式 `--example-control`、Control 文件优先级及真正的命令路由。通过对照 Demo 默认运行和显式示例 Control,证明清理没有改变有效功能。 + +### A6. Studio 删除旧 Catalog 字段消费分支 + +**事实。** [app.js](../../tools/pipeline_studio/web/app.js) 第 245、305 行拒绝非 v3 Catalog,但第 233–234 行仍读取 `model_config_field` / `model_capability`。[workbench.js](../../tools/pipeline_studio/web/workbench.js) 第 10、34、215、230、245 行保留同类回退。 + +native 当前输出已经只用 `model_dependencies`;[Catalog 契约测试](../../tests/contract/catalog/test_catalog_contract_ssot.cpp) 第 219–220 行明确断言旧字段不存在。因此这里是消费者未收尾,不需要再给 native 增加或删除一套旧字段。 + +**建议与验收。** 所有模型依赖读取统一为 `model_dependencies`,迁移旧字段测试夹具;保留合法的字段语义提示和无模型节点处理。覆盖模型选择、替换、删除、多个模型依赖,以及拒绝非 v3 Catalog。此项主要降低前端维护成本。 + +## 5. P2:结构重复与接口范围收缩 + +### B1. 统一 Core 诊断码,消除往返降维 + +**事实。** [pipeline_diagnostic.h](../../include/core/pipeline_diagnostic.h) 第 10 行定义 `PipelineErrorCode`;[pipeline_validator.h](../../include/core/pipeline_validator.h) 第 20 行另有 `DiagnosticCode`。实际路径是: + +```text +PipelineConfig 解析错误 + → PipelineErrorCode + → Validator 的 PipelineErrorCodeToDiagnosticCode + → DiagnosticCode + → Pipeline::Build 的 ValidationCodeToPipelineCode + → PipelineErrorCode +``` + +转换位置为 [pipeline_validator.cpp](../../src/core/pipeline_validator.cpp) 第 107 行和 [pipeline.cpp](../../src/core/pipeline.cpp) 第 21、483 行。多个精确错误被降为 `kInvalidCombination`,Build 又在第 485 行把原码拼入 message。这是可观察的重复表示和信息补偿;当前源码不足以证明它完全由历史兼容造成。 + +**建议。** 使用一个 Core 诊断码集合,覆盖解析、校验、物化和生命周期。单条 Build 错误与多条校验报告可以保留不同容器,避免让所有路径携带整份报告。C ABI 状态映射继续留在 Integration 边界。 + +**验收重点。** 同一错误在解析/校验/Build 中具有一致身份;CLI 的稳定错误码、JSON Pointer、关联节点与修复信息不丢失;[shared_algorithm_runtime.cpp](../../src/adapter/shared_algorithm_runtime.cpp) 第 170、259 行对注册冲突的状态映射不退化。涉及 Core C++ 契约,应有 RFC。 + +### B2. Node 的 creator 和 Definition 放入同一注册 Entry + +**事实。** [node_registry.h](../../include/core/node_registry.h) 第 134 行持有 creator 表,而 [pipeline_catalog.cpp](../../src/core/pipeline_catalog.cpp) 第 18、28 行另持 Definition 集合和锁。[node_registry.cpp](../../src/core/node_registry.cpp) 第 32–50 行先锁 Registry,再注册 Catalog Definition,再保存 creator。Validator 第 1188–1189 行分别查两侧。 + +Model/Backend 已在单个 Entry 同时持有 creator 与 Definition,见 [model_registry.h](../../include/engine/model_registry.h) 第 60 行和 [backend_registry.h](../../include/engine/backend_registry.h) 第 49 行。Catalog 对它们执行查询委托。 + +**建议。** Node 采用同样的单所有者结构;Catalog 提供排序、快照和序列化视图,停止单独维护可注册的 Node 状态。保留 Definition 的纯校验函数供测试调用。不要再为三个 Registry 引入复杂的万能注册框架,先解决 Node 的具体双表问题。 + +**收益与风险。** 减少一致性状态和嵌套锁约束,但属于高风险所有权重构,需要 RFC 和独立复核。Control 命令跨 Node 一致性审计、静态注册异常屏障、失败后 fail-closed、冲突检测及 creator 可重入能力仍要保留。不能因存储合并而删除这些约束的测试。 + +### B3. 清理 Model 查询和 Node 内部闲置接口 + +**候选一:IModel::GetMaxBatchSize。** [model_interface.h](../../include/engine/model_interface.h) 第 26 行强制所有 Model 实现此查询;`src/` 内只有定义,没有查询消费者。可以通过明确接口决策删除统一纯虚查询,减少每个模型及 mock 的必实现方法,让批处理能力留在实际执行路径。 + +它不是纯死代码:[dev_support/inference/test_capability_models.h](../../dev_support/inference/test_capability_models.h) 第 39、96 行及部分测试仍用它构造策略;[RFC-0036](../rfcs/0036-whisper-asr-backend.md) 第 176 行也定义过它的语义。仓内无生产调用不证明仓外扩展无人使用。修改涉及 Model 源码扩展契约,优先级低于明确旧语法清理。 + +迁移测试时改为局部常量或私有 helper,保留 Backend `GetBatchPolicy()`、模型限额约束和 `FixedBatchExecutor::Execute`。[BGE Embedding](../../src/engine/models/bge_embedding/bge_embedding_model.cpp) 第 172、230 行存在真实批策略校验及执行。验收重点是批次上限、padding 去除和来源对应,不能仅删除查询断言。 + +**候选二:GetRawBatch。** [function_node.h](../../include/nodes/function_node.h) 第 326 行声明此虚接口,第 406 行有唯一实现,全仓相关代码没有调用。可随其他修改删除裸 `const void*` 暴露,保留实际使用的 `HasBatch`、typed binding 和来源读取。收益只有数行及一个虚方法,不应包装成大幅减重。 + +### B4. Session 收缩便利查询与资源变更入口 + +**事实。** [session_context.h](../../include/core/session_context.h) 中以下接口当前没有调用:第 195 行 `GetAllRegistrations()`,226–230 行 `GetChipType()`、`GetPlatformMaxBatch()`、`GetDepthNum()`。第 174 行 `UpdateModelRevision()`、184 行 `GetAllModels()`、233/250 行 `SetResource/GetResource` 的调用集中在测试。 + +**建议。** 先删除确认零调用的便利接口。对仅测试使用的可变入口单独决定:模型 revision 是否允许运行中变更、通用资源是否真的需要覆盖写。若当前产品不支持这些操作,可把必要夹具能力移到测试支持代码,缩小生产 Session 操作面。 + +**保留与限制。** `GetOrCreateResource()` 和 `GetModelRevision()` 有真实消费者:[text_embedding_node.cpp](../../src/common_nodes/text_embedding_node.cpp) 第 61、66 行用它们做缓存,不能删除资源机制或 single-flight。`RegisterModel()` 虽主要服务测试/benchmark,却只是短小便利包装,保留它可能比迁移大量夹具更划算。这里不建议仅因 getter 无人用,就删除平台结构字段或 RuntimeOptions 中的同名参数。 + +## 6. P3:需要范围决策,不能直接当作死代码 + +### C1. 是否取消 kPrivateExtensionCompatible + +**事实。** [pipeline_validator.h](../../include/core/pipeline_validator.h) 第 15 行公开严格/私有扩展双策略。[pipeline_validator.cpp](../../src/core/pipeline_validator.cpp) 第 1042、1069、1078、1190、1272 行据此区别处理未知 biz 或缺 Definition。历史 [RFC-0008](../rfcs/0008-architecture-contract-consolidation.md) 第 89、250 行明确为私有 C++ 扩展和内部调用保留该策略。 + +当前显式选择宽松模式的仓内调用都在测试及 `tests/support/node_harness.h`;正常产品入口使用严格模式。Node 注册已经拒绝空 Definition,Model/Backend 也把 Definition 与 creator 一起存储,许多“只有 creator、没有 Definition”的兼容状态已不符合正常注册入口。 + +**仍有真实用途。** [test_validated_pipeline_plan.cpp](../../tests/unit/core/test_validated_pipeline_plan.cpp) 第 367 行起的 `StrictVsCompatiblePolicy` 证明宽松模式允许未注册测试 biz;[NodeHarness](../../tests/support/node_harness.h) 第 216、287 行构造 `harness_biz` 并使用宽松策略。Validator 第 1412 行附近也因 biz 不存在而不执行同样的 ingress 闭合检查。因此不是整条路径不可达。 + +**条件建议。** 先清理当前正规注册不可能产生的缺 Definition 兼容分支。若确定所有可交付 Pipeline 都必须有完整 biz,再把测试迁移到明确的最小测试契约,补齐 ingress/egress 和节点 biz 约束,随后取消宽泛策略。不能简单全局改成 strict。若保留无 Integration biz 的嵌入式产品场景,应明确其范围,不用“兼容”名称无限容纳不完整状态。 + +### C2. 收敛 Node 初始化与脚手架作者路径 + +**有计划/无计划初始化。** [model_bound_node.h](../../include/nodes/model_bound_node.h) 第 54–81 行,以及 [function_node.h](../../include/nodes/function_node.h) 第 360、642、713、1129、1321 行附近,为 `init_ctx.plan == nullptr` 保留逻辑端口直连、重新规范化配置或模型字段解析。这是可见的第二条初始化路径。 + +独立 Node 单测和作者直接初始化依赖它,因此不能仅按重复解析认定为历史垃圾。若统一要求所有 Node 带计划,应由 NodeHarness/作者工具构造最小计划后删除无计划分支;若直接初始化是长期能力,则仅合并两路共用机制。不要迫使单节点测试加载完整 Pipeline、Adapter 和模型系统。 + +**脚手架模式重叠。** [traceable_unary_inference_node.h](../../include/nodes/traceable_unary_inference_node.h) 共 91 行,当前没有内置 `src/` 派生类,直接实例化在 [test_node_base_contracts.cpp](../../tests/unit/core/test_node_base_contracts.cpp) 第 406、420 行。但它不是只有测试使用:[scaffold_custom_node.py](../../scripts/scaffold_custom_node.py) 第 186、269、319 行仍为 `--kind unary_inference` 生成继承代码和 include;第 1188 行作者模式默认仍为 advanced。basic 模式第 138–155 行也没有覆盖全部模型能力。 + +建议先比较并收敛脚手架 `model` / `unary_inference` 作者路径,更新生成结果及作者指南,再评估移除中间模板。保留空批次、模型错误、输出数量和 provenance 检查。**不能以 src 无派生类为依据直接删掉这个头,也不能仅删专属测试而让生成器继续输出失效代码。** + +### C3. 测试装配只维护一份声明 + +**事实。** 根 [CMakeLists.txt](../../CMakeLists.txt) 第 260 行提供 `LLM_EDGEFLOW_SHARDED_TEST_RUNNERS`;[Tests.cmake](../../cmake_ext/Tests.cmake) 第 17–22 行切到另一套 370 行的 [IndividualTests.cmake](../../cmake_ext/IndividualTests.cmake)。两者重复维护 ABI/静态门禁、可执行目标、链接、标签和工作目录。[TestInventory.cmake](../../cmake_ext/TestInventory.cmake) 共享源清单,但没有消除注册规则重复。canonical gate 固定使用 sharded。 + +**反证。** [测试指南](../../tests/README.md) 第 22 行明确 individual 用于逐文件独立进程诊断,它不是单纯旧格式兼容。如果诊断需求存在,不能只为删 370 行去掉能力。 + +**建议。** 优先让一份测试声明生成两种装配;或者取消全量 individual 开关,仅保留确实依赖独立进程的注册冲突、生命周期等 fixture。验收比较测试清单、标签、环境及隔离要求,不能以删测试获得更快门禁。收益是构建矩阵和维护成本,不直接减少生产 SDK。 + +### C4. 工具和治理的局部收敛 + +1. **删除 Studio 指纹别名及猜测式测试。** [server.py](../../tools/pipeline_studio/server.py) 第 325–326 行 `tool_fingerprint()` 只转调 `get_tool_fingerprint()`;[test_pipeline_studio.py](../../tests/tooling/test_pipeline_studio.py) 第 205–218 行却用 `hasattr`、callable/property 探测、模块级兜底、吞异常和常量兜底寻找它。测试直接调用当前方法即可,让接口失配明确失败。 +2. **include/vendor 归属规则单点维护。** [check_layer_isolation.sh](../../scripts/check_layer_isolation.sh) 第 204、217 行的跨层 include 正则,与 [check_layer_dependencies.py](../../scripts/check_layer_dependencies.py) 第 56–79 行部分重叠;shell 第 463 行最后又调用 Python。把相同规则集中到 Python,shell 保留协调职责。保留第 465–467 行真实 CMake 头视图编译探针,它和静态扫描覆盖不同失效方式。 +3. **压缩历史禁用项检查。** [check_architecture_docs.sh](../../scripts/check_architecture_docs.sh) 第 68–85 行,以及 LayerGuard 第 292–303、454–459 行,为已删除名称维护专门规则。可用小型禁止项表代替散落逻辑和重复输出;必要拒绝测试继续保留。这些代码是在拒绝旧架构,不能作为框架仍支持旧架构的证据。 + +## 7. 不建议因“未生产”而直接删除的机制 + +| 机制 | 保留依据 | +| --- | --- | +| C ABI 与 Operator 两个入口 | [c_api_adapter.cpp](../../src/adapter/c_api_adapter.cpp) 第 95 行和 [operator_adapter.cpp](../../src/adapter/operator/operator_adapter.cpp) 第 372 行共用 SharedAlgorithmRuntime::ExecuteBatch;Operator 另承担命名 I/O、宿主载体和输出池。没有发现两份独立推理运行时 | +| Adapter 与 Operator Bridge | 分别承担业务解包/组装和宿主载体/所有权适配。相同 C struct 不等于相同 JSON 契约;Translate 的完整请求/响应转换仍须在 Adapter,不能转移到 Demo | +| C11 公开头、六个 Alg_* 异常屏障、输出池与容量检查 | 它们保证语言边界和内存生命周期,不属于历史兼容语法。破坏旧名称兼容不意味着可以削弱这些机制 | +| 单一 Validator、ValidatedPipelinePlan 和显式 DAG | [pipeline_config.cpp](../../src/core/pipeline_config.cpp) 第 510 行要求显式字段;[pipeline.cpp](../../src/core/pipeline.cpp) 第 475 行调用 ValidateAndPlan。未发现旧顺序 Pipeline 自动转换或运行时再次排序 | +| Studio 轻量编辑检查 | [server.py](../../tools/pipeline_studio/server.py) 第 328–340 行调用 native validate --stdin;前端端口/成环提示用于交互,不能声称它复制了完整 Validator | +| Model/Backend 分离、中性协议、批策略和来源校验 | 多种 Model/Backend 已实际注册;FixedBatchExecutor 承担 padding 去除和 (req_id, sub_id) 保持。多个 Backend 的相似调用壳不等于算法重复 | +| 配置快照和会话 single-flight | 有真实节点消费者和并发/生命周期覆盖。RFC-0052/0054 仍为 In Implementation,主要剩真实开发者体验验收,不能据状态误报工程迁移未完成 | +| mock/test Backend、fixtures 和历史 RFC | mock 支持目标单独链接,不能把其源码行数算进生产 SDK 膨胀。历史 RFC 保存决策和验证基线,适合归档及减少重复现行指南,而非整体删除 | + +GeneratedTextEmbedding 等实验能力有配置/协议及测试依据,当前证据不足以认定冗余。如果想通过砍模态、Backend、Studio 或 Demo 大幅缩小产品范围,应先明确首个生产场景和交付集合并测量产物;这属于产品裁剪,不应混入兼容清理当作无损删除。 + +同样,不建议因 `function_node.h` 约 1,623 行、`pipeline_validator.cpp` 约 1,977 行就删除函数式作者接口或修复建议。可讨论拆分文件和减少模板实例化,但仅拆文件不会减少实现量;Explain 当前用同一个 Validator 检查候选修复,属于复用。 + +## 8. 建议实施顺序与退出条件 + +### 第一批:兼容入口收口 + +范围为 A1–A6,可搭配 B3 的 GetRawBatch 和 C4 的指纹别名清理。先记录破坏性契约,再原子更新仓内实现、配置、生成器、Demo、测试和现行文档。旧头等微收益项可随主要改动一起完成,不必各自开启大型重构。 + +退出条件:旧格式不再有成功执行路径;当前 Catalog/配置/模板/Bridge 只用规范表示;所有官方方案通过对应构建检查和 smoke;旧格式失败明确可诊断。必要的旧输入拒绝案例留在现有套件,不继续维护旧格式成功测试。 + +### 第二批:内部表示与扩展面收敛 + +按 B1 → B3/B4 → B2 分开实施。诊断码统一和闲置接口删除可较局部验证;Node Registry 所有权合并应单独 RFC、单独审查,并做异常/冲突/可重入验证。不要同时改诊断、锁和注册生命周期,避免出问题后难以定位。 + +退出条件:每项变更都能展示净减少的字段、分支或必实现接口;输出诊断与失败行为不退化;没有用新的兼容适配层抵消删除收益。 + +### 第三批:依据真实扩展和诊断需求裁剪 + +对 C1–C3 明确选择:是否支持无 biz 嵌入式 Pipeline、是否支持无计划 Node 初始化、脚手架保留哪些作者模式、是否需要完整 individual 测试模式。默认倾向一个可交付契约,但测试便利不应通过生产宽松开关无限扩散,也不应因收敛而让单测加载无关子系统。 + +涉及公共契约、兼容策略、跨层所有权和生命周期的实施遵循 [CONTRIBUTING.md](../../CONTRIBUTING.md),用新 RFC 明确取代旧决策的范围。[RFC-0024](../rfcs/0024-pre-release-contract-cleanup.md) 已做过一轮兼容删除,而 [RFC-0046](../rfcs/0046-naming-and-header-boundaries.md) 又有意保留旧头;不能假设所有已完成 RFC 永远要求保留或永远禁止兼容。 + +每批完成后运行一次 canonical gate `./scripts/run_all_tests.sh`;非默认 Backend 或并发语义被触及时另做所需专项验证,不重复运行无关完整门禁。 + +## 9. 如何衡量是否真的瘦身 + +逐批记录公开入口数、配置可接受形式数、兼容条件分支数、Node/Model 必实现接口数、重复状态存储数、测试装配声明数和净代码行数。性能结论另用相同构建选项测量库大小、构建耗时、初始化耗时及代表性请求耗时。 + +不要预先承诺删除若干千行或体积降低若干百分比。本轮最明确的可量化对象包括七个旧头、一个旧别名、三模式模板选择、25 份旧 .conf 的迁移、两种输出回调位置、Studio 旧模型字段分支。它们收益不同,不能简单相加成运行时收益;91 行的 TraceableUnaryInferenceNode 则必须先迁移脚手架才可能删除。 + +## 10. 本次审查验证记录 + +已完成基线与工作区状态确认、Catalog 查询、符号及配置使用检索、整文件内容比对、生成器/相关测试/RFC 静态核对,并对易误删候选进行了独立交叉复核。报告保存于独立本地文档分支,仅新增本报告并更新归档索引。 + +验证结果: + +- `LLM_EDGEFLOW_JOBS=4 ./scripts/run_all_tests.sh`:退出码 0,97/97 个 CTest 测试通过,失败 0;门禁耗时 38 秒,CTest 耗时 26.58 秒。包含默认配置构建、格式/静态检查和全部注册测试,没有另跑重复完整门禁。 +- `git diff --check`:通过。 +- 报告独立检查:71 个相对文件链接全部存在,无行尾空白,文件末尾有换行;补录本验证记录后再次检查链接及空白。 +- 完整命令日志保存在本机 `/tmp/framework-slimming-gate-2026-09-14.log`,未作为项目源码提交。 + +以上结果验证的是当前基线及本次文档交付,不证明尚未实施的删除建议已经安全,也不构成真实模型效果、非默认 Backend 或目标设备验收。 diff --git a/doc/archive/README.md b/doc/archive/README.md index cb41b6bb..8663503f 100644 --- a/doc/archive/README.md +++ b/doc/archive/README.md @@ -6,6 +6,8 @@ | 日期 | 历史材料 | 后续记录与阅读范围 | | --- | --- | --- | +| 2026-09-14 | [第一批框架瘦身实施复核](FRAMEWORK_SLIMMING_IMPLEMENTATION_REVIEW_2026-09-14.md) | 复核第一批暂存实现;97/97 门禁通过,记录输出 key 回归及元数据、指南、测试与 RFC 收尾项 | +| 2026-09-14 | [投产前框架冗余、兼容负担与瘦身审查](FRAMEWORK_SLIMMING_REVIEW_2026-09-14.md) | 基于 `b08c5bd` 的只读审查;区分兼容清理、结构收敛与产品裁剪,建议尚未实施 | | 2026-09-02 | [架构与实现审计](ARCHITECTURE_AUDIT_2026-09-02.md) | [RFC-0028](../rfcs/0028-preproduction-runtime-and-abi-hardening.md) 记录阻断项及运行时风险修复;[RFC-0029](../rfcs/0029-external-readiness-and-intranet-sdk-migration.md) 跟踪剩余整改和内网迁移 | | 2026-09-05 | [系统性架构、业务适配与易用性审计](SYSTEMATIC_ARCHITECTURE_REVIEW_2026-09-05.md) | [RFC-0037](../rfcs/0037-audit-remediation.md) 与下列五阶段交付记录覆盖其中选定整改项;不表示全部审计建议均已完成 | | 2026-09-06 | [五阶段审计整改交付记录](AUDIT_REMEDIATION_REPORT_2026-09-06.md) | 记录当轮实现、提交与验证范围;日常操作见[业务接入](../dev_guide/business_onboarding.md)和[效果验收](../VERIFIABLE_SELECTION.md) | diff --git a/doc/dev_guide/business_onboarding.md b/doc/dev_guide/business_onboarding.md index 1c22c43b..8ac32534 100644 --- a/doc/dev_guide/business_onboarding.md +++ b/doc/dev_guide/business_onboarding.md @@ -52,7 +52,7 @@ JSON 请求是不同的输入约定。已有 Nodes 能完成算法,也不代 ./build/alg_pipeline_tool describe-node TextRuleMatchNode ./build/alg_pipeline_tool validate configs/pipeline_keyword_match_rules.json ./build/alg_pipeline_tool plan configs/pipeline_keyword_match_rules.json -./build/alg_demo --biz keyword_match --config configs/pipeline_keyword_match_rules.conf --dataset tests/fixtures/effects/keyword_inputs.txt --no-default-control --output-dir results/business-onboarding +./build/alg_demo --biz keyword_match --config configs/pipeline_keyword_match_rules.conf --dataset tests/fixtures/effects/keyword_inputs.txt --output-dir results/business-onboarding ``` 查看 `results/business-onboarding/keyword_match/results.jsonl`:请求编号为 20001–20004, @@ -125,9 +125,9 @@ JSON 请求是不同的输入约定。已有 Nodes 能完成算法,也不代 已注册的输入输出后缀;多输入时参考 [ocr_doc_qa_operator_bridge.cpp](../../src/adapter/biz/ocr_doc_qa_operator_bridge.cpp)。 3. 实现 `convert_sample_input`:从输入槽读取宿主结构,使用 - `ProcessLocalShadowStorage` 复制字符串、保存临时输入结构。实现 - `convert_sample_output`:把业务 Result 复制进已租用的输出池,容量只取自 - `ResolvedOutputPoolSpec`,不在 bridge 内重新读取原始 JSON 或设置默认容量。 + `ProcessLocalShadowStorage` 复制字符串、保存临时输入结构。在输出槽位提供 + `convert_output`(单输出 helper 会绑定到 `output_slots.front().convert_output`): + 把业务 Result 复制进已租用的输出池,容量只取自 `ResolvedOutputPoolSpec`,不在 bridge 内重新读取原始 JSON 或设置默认容量。 4. 包含 `adapter/operator_biz_bridge.h`,使用无参注册函数调用 `RegisterOperatorBizBridge(desc)`;输出文本使用 `CopyToOperatorString`。 用 `REGISTER_OPERATOR_BIZ_BRIDGE` 登记注册函数,并将新增源码加入 @@ -178,8 +178,7 @@ JSON 请求是不同的输入约定。已有 Nodes 能完成算法,也不代 [公开 Operator 契约](../../include/edgeflow/operator/interface.h)。 Operator 的输出路径是 `Pipeline → 可变长业务 Result → 已租用输出池`。 -Result 与请求 Context 均不跨 Process 保存。`.conf` 的 `data.mem_que.type` 选择已注册 -输出类型,`capacities` 设置它声明的字段容量;多输出使用互斥的 `data.outputs`,按逻辑 +Result 与请求 Context 均不跨 Process 保存。`.conf` 的 `data.outputs` 按逻辑 槽位分别指定类型、`allocator`、`params` 和容量。字符串不受中间 C 输出数组大小限制; 超过输出池容量时返回 `-4`,尚未发布的输出租约全部回滚。 diff --git a/doc/dev_guide/first_control.md b/doc/dev_guide/first_control.md index c50cfa58..d463ba94 100644 --- a/doc/dev_guide/first_control.md +++ b/doc/dev_guide/first_control.md @@ -104,11 +104,13 @@ mkdir -p build/control_tutorial { "data": { "pipe_path": "build/control_tutorial/pipeline.json", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": {"match_result_json": 2047} + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": {"match_result_json": 2047} + } } } } @@ -128,7 +130,7 @@ mkdir -p build/control_tutorial ```bash ./build/alg_pipeline_tool validate build/control_tutorial/pipeline.json ./build/alg_pipeline_tool plan build/control_tutorial/pipeline.json -./build/alg_demo --biz keyword_match --config build/control_tutorial/pipeline.conf --dataset build/control_tutorial/input.txt --no-default-control --control-cmd 1001 --control-file build/control_tutorial/control.json --output-dir build/control_tutorial/updated +./build/alg_demo --biz keyword_match --config build/control_tutorial/pipeline.conf --dataset build/control_tutorial/input.txt --control-cmd 1001 --control-file build/control_tutorial/control.json --output-dir build/control_tutorial/updated ``` 查看 `build/control_tutorial/updated/keyword_match/results.jsonl`:应有 `status: 0`、 @@ -154,8 +156,7 @@ C ABI 继续直接使用 `CompanyAlgParamControl{cmd_id, json}`,不需要新 Demo 的 `--control-cmd` 也可配置为 Profile 的 `control_cmd`,CLI 显式值优先;指定命令 必须提供 `control_file`。省略命令时保留该 Demo 的默认命令。Demo 默认不发送内置演示 -更新;显式 `--example-control` 才启用,且显式文件优先。`--no-default-control` 保留为 -兼容选项,不影响显式文件,与 `--example-control` 同时使用会报错。 +更新;显式 `--example-control` 才启用,且显式文件优先。 同一 handle 的 C ABI / Operator 调用串行;多个线程提交不保证顺序。内部直接调用 Pipeline/Node 的 Control 时,由调用者序列化更新。裸 payload 广播到所有声明支持该 diff --git a/doc/dev_guide/operator_output_allocation.md b/doc/dev_guide/operator_output_allocation.md index 4e302e18..9cfa500c 100644 --- a/doc/dev_guide/operator_output_allocation.md +++ b/doc/dev_guide/operator_output_allocation.md @@ -36,7 +36,7 @@ JSON 读取器将选中值通过 `dump()` 转为拥有自身存储的 `std::stri | 字段 | 用途 | | --- | --- | | bridge `logical_name` | 业务中的输出槽位,也是 `data.outputs` 的配置键 | -| bridge `key_suffix` | 外部 map key 最后一个点号后的部分;省略时沿用 `type_suffix` | +| bridge `key_suffix` | 外部 map key 最后一个点号后的部分;描述符必须显式填写,与 `logical_name` 及 `type_suffix` 相互独立;单槽 Helper 默认填充为规范 `type_suffix`,不再支持运行时省略或隐式回退 | | `type` | 已注册的外层 ValueType,必须匹配槽位的 `type_suffix` | | `allocator` | 为该外层类型注册的分配方案标识;省略时使用类型的默认实现 | | `params` | 由方案解释、校验并补齐的单份布局参数,例如嵌套枚举与数组容量 | @@ -75,8 +75,7 @@ JSON 读取器将选中值通过 `dump()` 转为拥有自身存储的 `std::stri 每个声明的输出槽位都需要配置,包括 `required=false` 的可选输出;可选是指 Process 可以省略该输出 map 项。逻辑名和有效 map 后缀分别唯一;不同槽位可以复用相同类型 -和方案,各自使用独立容量和输出池。原单输出 `data.mem_que` 继续可用,也接受 -`allocator` / `params`,但不能与 `data.outputs` 同时出现。 +和方案,各自使用独立容量和输出池。所有输出均统一在以逻辑槽位为键的 `data.outputs` 中配置。 ## 实现与注册 @@ -130,8 +129,8 @@ binding.normalize_parameters = ## 转换与有效期 -在 `OperatorBizSlot::convert_output` 注册该槽位的业务结果转换函数。多输出 bridge -每个槽位都要提供转换;已有单输出仍可使用 `convert_sample_output`。 +在 `OperatorBizSlot::convert_output` 注册该槽位的业务结果转换函数。所有输出槽位 +均通过槽位级 `convert_output` 提供转换,旧顶层 `convert_sample_output` 已被移除。 回调接收内部结果、已分配的外层结构和对应的 `ResolvedOutputPoolSpec`,按同一份 `allocator` 和类型化参数填充载荷。转换必须保持指针与已分配布局一致,不重新读取 部署文件、不另设默认容量,也不把请求局部指针塞进输出结构。 diff --git a/doc/dev_guide/recipe_prompt_config.md b/doc/dev_guide/recipe_prompt_config.md index 5d8f186d..0dab8f41 100644 --- a/doc/dev_guide/recipe_prompt_config.md +++ b/doc/dev_guide/recipe_prompt_config.md @@ -17,8 +17,8 @@ python3 scripts/dev_recipe.py prepare prompt-config \ ``` 生成三份文件:`pipeline_entity_prompt_task.json`、同名 `.conf` 和 `_effects.json`。 -现有文件不会被覆盖。conf 保留源 Profile 的整个 `data.mem_que`,包括 allocator、params、 -容量与元数据字段,并由原生 Resolver 检查;`data.outputs` 在写入或构建前被拒绝。 +现有文件不会被覆盖。conf 保留源 Profile 的整个 `data.outputs`,包括 allocator、params、 +容量与元数据字段,并由原生 Resolver 检查;多输出在写入或构建前被拒绝。 ## 修改与验收 diff --git a/doc/dev_guide/recipe_text_llm_node.md b/doc/dev_guide/recipe_text_llm_node.md index 26215882..5a5cfac5 100644 --- a/doc/dev_guide/recipe_text_llm_node.md +++ b/doc/dev_guide/recipe_text_llm_node.md @@ -57,7 +57,7 @@ verify 依次完成: 编译通过,不代表所选 CLI 或 Demo 已更新;verify 会明确构建这些独立目标。 其他 Profile 的 `--effects`、`--model-root`、`--manifest` 使用方式见 -[提示词任务](recipe_prompt_config.md)。两条 recipe 只支持 `data.mem_que` 单输出;多输出部署 +[提示词任务](recipe_prompt_config.md)。两条 recipe 只支持单输出 `data.outputs`;多输出部署 继续使用原生 Operator 流程。 ## 文件冲突 diff --git a/doc/dev_guide/source_layout.md b/doc/dev_guide/source_layout.md index 22d7cb52..d2cf289a 100644 --- a/doc/dev_guide/source_layout.md +++ b/doc/dev_guide/source_layout.md @@ -90,24 +90,24 @@ Catalog JSON 为兼容现有消费者,继续在两种声明中输出 `key`, `engine/inference_definition.h` 维护 Model/Backend 元数据;张量与 Host 内存辅助接口 在 `engine/tensor.h`。`node_registry.h` 的主要类型是 `NodeRegistry`。 -## 兼容迁移 +## 公共头与统一入口 -新代码使用以下头路径;旧公共头继续转发到同一声明,新旧头可以同时包含。 +仓内代码统一使用 `edgeflow/` 前缀入口;历史转发头与别名已清理: -| 旧路径 | 当前路径 | +| 规范入口 | 说明 | | --- | --- | -| `company_alg_interface.h` | `edgeflow/c_api.h` | -| `company_alg_cpp.hpp` | `edgeflow/c_api.hpp` | -| `company_alg_export.h` | `edgeflow/export.h` | -| `company_alg_log.h` | `edgeflow/log.h` | -| `company_alg_version.h` | `edgeflow/version.h`(由 CMake 生成) | -| `operator/operator_interface.h` | `edgeflow/operator/interface.h` | -| `operator/company_operator_types.h` | `edgeflow/operator/types.h` | +| `edgeflow/c_api.h` | 纯 C ABI 导出头 | +| `edgeflow/c_api.hpp` | C++ 异常屏障与辅助包装 | +| `edgeflow/export.h` | 符号可见性宏 | +| `edgeflow/log.h` | 统一日志入口 | +| `edgeflow/version.h` | 版本头(由 CMake 生成) | +| `edgeflow/operator/interface.h` | Operator 纯 C 接口及函数表 | +| `edgeflow/operator/types.h` | Operator 平台交互类型门面(转发至 platform_mock) | `Alg_*`、`Company*`、公共宏、C/C++ 公开函数签名、结构布局及 `libcompany_alg_sdk` 名称保持原样;这些名称属于既有调用契约。内部扩展应更新 `BizName()`、`pipelines`、 -`abi_version` 等旧成员,使用上表中的明确命名。`NodeFactory` 保留源码别名,新增代码 -使用 `NodeRegistry`。业务 bridge 转为无参注册函数,并使用上述扩展入口。 +`abi_version` 等旧成员。原 `NodeFactory` 兼容别名已移除,代码统一使用 `NodeRegistry`。 +业务 bridge 转为无参注册函数,并使用上述扩展入口。 `edgeflow/c_api.h` 的参数类型来自 `platform_mock/alg_types.h`,错误码来自 `platform_mock/error_codes.h`;`edgeflow/operator/types.h` 转发到 diff --git a/doc/developer_guide.md b/doc/developer_guide.md index 96440371..6e9fa533 100644 --- a/doc/developer_guide.md +++ b/doc/developer_guide.md @@ -69,9 +69,9 @@ SOVERSION/C ABI major 为 5。 仓库内 Node、Registry、Model、Backend 和第三方运行时是隐藏实现,不得被外部扩展直接链接。 Operator v4 的 Create 和配置预检都使用部署根 `model_path` 加相对 `cfg_file_name`。每份 `.conf` 的根对象只能包含 `data`,`data` 只接受 -`pipe_path`、`model_paths`、`mem_que` 和 `outputs`;单模型覆盖也必须使用以 `model_id` -为键的 `model_paths` 映射。单输出 `data.mem_que` 与按逻辑槽位配置的 `data.outputs` -互斥。Resolver 选择注册的输出类型与 `allocator`;独立配置读取组件通过固定枚举 +`pipe_path`、`model_paths` 和 `outputs`;单模型覆盖也必须使用以 `model_id` +为键的 `model_paths` 映射。所有输出统一在按逻辑槽位配置的 `data.outputs` 中定义; +旧 `data.mem_que` 已不再支持。Resolver 选择注册的输出类型与 `allocator`;独立配置读取组件通过固定枚举 选取配置项并返回字符串。方案用 `MakeOutputParameterParser` 将参数文本解析为 普通 C++ 结构,框架归一化 `meta_num`、metadata type 和字段容量;业务桥接使用 该规范化结果,不重复解析原始部署 JSON 或 diff --git a/doc/rfcs/0056-batch-1-framework-slimming.md b/doc/rfcs/0056-batch-1-framework-slimming.md new file mode 100644 index 00000000..9d76ef0d --- /dev/null +++ b/doc/rfcs/0056-batch-1-framework-slimming.md @@ -0,0 +1,83 @@ +# RFC 0056: 投产前框架兼容入口与配置收口(第一批瘦身落地) + +- **RFC 编号**:0056-batch-1-framework-slimming +- **创建日期**:2026-09-14 +- **文档状态**:Completed +- **关联分支**:`docs/framework-slimming-review-2026-09-14` +- **目标版本**:投产前基线 +- **负责人 / 作者**:LLM-EdgeFlow Team +- **关联决策**: + - 取代 [RFC-0046](0046-naming-and-header-boundaries.md) 中保留旧公共头转发(`company_alg_*.h`、`operator/*.h`)的决策; + - 取代 [RFC-0049](0049-operator-output-allocation-strategies.md) 中关于根级 `data.mem_que` 兼容及输出 key/回调隐式回退的决策; + - 补充 [RFC-0042](0042-studio-and-contract-boundaries.md)(模板语法统一为 `{{name}}`,单括号 `{...}` 保留为字面量); + - 补充 [RFC-0045](0045-solution-developer-workflow.md)(清理无效果 CLI 兼容选项 `--no-default-control`); + - 补充 [RFC-0053](0053-function-oriented-adapter-authoring.md)(Bridge 描述符回调收敛为槽位级 `convert_output`,`AdapterName` 严格 1:1 匹配,单槽 Helper 填充规范 `key_suffix`); + - 保留 [RFC-0029](0029-external-readiness-and-intranet-sdk-migration.md) 的内网迁移与隔离边界。 + +## 1. 问题与范围 + +在投产前代码审查(参见 [FRAMEWORK_SLIMMING_REVIEW_2026-09-14.md](../archive/FRAMEWORK_SLIMMING_REVIEW_2026-09-14.md))中发现,框架经过多轮演进后积累了一批过渡期兼容入口与冗余分支: +1. **旧公共头与兼容别名**:RFC-0046 保留了 7 个历史顶层转发头以及 `NodeRegistry` 内部的 `NodeFactory` 别名,调用方入口未彻底收口。 +2. **模板解析与语法分流**:`PromptGuidedLlmNode` 维护私有正则解析器及 `template_syntax` 配置(`auto`/`standard`/`legacy`),占位符支持单双括号混用,与标准 `TextTemplateNode` 存在语义分流。 +3. **Operator 输出配置双路径**:RFC-0049 引入槽位级 `data.outputs` 后,根级 `data.mem_que` 历史配置仍被读取和转换,加重维护负担。 +4. **Bridge 描述符多入口与隐式回退**:顶层 `convert_sample_output` 与槽位级 `convert_output` 并存;`OperatorBizBridgeRegistry` 允许 `adapter_name` 回退匹配 Pipeline `biz_name`;单槽 Helper 的 `key_suffix` 依赖运行时空值回退。 +5. **无效果 CLI 参数与废弃 Catalog 字段**:Demo 保留无效果的 `--no-default-control` 参数;Pipeline Studio 仍处理废弃的 `model_config_field` / `model_capability`。 +6. **未调用的闲置接口**:`function_node.h` 中的纯虚 `GetRawBatch`,以及 `SessionContext` 中未被消费的 4 个零调用便利查询。 + +**范围边界**: +本 RFC 作为“第一批瘦身落地”,仅覆盖上述 A1–A6、B3(部分)、B4(部分)、C4.1 明确确认的冗余项。 +对于审查报告中列出的 B1(Core 诊断统一)、B2(Node 注册状态统一)、剩余 B3/B4、C1–C3(宽松 Validator、双测试装配等)以及 C4 静态 include 集中化规则,保持现状或留待后续独立评估,不纳入本批次,避免在同一改动中扩大架构风险。 + +## 2. 决策与权衡 + +### 2.1 接入适配层(Integration) +- **彻底删除 7 个历史转发头**:删除 `company_alg_interface.h`、`company_alg_cpp.hpp`、`company_alg_export.h`、`company_alg_log.h`、`company_alg_version.h`、`operator/operator_interface.h`、`operator/company_operator_types.h`。SDK 公共入口唯一收敛至 `include/edgeflow/` 下的规范 C11 头与 Operator 头。 +- **Operator 输出配置单路径收口**:下线根级 `data.mem_que`,统一使用槽位级 `data.outputs.` 配置;彻底移除遗留字段过渡期脚手架,含 `data.mem_que` 的配置直接按未知字段统一 Fail-Closed 拒绝。 +- **Bridge 描述符回调与标识收敛**: + - 输出转换回调统一收敛至每个输出槽位独立的 `convert_output`,移除结构体顶层的 `convert_sample_output` 回调及其执行期 fallback。 + - `OperatorBizBridgeRegistry::GlobalInit` 严格要求描述符的 `adapter_name` 与 `IBizAdapter::AdapterName()` 完全相等,彻底移除遍历 `biz_definitions` 回退匹配 `biz_name` 的分支。 + - 单槽 Helper `MakeSingleSlotBizBridge` 与 `MakeTypedSingleSlotBizBridge` 在初始化时显式填充 `key_suffix` 为规范类型后缀(例如 `entity_out`),消除 `KeySuffix()` 的空值运行时回退;同时保持逻辑槽名 `logical_name`(如 `custom_out`)与外部 key 后缀解耦,并支持通过可选参数 `output_key_suffix` 或字段显式自定义覆盖。 + +### 2.2 流程编排层(Orchestration) +- 删除 `NodeRegistry` 中的 `using NodeFactory = NodeRegistry;` 历史别名。 +- 清理 `SessionContext` 中未被调用的便利查询接口:`GetAllRegistrations`、`GetChipType`、`GetPlatformMaxBatch`、`GetDepthNum`,保留与上下文生命周期、资源与配置紧密相关的核心接口。 + +### 2.3 能力节点层(Capability Nodes) +- **模板语法统一**:删除 `PromptGuidedLlmNode` 中的私有模板解析器和 `template_syntax` 配置选项,统一调用 `include/nodes/text_template.h` 中的共享 `ParseTextTemplate`。 +- **语法规范**:唯一规范为 `{{name}}` 占位符;单括号 `{...}` 保持为字面量(兼容标准 JSON),消除歧义分支。同步修正 `TextTemplateNode` 的 Definition 描述与头文件注释。 +- **闲置接口清理**:移除 `include/nodes/function_node.h` 中未使用的纯虚接口 `GetRawBatch`。 + +### 2.4 模型执行层与工具(Tooling / Demo) +- **Demo CLI 参数收敛**:移除 `--no-default-control` 参数,未知参数保持 Fail-Closed;Demo 默认不发送示例 Control,仅在显式指定 `--example-control` 时触发。 +- **Pipeline Studio 收口**:移除针对 `model_config_field` / `model_capability` 的历史兼容逻辑,统一通过 `model_dependencies` 表达模型绑定。 +- **清理测试与指纹别名**:删除已废弃的指纹测试别名与基于猜测的冗余用例。 + +## 3. 兼容与迁移 + +1. **头文件迁移**: + - 依赖旧公共头的代码必须迁移至 `#include "edgeflow/c_api.h"` 或 `#include "edgeflow/operator/interface.h"`。 +2. **配置文件迁移**: + - 部署 `.conf` 中所有的 `data.mem_que` 必须迁移为 `data.outputs.`。仓内所有 25 份正式部署配置文件及 fixtures 均已一次性完成迁移。 +3. **模板配置迁移**: + - Prompt 模板中的变量必须使用 `{{name}}` 格式,单括号 `{name}` 将作为字面量文本处理,不被替换。 +4. **Bridge 扩展迁移**: + - 手写注册 `OperatorBizBridgeDescriptor` 时,必须为每个输出槽位设置 `convert_output` 与非空的 `key_suffix`,`adapter_name` 必须精确匹配对应 `IBizAdapter::AdapterName()`。使用 `MakeTypedSingleSlotBizBridge` 时自动填充规范 `type_suffix` 与 `key_suffix`。 + +## 4. 验证与完成条件 + +1. **单元与集成测试**: + - `test_c11_abi_compliance` 验证 C11 ABI 仅暴露规范公共头; + - `test_operator_biz_bridge_registry` 验证单槽 typed helper 逻辑槽名、规范类型与外部 key 的正确绑定(含可选参数直接指定与缺省规范回填)、全业务负向严格 AdapterName 检查(拒绝各业务真实 declared biz_name)以及非空 key_suffix 约束; + - `test_text_template_node` 验证单双括号模板语义及 Definition 描述准确性; + - `test_demo_runner` 验证 CLI 参数拒绝 `--no-default-control`; + - `test_pipeline_studio.py` 验证配置导出与 Studio 前后端仅依赖 `model_dependencies`。 +2. **全局门禁**: + - 执行 `./scripts/run_all_tests.sh` 全量通过(包含 shell 检查、代码格式、LayerGuard 架构边界检查、全部 CTest 测试通过)。 +3. **二进制与 Catalog 检查**: + - `./build/alg_pipeline_tool catalog` 成功输出有效 JSON,验证节点与业务定义一致性。 + +## 5. 实施与最终结果 + +- 实施代码变更覆盖 88 个源码及配置文件,删除 7 个废弃头文件,全仓迁移至规范单路径。 +- 复核发现的 5 项收尾问题(typed helper 外部 key 回归、TextTemplate catalog 描述错误、输出分配指南与 skill 遗留说明、负向 AdapterName 鉴别性测试用例、RFC 决策记录缺失)已全部修复并补全验证。 +- 自动化门禁 `./scripts/run_all_tests.sh` 运行全部通过。 diff --git a/doc/rfcs/README.md b/doc/rfcs/README.md index 14368f1f..fea4e1a5 100644 --- a/doc/rfcs/README.md +++ b/doc/rfcs/README.md @@ -81,6 +81,7 @@ RFC-0054 是接续 RFC-0052 与已交付 RFC-0053、RFC-0055 的实施规格。` | **RFC-0050** | Operator 配置读取边界与普通参数结构 | `Completed` | `v10.x` | 接入适配层 / Tooling | [0050-operator-configuration-text-boundary.md](0050-operator-configuration-text-boundary.md) | | **RFC-0053** | 业务 Adapter 函数式作者接口与载体机制收敛 | `Completed` | 投产前 | 接入适配层 / Tooling / Docs | [0053-function-oriented-adapter-authoring.md](0053-function-oriented-adapter-authoring.md) | | **RFC-0055** | 批次关联、分组、选择回填与拆分公共工具 | `Completed` | 投产前 / Catalog v3 | 能力节点层 / Tooling / Docs | [0055-traceable-batch-operations.md](0055-traceable-batch-operations.md) | +| **RFC-0056** | 投产前框架兼容入口与配置收口(第一批瘦身落地) | `Completed` | 投产前 | 四层 / Build / Tooling / Docs | [0056-batch-1-framework-slimming.md](0056-batch-1-framework-slimming.md) | ## 专项验收与评审归档 diff --git a/include/adapter/operator_biz_bridge.h b/include/adapter/operator_biz_bridge.h index c0b2906d..21e45b02 100644 --- a/include/adapter/operator_biz_bridge.h +++ b/include/adapter/operator_biz_bridge.h @@ -58,13 +58,11 @@ struct OperatorBizSlot { std::string type_suffix; // 规范类型后缀 IoDirection direction = IoDirection::kInput; bool required = true; - // Output-only: empty preserves the legacy type suffix as the map key suffix. + // Output key suffix in the returned NamedIoBatch map. std::string key_suffix{}; ConvertSampleOutputFn convert_output = nullptr; - const std::string& KeySuffix() const { - return key_suffix.empty() ? type_suffix : key_suffix; - } + const std::string& KeySuffix() const { return key_suffix; } bool operator==(const OperatorBizSlot& other) const { return logical_name == other.logical_name && @@ -93,7 +91,6 @@ struct OperatorBizBridgeDescriptor { std::vector input_slots; std::vector output_slots; ConvertSampleInputFn convert_sample_input = nullptr; - ConvertSampleOutputFn convert_sample_output = nullptr; CreateShadowOutputDtoFn create_shadow_output_dto = nullptr; bool operator==(const OperatorBizBridgeDescriptor& other) const { @@ -104,7 +101,6 @@ struct OperatorBizBridgeDescriptor { input_slots == other.input_slots && output_slots == other.output_slots && convert_sample_input == other.convert_sample_input && - convert_sample_output == other.convert_sample_output && create_shadow_output_dto == other.create_shadow_output_dto; } }; @@ -112,22 +108,22 @@ struct OperatorBizBridgeDescriptor { // The built-in one-input/one-output pattern needs only its conversions and // registered type names; slot boilerplate and result allocation are shared. template -OperatorBizBridgeDescriptor MakeSingleSlotBizBridge(CompanyAlgBizType biz_type, - std::string adapter_name, - std::string input_type, - std::string identity, - std::string input_slot, - std::string output_slot) { +OperatorBizBridgeDescriptor MakeSingleSlotBizBridge( + CompanyAlgBizType biz_type, std::string adapter_name, + std::string input_type, std::string identity, std::string input_slot, + std::string output_slot, std::string output_key_suffix = "") { OperatorBizBridgeDescriptor desc; desc.biz_type = biz_type; desc.adapter_name = std::move(adapter_name); desc.internal_input_type_name = std::move(input_type); desc.internal_output_type_name = Result::kTypeName; desc.registration_identity = std::move(identity); + std::string actual_key_suffix = + output_key_suffix.empty() ? output_slot : std::move(output_key_suffix); desc.input_slots.push_back( - {input_slot, input_slot, IoDirection::kInput, true}); - desc.output_slots.push_back( - {output_slot, output_slot, IoDirection::kOutput, true}); + {input_slot, input_slot, IoDirection::kInput, true, "", nullptr}); + desc.output_slots.push_back({output_slot, output_slot, IoDirection::kOutput, + true, std::move(actual_key_suffix), nullptr}); desc.create_shadow_output_dto = [](ProcessLocalShadowStorage& storage) -> void* { return storage.AllocateShadowDto(); @@ -160,7 +156,8 @@ inline OperatorBizBridgeDescriptor MakeTypedSingleSlotBizBridge( CompanyAlgBizType biz_type, std::string adapter_name, std::string internal_input_type_name, std::string identity, std::string input_slot = OperatorSlotTraits::kTypeSuffix, - std::string output_slot = OperatorSlotTraits::kTypeSuffix) { + std::string output_slot = OperatorSlotTraits::kTypeSuffix, + std::string output_key_suffix = "") { auto desc = MakeSingleSlotBizBridge( biz_type, std::move(adapter_name), std::move(internal_input_type_name), std::move(identity), input_slot, output_slot); @@ -169,6 +166,9 @@ inline OperatorBizBridgeDescriptor MakeTypedSingleSlotBizBridge( OperatorSlotTraits::kTypeSuffix; desc.output_slots.front().type_suffix = OperatorSlotTraits::kTypeSuffix; + desc.output_slots.front().key_suffix = + output_key_suffix.empty() ? OperatorSlotTraits::kTypeSuffix + : std::move(output_key_suffix); desc.convert_sample_input = [](const std::unordered_map& slots, @@ -197,7 +197,7 @@ inline OperatorBizBridgeDescriptor MakeTypedSingleSlotBizBridge( return ret; }; - desc.convert_sample_output = + desc.output_slots.front().convert_output = [](const void* internal_dto, void* external_output_struct, const ResolvedOutputPoolSpec& spec, std::string* err) -> int { if (!internal_dto || !external_output_struct) { @@ -218,7 +218,8 @@ inline OperatorBizBridgeDescriptor MakeTypedSingleSlotBizBridge( CompanyAlgBizType biz_type, std::string adapter_name, std::string internal_input_type_name, std::string identity, std::string input_slot, std::string output_slot, - ConvertSampleInputFn convert_input, ConvertSampleOutputFn convert_output) { + ConvertSampleInputFn convert_input, ConvertSampleOutputFn convert_output, + std::string output_key_suffix = "") { auto desc = MakeSingleSlotBizBridge( biz_type, std::move(adapter_name), std::move(internal_input_type_name), std::move(identity), input_slot, output_slot); @@ -227,8 +228,11 @@ inline OperatorBizBridgeDescriptor MakeTypedSingleSlotBizBridge( OperatorSlotTraits::kTypeSuffix; desc.output_slots.front().type_suffix = OperatorSlotTraits::kTypeSuffix; + desc.output_slots.front().key_suffix = + output_key_suffix.empty() ? OperatorSlotTraits::kTypeSuffix + : std::move(output_key_suffix); desc.convert_sample_input = convert_input; - desc.convert_sample_output = convert_output; + desc.output_slots.front().convert_output = convert_output; return desc; } diff --git a/include/company_alg_cpp.hpp b/include/company_alg_cpp.hpp deleted file mode 100644 index 05f11fee..00000000 --- a/include/company_alg_cpp.hpp +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -// Compatibility include; new callers should include . -#include "edgeflow/c_api.hpp" diff --git a/include/company_alg_export.h b/include/company_alg_export.h deleted file mode 100644 index b82d50ba..00000000 --- a/include/company_alg_export.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -// Compatibility include; new callers should include . -#include "edgeflow/export.h" diff --git a/include/company_alg_interface.h b/include/company_alg_interface.h deleted file mode 100644 index 67aa9ad4..00000000 --- a/include/company_alg_interface.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -// Compatibility include; new callers should include . -#include "edgeflow/c_api.h" diff --git a/include/company_alg_log.h b/include/company_alg_log.h deleted file mode 100644 index 451a62d2..00000000 --- a/include/company_alg_log.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -// Compatibility include; new callers should include . -#include "edgeflow/log.h" diff --git a/include/company_alg_version.h b/include/company_alg_version.h deleted file mode 100644 index 172e051c..00000000 --- a/include/company_alg_version.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -// Compatibility include for the generated SDK version. -#include "edgeflow/version.h" diff --git a/include/core/node_registry.h b/include/core/node_registry.h index 145900e4..bcf41185 100644 --- a/include/core/node_registry.h +++ b/include/core/node_registry.h @@ -136,9 +136,6 @@ class NodeRegistry { std::vector conflict_errors_; }; -// Source compatibility for extensions using the former name. -using NodeFactory = NodeRegistry; - #define REGISTER_NODE_WITH_DEFINITION(NodeType, ...) \ static bool _reg_node_##NodeType = []() noexcept { \ return ::llm_edgeflow::NodeRegistry::Instance() \ diff --git a/include/core/session_context.h b/include/core/session_context.h index 10205aa1..ef810c43 100644 --- a/include/core/session_context.h +++ b/include/core/session_context.h @@ -192,16 +192,6 @@ class ModelManager { return result; } - std::vector GetAllRegistrations() const { - std::lock_guard lock(mutex_); - std::vector result; - result.reserve(registrations_.size()); - for (const auto& pair : registrations_) { - result.push_back(pair.second); - } - return result; - } - private: mutable std::mutex mutex_; std::unordered_map registrations_; @@ -223,12 +213,6 @@ class SessionContext { } const RuntimeOptions& GetRuntimeOptions() const { return runtime_options_; } - const std::string& GetChipType() const { return runtime_options_.chip_type; } - int GetPlatformMaxBatch() const { - return runtime_options_.platform_max_batch; - } - uint32_t GetDepthNum() const { return runtime_options_.depth_num; } - template void SetResource(const SessionResourceKey& key, std::shared_ptr resource) { diff --git a/include/nodes/function_node.h b/include/nodes/function_node.h index 10756003..62cbd4a4 100644 --- a/include/nodes/function_node.h +++ b/include/nodes/function_node.h @@ -323,7 +323,6 @@ class InputPortBinding { virtual bool BindPort(const NodeInitContext& init_ctx) = 0; virtual bool PopulateInput(const AlgContext& ctx, InputsT* inputs, std::string* err) const = 0; - virtual const void* GetRawBatch(const InputsT& inputs) const = 0; virtual bool HasBatch(const InputsT& inputs) const = 0; virtual TraceableAlignmentResult ValidateAlignment( const InputsT& inputs, const IProvenanceReader& output_reader) const = 0; @@ -403,10 +402,6 @@ class ConcreteInputPortBinding final : public InputPortBinding { return true; } - const void* GetRawBatch(const InputsT& inputs) const override { - return inputs.*member_ptr_; - } - bool HasBatch(const InputsT& inputs) const override { return (inputs.*member_ptr_) != nullptr; } diff --git a/include/nodes/text_template.h b/include/nodes/text_template.h index 67244938..00e9e79b 100644 --- a/include/nodes/text_template.h +++ b/include/nodes/text_template.h @@ -24,9 +24,9 @@ inline bool IsTextTemplateIdentifier(std::string_view name) { return true; } -// Both {{name}} and the compatible {name} spelling are variables. Other -// single braces remain literal (for example JSON); double braces never escape -// variables. Only compile the original template, never inserted request text. +// Only {{name}} placeholders are variables. Single braces remain literal (for +// example JSON); double braces never escape variables. Only compile the +// original template, never inserted request text. inline bool ParseTextTemplate(const std::string& pattern, std::vector* tokens, std::string* error = nullptr) { @@ -35,44 +35,48 @@ inline bool ParseTextTemplate(const std::string& pattern, if (error) *error = message; return false; }; + auto add_literal = [&](std::string_view lit) { + if (lit.empty()) return; + if (!tokens->empty() && + tokens->back().type == TextTemplateTokenType::kLiteral) { + tokens->back().value.append(lit); + } else { + tokens->push_back({TextTemplateTokenType::kLiteral, std::string(lit)}); + } + }; tokens->clear(); size_t pos = 0; while (pos < pattern.size()) { const size_t open = pattern.find('{', pos); if (open == std::string::npos) { - tokens->push_back({TextTemplateTokenType::kLiteral, pattern.substr(pos)}); + add_literal(pattern.substr(pos)); break; } if (open > pos) { - tokens->push_back( - {TextTemplateTokenType::kLiteral, pattern.substr(pos, open - pos)}); + add_literal(pattern.substr(pos, open - pos)); } const bool is_double = open + 1 < pattern.size() && pattern[open + 1] == '{'; - const size_t width = is_double ? 2 : 1; - const size_t close = pattern.find(is_double ? "}}" : "}", open + width); - if (close == std::string::npos && is_double) { + if (!is_double) { + add_literal("{"); + pos = open + 1; + continue; + } + const size_t close = pattern.find("}}", open + 2); + if (close == std::string::npos) { return reject("Unclosed {{ placeholder in template"); } - if (close != std::string::npos) { - const std::string raw = - pattern.substr(open + width, close - open - width); - const size_t first = raw.find_first_not_of(" \t"); - const size_t last = raw.find_last_not_of(" \t"); - const std::string name = first == std::string::npos - ? std::string{} - : raw.substr(first, last - first + 1); - if (IsTextTemplateIdentifier(name)) { - tokens->push_back({TextTemplateTokenType::kVariable, name}); - pos = close + width; - continue; - } - if (is_double) { - return reject("Invalid {{name}} template placeholder: " + raw); - } + const std::string raw = pattern.substr(open + 2, close - open - 2); + const size_t first = raw.find_first_not_of(" \t"); + const size_t last = raw.find_last_not_of(" \t"); + const std::string name = first == std::string::npos + ? std::string{} + : raw.substr(first, last - first + 1); + if (!IsTextTemplateIdentifier(name)) { + return reject("Invalid {{name}} template placeholder: " + raw); } - tokens->push_back({TextTemplateTokenType::kLiteral, "{"}); - pos = open + 1; + tokens->push_back({TextTemplateTokenType::kVariable, name}); + pos = close + 2; } return true; } diff --git a/include/operator/company_operator_types.h b/include/operator/company_operator_types.h deleted file mode 100644 index cc3579ac..00000000 --- a/include/operator/company_operator_types.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -// Compatibility include; new callers should include -// . -#include "edgeflow/operator/types.h" diff --git a/include/operator/operator_interface.h b/include/operator/operator_interface.h deleted file mode 100644 index 476cb3de..00000000 --- a/include/operator/operator_interface.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -// Compatibility include; new callers should include -// . -#include "edgeflow/operator/interface.h" diff --git a/include/platform_mock/operator_types.h b/include/platform_mock/operator_types.h index db469bb5..21143d54 100644 --- a/include/platform_mock/operator_types.h +++ b/include/platform_mock/operator_types.h @@ -54,7 +54,7 @@ struct ControlUpdateRulesParam { struct ControlSwitchPromptParam { const char* prompt_id = nullptr; // 提示词标识符 (可选) const char* prompt_template_str = - nullptr; // 提示词模板字符串 (包含 {context}/{query} 等占位符) + nullptr; // 提示词模板字符串 (包含 {{context}}/{{query}} 等占位符) }; /** diff --git a/scripts/check_layer_dependencies.py b/scripts/check_layer_dependencies.py index 8f71fc9c..6d55fc49 100644 --- a/scripts/check_layer_dependencies.py +++ b/scripts/check_layer_dependencies.py @@ -19,14 +19,11 @@ def owner(path): if path.startswith("include/contracts/") or path in { - "include/edgeflow/log.h", "include/edgeflow/export.h", - "include/company_alg_log.h", "include/company_alg_export.h", - "include/company_alg_version.h" + "include/edgeflow/log.h", "include/edgeflow/export.h" }: return "Contracts" - if path.startswith(("include/adapter/", "include/operator/", "include/edgeflow/operator/", "include/platform_mock/", "src/adapter/")) or path in { - "include/edgeflow/c_api.h", "include/edgeflow/c_api.hpp", - "include/company_alg_interface.h", "include/company_alg_cpp.hpp" + if path.startswith(("include/adapter/", "include/edgeflow/operator/", "include/platform_mock/", "src/adapter/")) or path in { + "include/edgeflow/c_api.h", "include/edgeflow/c_api.hpp" }: return "Integration" if path.startswith(("include/core/", "src/core/")): diff --git a/scripts/dev_recipe.py b/scripts/dev_recipe.py index beadaeba..bdd9dd5c 100644 --- a/scripts/dev_recipe.py +++ b/scripts/dev_recipe.py @@ -5,8 +5,8 @@ - prompt-config: adjust prompts using existing nodes within identical biz contracts - text-llm-node: create a custom LLM node (TextBatch -> TextBatch) with full scaffolding -Both recipes require a single-output mem_que deployment. Pipelines or confs with -data.outputs are rejected early with UNSUPPORTED_RECIPE_DEPLOYMENT. +Both recipes require a single-output deployment. Pipelines or confs with +multiple outputs are rejected early with UNSUPPORTED_RECIPE_DEPLOYMENT. """ import argparse @@ -37,13 +37,13 @@ "prompt-config": { "title": "Prompt Configuration Recipe", "description": "Adjust prompt templates and configurations using existing nodes within identical biz contracts.", - "preconditions": "Requires a verified data.mem_que Profile and valid Pipeline.", + "preconditions": "Requires a verified data.outputs Profile and valid Pipeline.", "artifacts": "Pipeline JSON, pipeline .conf, effects sample, verification command.", }, "text-llm-node": { "title": "Text LLM Node Recipe", "description": "Create a custom LLM node handling TextBatch -> TextBatch with tests and pipeline deployment.", - "preconditions": "Requires TextBatch 1:1 preserve ports, registered LLM model capability, and data.mem_que.", + "preconditions": "Requires TextBatch 1:1 preserve ports, registered LLM model capability, and single-output data.outputs.", "artifacts": "Node source, unit test, CMake registration, Pipeline JSON, .conf, effects sample.", }, } @@ -76,11 +76,12 @@ def absolute(path, root): def check_unsupported_deployment(conf_path): conf = read_json_file(conf_path) data = conf.get("data", {}) - if "outputs" in data: + outputs = data.get("outputs") + if not isinstance(outputs, dict) or not outputs: + raise RecipeError("A data.outputs deployment is required") + if len(outputs) > 1: return {"error_code": UNSUPPORTED_RECIPE_DEPLOYMENT, - "message": "This recipe supports only single-output data.mem_que; use the native Operator workflow for data.outputs."} - if not isinstance(data.get("mem_que"), dict): - raise RecipeError("A data.mem_que deployment is required") + "message": "This recipe supports only single-output deployment; use the native Operator workflow for data.outputs with multiple slots."} return None @@ -88,7 +89,7 @@ def require_deployment(conf_path): unsupported = check_unsupported_deployment(conf_path) if unsupported: raise RecipeError(unsupported["message"], code=unsupported["error_code"]) - return read_json_file(conf_path)["data"]["mem_que"] + return read_json_file(conf_path)["data"]["outputs"] def get_profile_data(profile_name, root=ROOT): @@ -151,12 +152,12 @@ def deployment_root(root, pipeline, model_root): return Path(os.path.commonpath([root, pipeline.parent, model_root])).resolve() -def make_recipe_conf(pipeline_doc, mem_que, pipeline_target, root, model_root=None): +def make_recipe_conf(pipeline_doc, outputs, pipeline_target, root, model_root=None): root = root.resolve() pipeline = absolute(pipeline_target, root) models = absolute(model_root, root) if model_root is not None else root / "models" bundle = deployment_root(root, pipeline, models) - return VERIFY_SELECTION.build_run_conf(pipeline_doc, mem_que, + return VERIFY_SELECTION.build_run_conf(pipeline_doc, outputs, pipeline.relative_to(bundle), models, bundle) @@ -257,7 +258,7 @@ def prepare(recipe, name, profile_name, tool_path, build_dir, pipeline_target, r raise RecipeError("Node name must be a PascalCase C++ identifier") name = name if name.endswith("Node") else name + "Node" _, source_conf, _ = get_profile_data(profile_name, root) - mem_que = require_deployment(source_conf) # Reject outputs before tools or writes. + outputs = require_deployment(source_conf) # Reject multi-output before tools or writes. tool, build, demo = tool_context(tool_path, build_dir, root) catalog = native(tool, ["catalog"], root) profile = next((p for p in catalog["profiles"] if p["name"] == profile_name), None) @@ -298,7 +299,7 @@ def prepare(recipe, name, profile_name, tool_path, build_dir, pipeline_target, r ]: plan.add_modification(path, path.read_text(encoding="utf-8"), update(path, filename)) generated = [src, test] - conf = make_recipe_conf(pipeline, mem_que, target, root, models) + conf = make_recipe_conf(pipeline, outputs, target, root, models) spec = copy.deepcopy(spec) spec["name"] = name + "_effects" spec["dataset"] = os.path.relpath(dataset, effects_target.parent) @@ -309,7 +310,7 @@ def prepare(recipe, name, profile_name, tool_path, build_dir, pipeline_target, r temp = Path(temporary) preview_pipeline = temp / "pipeline.json" preview_pipeline.write_text(json.dumps(deployment_preview), encoding="utf-8") - preview_conf = VERIFY_SELECTION.build_run_conf(deployment_preview, mem_que, preview_pipeline.relative_to(bundle), models, bundle) + preview_conf = VERIFY_SELECTION.build_run_conf(deployment_preview, outputs, preview_pipeline.relative_to(bundle), models, bundle) (temp / "pipeline.conf").write_text(json.dumps(preview_conf), encoding="utf-8") native(tool, ["resolve-conf", str((temp / "pipeline.conf").relative_to(bundle)), "--root", str(bundle)], root) for path, document in [(target, pipeline), (conf_target, conf), (effects_target, spec)]: diff --git a/src/adapter/biz/audio_asr_intent_operator_bridge.cpp b/src/adapter/biz/audio_asr_intent_operator_bridge.cpp index 4063057c..85e7af46 100644 --- a/src/adapter/biz/audio_asr_intent_operator_bridge.cpp +++ b/src/adapter/biz/audio_asr_intent_operator_bridge.cpp @@ -34,7 +34,7 @@ void RegisterAudioAsrIntentBridge() { return 0; }; - desc.convert_sample_output = + desc.output_slots.front().convert_output = [](const void* internal_dto, void* external_output_struct, const ResolvedOutputPoolSpec& spec, std::string* err) -> int { if (!internal_dto || !external_output_struct) { diff --git a/src/adapter/biz/compliance_audit_operator_bridge.cpp b/src/adapter/biz/compliance_audit_operator_bridge.cpp index c2bc0e1f..41d6a7ac 100644 --- a/src/adapter/biz/compliance_audit_operator_bridge.cpp +++ b/src/adapter/biz/compliance_audit_operator_bridge.cpp @@ -27,7 +27,7 @@ void RegisterComplianceAuditBridge() { return 0; }; - desc.convert_sample_output = + desc.output_slots.front().convert_output = [](const void* internal_dto, void* external_output_struct, const ResolvedOutputPoolSpec& spec, std::string* err) -> int { if (!internal_dto || !external_output_struct) { diff --git a/src/adapter/biz/cross_rerank_operator_bridge.cpp b/src/adapter/biz/cross_rerank_operator_bridge.cpp index 0b012aa6..f07a26a8 100644 --- a/src/adapter/biz/cross_rerank_operator_bridge.cpp +++ b/src/adapter/biz/cross_rerank_operator_bridge.cpp @@ -40,7 +40,7 @@ void RegisterCrossRerankBridge() { return 0; }; - desc.convert_sample_output = + desc.output_slots.front().convert_output = [](const void* internal_dto, void* external_output_struct, const ResolvedOutputPoolSpec& /*spec*/, std::string* err) -> int { if (!internal_dto || !external_output_struct) { diff --git a/src/adapter/biz/doc_qa_operator_bridge.cpp b/src/adapter/biz/doc_qa_operator_bridge.cpp index 4699bc34..90acaa9d 100644 --- a/src/adapter/biz/doc_qa_operator_bridge.cpp +++ b/src/adapter/biz/doc_qa_operator_bridge.cpp @@ -26,7 +26,7 @@ void RegisterDocQaBridge() { return 0; }; - desc.convert_sample_output = + desc.output_slots.front().convert_output = [](const void* internal_dto, void* external_output_struct, const ResolvedOutputPoolSpec& spec, std::string* err) -> int { if (!internal_dto || !external_output_struct) { diff --git a/src/adapter/biz/keyword_match_operator_bridge.cpp b/src/adapter/biz/keyword_match_operator_bridge.cpp index 8cf3e4c1..a6e8efd4 100644 --- a/src/adapter/biz/keyword_match_operator_bridge.cpp +++ b/src/adapter/biz/keyword_match_operator_bridge.cpp @@ -26,7 +26,7 @@ void RegisterKeywordMatchBridge() { return 0; }; - desc.convert_sample_output = + desc.output_slots.front().convert_output = [](const void* internal_dto, void* external_output_struct, const ResolvedOutputPoolSpec& spec, std::string* err) -> int { if (!internal_dto || !external_output_struct) { diff --git a/src/adapter/biz/ocr_doc_qa_operator_bridge.cpp b/src/adapter/biz/ocr_doc_qa_operator_bridge.cpp index 9a11bb64..65d8fafe 100644 --- a/src/adapter/biz/ocr_doc_qa_operator_bridge.cpp +++ b/src/adapter/biz/ocr_doc_qa_operator_bridge.cpp @@ -33,6 +33,7 @@ void RegisterOcrDocQaBridge() { out_slot.type_suffix = "od_out"; out_slot.direction = IoDirection::kOutput; out_slot.required = true; + out_slot.key_suffix = "od_out"; desc.output_slots.push_back(out_slot); desc.convert_sample_input = @@ -61,7 +62,7 @@ void RegisterOcrDocQaBridge() { return 0; }; - desc.convert_sample_output = + desc.output_slots.front().convert_output = [](const void* internal_dto, void* external_output_struct, const ResolvedOutputPoolSpec& spec, std::string* err) -> int { if (!internal_dto || !external_output_struct) { diff --git a/src/adapter/operator/operator_adapter.cpp b/src/adapter/operator/operator_adapter.cpp index dd7b4849..76093376 100644 --- a/src/adapter/operator/operator_adapter.cpp +++ b/src/adapter/operator/operator_adapter.cpp @@ -395,8 +395,7 @@ int Operator_Process(void* handle, const NamedIoBatch& inputs, llm_edgeflow::ConvertSampleOutputFn convert = nullptr; for (const auto& slot : h->bridge->output_slots) { if (slot.logical_name == acq.logical_name) { - convert = slot.convert_output ? slot.convert_output - : h->bridge->convert_sample_output; + convert = slot.convert_output; break; } } diff --git a/src/adapter/operator/operator_biz_bridge_registry.cpp b/src/adapter/operator/operator_biz_bridge_registry.cpp index 06305798..cdb836e0 100644 --- a/src/adapter/operator/operator_biz_bridge_registry.cpp +++ b/src/adapter/operator/operator_biz_bridge_registry.cpp @@ -120,26 +120,27 @@ bool OperatorBizBridgeRegistry::RegisterBridge( return reject({"Bridge must declare at least one output slot"}); } for (const auto& s : desc.output_slots) { - if (s.logical_name.empty() || s.type_suffix.empty()) { + if (s.logical_name.empty() || s.type_suffix.empty() || + s.key_suffix.empty()) { return reject( - {"Output slot requires nonempty logical_name and type_suffix; slot '", + {"Output slot requires nonempty logical_name, type_suffix, and " + "key_suffix; slot '", s.logical_name, "', type '", s.type_suffix, "'"}); } - if (s.KeySuffix().find('.') != std::string::npos) { + if (s.key_suffix.find('.') != std::string::npos) { return reject( - {"Output key suffix must not contain a dot: '", s.KeySuffix(), "'"}); + {"Output key suffix must not contain a dot: '", s.key_suffix, "'"}); } if (s.direction != IoDirection::kOutput) { return reject({"Output slot '", s.logical_name, "' (", s.type_suffix, ") must have output direction"}); } if (!out_names.insert(s.logical_name).second || - !out_suffixes.insert(s.KeySuffix()).second) { + !out_suffixes.insert(s.key_suffix).second) { return reject({"Duplicate output slot name or key suffix: '", - s.logical_name, "' (", s.KeySuffix(), ")"}); + s.logical_name, "' (", s.key_suffix, ")"}); } - if (!s.convert_output && - (desc.output_slots.size() != 1 || !desc.convert_sample_output)) { + if (!s.convert_output) { return reject({"Output slot '", s.logical_name, "' requires its own convert_output callback"}); } @@ -253,21 +254,10 @@ int OperatorBizBridgeRegistry::GlobalInit(std::string* diagnostic) { "' does not match BizAdapter result '", adapter->ResultTypeName(), "'"}); } - // Adapter 标识匹配;兼容旧 bridge 使用已声明的 Pipeline biz_name。 - bool adapter_name_matched = (desc.adapter_name == adapter->AdapterName()); - if (!adapter_name_matched) { - for (const auto& p : adapter_desc.biz_definitions) { - if (p.biz_name == desc.adapter_name) { - adapter_name_matched = true; - break; - } - } - } - if (!adapter_name_matched) { - return reject( - desc.biz_type, desc.adapter_name, - {"Bridge adapter_name does not match BizAdapter '", - adapter->AdapterName(), "' or any of its Pipeline biz_names"}); + if (desc.adapter_name != adapter->AdapterName()) { + return reject(desc.biz_type, desc.adapter_name, + {"Bridge adapter_name does not match BizAdapter '", + adapter->AdapterName(), "'"}); } for (const auto& slot : desc.input_slots) { diff --git a/src/adapter/operator/operator_config_resolver.cpp b/src/adapter/operator/operator_config_resolver.cpp index c9a27ce9..1210b29e 100644 --- a/src/adapter/operator/operator_config_resolver.cpp +++ b/src/adapter/operator/operator_config_resolver.cpp @@ -390,7 +390,7 @@ int OperatorConfigResolver::Resolve(const char* model_path, } const nlohmann::json* data_obj = &conf_json["data"]; static const std::unordered_set kAllowedDataFields = { - "pipe_path", "model_paths", "mem_que", "outputs"}; + "pipe_path", "model_paths", "outputs"}; for (auto it = data_obj->begin(); it != data_obj->end(); ++it) { if (kAllowedDataFields.find(it.key()) == kAllowedDataFields.end()) { if (error_msg) { @@ -465,55 +465,36 @@ int OperatorConfigResolver::Resolve(const char* model_path, return -5; } - const bool legacy_output = data_obj->contains("mem_que"); - const bool named_outputs = data_obj->contains("outputs"); - if (legacy_output == named_outputs) { - if (error_msg) - *error_msg = - legacy_output - ? "data.mem_que and data.outputs are mutually exclusive" - : "Missing required 'mem_que' object or 'outputs' object in " - "conf"; - return -2; - } - if (legacy_output && (bridge_desc->output_slots.size() != 1 || - !(*data_obj)["mem_que"].is_object())) { - if (error_msg) - *error_msg = - "data.mem_que requires exactly one output slot and an object"; + if (!data_obj->contains("outputs")) { + if (error_msg) *error_msg = "Missing required 'outputs' object in conf"; return -2; } - if (named_outputs && !(*data_obj)["outputs"].is_object()) { + if (!(*data_obj)["outputs"].is_object()) { if (error_msg) *error_msg = "data.outputs must be an object keyed by logical output slot"; return -2; } - if (named_outputs) { - for (const auto& [name, value] : (*data_obj)["outputs"].items()) { - bool known = false; - for (const auto& slot : bridge_desc->output_slots) { - if (slot.logical_name == name) known = true; - } - if (!known) { - if (error_msg) *error_msg = "Unknown configured output slot: " + name; - return -2; - } + for (const auto& [name, value] : (*data_obj)["outputs"].items()) { + bool known = false; + for (const auto& slot : bridge_desc->output_slots) { + if (slot.logical_name == name) known = true; + } + if (!known) { + if (error_msg) *error_msg = "Unknown configured output slot: " + name; + return -2; } } std::unordered_map pool_specs; std::unordered_map parameter_texts; for (const auto& slot : bridge_desc->output_slots) { - if (named_outputs && - !(*data_obj)["outputs"].contains(slot.logical_name)) { + if (!(*data_obj)["outputs"].contains(slot.logical_name)) { if (error_msg) *error_msg = "Missing allocation configuration for output slot: " + slot.logical_name; return -2; } - const auto& config = legacy_output - ? (*data_obj)["mem_que"] - : (*data_obj)["outputs"][slot.logical_name]; + const auto& config = (*data_obj)["outputs"][slot.logical_name]; ResolvedOutputPoolSpec spec; std::string parameter_text; std::string allocation_error; diff --git a/src/common_nodes/text_template_node.cpp b/src/common_nodes/text_template_node.cpp index 45a47e61..82dc4178 100644 --- a/src/common_nodes/text_template_node.cpp +++ b/src/common_nodes/text_template_node.cpp @@ -572,7 +572,7 @@ class TextTemplateNode final : public NodeBase { return Fail(req_ctx, node_error::text_template::kMissingVariable, "Missing required template variable: " + var); } else if (state.missing_variable_policy == "preserve") { - rendered += "{" + var + "}"; + rendered += "{{" + var + "}}"; } } } @@ -625,8 +625,8 @@ NodeDefinition MakeTextTemplateNodeDefinition() { def.category = "common"; def.validate_config = TextTemplateNode::ValidateConfig; def.description = - "Text template rendering: {{name}} and {name} substitute variables; " - "JSON braces remain literal"; + "Text template rendering: {{name}} substitutes variables; " + "single {name} and JSON braces remain literal"; def.inputs = { OptionalInputPort("primary", BlackboardKey{"", "TextBatch"}, "1:1", "preserve", "request"), diff --git a/src/custom_nodes/README.md b/src/custom_nodes/README.md index a3d5d20e..96357dca 100644 --- a/src/custom_nodes/README.md +++ b/src/custom_nodes/README.md @@ -172,24 +172,11 @@ helper 不增加热更新或并发能力。 结果位于 `results/custom-node//results.jsonl` 与 `summary.json`;两个 Profile 也纳入 `--suite smoke`。测试模型用于验证编排、来源和输出转换,不能据此评价模型效果。 -样例显式设置 `template_syntax: "standard"`,使用 `{{input}}` / `{{context}}`。 -它与 `TextTemplateNode` 共用解析器:`{{name}}` 与兼容写法 `{name}` 都替换变量, -允许变量名两侧的空格;JSON 的普通花括号直接保留,例如 -`{"question":"{{input}}"}`。双花括号不再表示字面转义。变量名仍由各 Node 声明: -TextTemplate 的主文本叫 `primary`,本样例叫 `input`;复制模板时应对应替换, -两者共有的 `context` 保持相同语义,未知变量会报错。 - -已有 PromptGuided 模板按以下规则迁移,避免旧模板的字面内容变成变量: - -| `template_syntax` | 语义与迁移方式 | -| --- | --- | -| `standard` | 推荐用于新方案;单双括号均为变量,JSON 花括号直接书写。 | -| `auto`(默认) | 保留原先没有双花括号的模板行为;发现 `{{` 或 `}}` 就拒绝,并提示显式选择语法,不猜测含义。默认 `{input}` 无需迁移。 | -| `legacy` | 明确保留旧模板:`{input}` / `{context}` 为变量,`{{` / `}}` 为字面花括号;例如 `{{input}}` 输出字面 `{input}`。 | - -旧 JSON 模板如 `{{"question":"{input}"}}` 可先设置 `legacy` 保持输出;迁移到 -`standard` 时改成 `{"question":"{{input}}"}`。不要只切换模式而保留旧转义文本。 -两种显式模式都由原生 Validator 与 Node 初始化使用同一条校验路径。 +样例统一使用 `{{input}}` / `{{context}}` 占位符。 +它与 `TextTemplateNode` 共用解析器:`{{name}}` 统一替换变量,允许变量名两侧的空格; +普通单个花括号(如 JSON)直接作为字面内容保留,例如 `{"question":"{{input}}"}`。 +双花括号不再表示字面转义。变量名由各 Node 声明:TextTemplate 的主文本叫 `primary`, +本样例叫 `input`;复制模板时应对应替换,两者共有的 `context` 保持相同语义,未知变量会报错。 使用 context 变量必须连入 context;按相同 `req_id` 合并片段,空上下文批次表示 没有参考内容。默认模板只插入 input,不隐式追加 context。输入、上下文和 diff --git a/src/custom_nodes/prompt_guided_llm_node.cpp b/src/custom_nodes/prompt_guided_llm_node.cpp index d1d1d768..1dda7eeb 100644 --- a/src/custom_nodes/prompt_guided_llm_node.cpp +++ b/src/custom_nodes/prompt_guided_llm_node.cpp @@ -26,48 +26,6 @@ constexpr int kMissingInput = -8001; constexpr int kModelInferenceFailed = -8002; constexpr int kOutputProvenanceMismatch = -8003; -// Kept only for explicitly selected legacy syntax and unambiguous old -// templates. -bool ParseLegacyPromptTemplate(const std::string& pattern, - std::vector* parts, - std::string* error) { - auto reject = [&](const std::string& message) { - if (error) *error = message; - return false; - }; - parts->clear(); - std::string literal; - for (size_t i = 0; i < pattern.size();) { - const char c = pattern[i]; - if ((c == '{' || c == '}') && i + 1 < pattern.size() && - pattern[i + 1] == c) { - literal += c; - i += 2; - } else if (c == '{') { - const auto end = pattern.find('}', i + 1); - if (end == std::string::npos) - return reject("Unclosed prompt placeholder"); - const auto token = pattern.substr(i + 1, end - i - 1); - if (token != "input" && token != "context") { - return reject("Unknown prompt placeholder: " + token); - } - parts->push_back({TextTemplateTokenType::kLiteral, std::move(literal)}); - literal.clear(); - parts->push_back({TextTemplateTokenType::kVariable, token}); - i = end + 1; - } else if (c == '}') { - return reject( - "Unescaped } in legacy prompt template; choose " - "template_syntax=standard for literal JSON braces"); - } else { - literal += c; - ++i; - } - } - parts->push_back({TextTemplateTokenType::kLiteral, std::move(literal)}); - return true; -} - // Ordinary, owned configuration used by processing after initialization. struct PromptConfig { std::vector prompt_parts; @@ -89,21 +47,7 @@ bool ParsePromptConfig(const nlohmann::json& config, PromptConfig* parameters, const auto& pattern = config.at("prompt_template").get_ref(); if (pattern.empty()) return reject("prompt_template must not be empty"); - const auto& syntax = - config.at("template_syntax").get_ref(); - if (syntax == "auto" && (pattern.find("{{") != std::string::npos || - pattern.find("}}") != std::string::npos)) { - return reject( - "Ambiguous double braces in prompt_template: set " - "template_syntax=standard to substitute {{input}}/{{context}} " - "as in TextTemplateNode, or template_syntax=legacy to preserve " - "old {{ / }} literal-brace escaping"); - } - if (syntax == "standard") { - if (!ParseTextTemplate(pattern, &parts, error)) return false; - } else if (!ParseLegacyPromptTemplate(pattern, &parts, error)) { - return false; - } + if (!ParseTextTemplate(pattern, &parts, error)) return false; for (const auto& part : parts) { if (part.type != TextTemplateTokenType::kVariable) continue; if (part.value != "input" && part.value != "context") { @@ -143,22 +87,12 @@ const NodeConfigParser& PromptConfiguration() { ConfigFieldDefinition{"prompt_template", ConfigValueKind::kString, false, - "{input}", + "{{input}}", std::nullopt, std::nullopt, {}, - "提示词模板;standard 示例为 " - "\"回答:{{input}}\\n背景:{{context}}\",使用 " + "提示词模板;使用 {{input}}/{{context}},使用 " "context 时须连接该输入。"}, - ConfigFieldDefinition{"template_syntax", - ConfigValueKind::kString, - false, - "auto", - std::nullopt, - std::nullopt, - {"auto", "standard", "legacy"}, - "standard 使用 {{input}}/{{context}};legacy 使用 " - "{input}/{context};auto 拒绝有歧义的混用。"}, ConfigFieldDefinition{"prompt_prefix", ConfigValueKind::kString, false, @@ -415,9 +349,7 @@ NodeDefinition MakePromptGuidedLlmNodeDefinition() { def.category = "custom"; def.description = "Custom domain node combining prompt construction, LLM generation, " - "and response post-processing; template_syntax=standard uses " - "{{name}}/{name} like TextTemplateNode, auto rejects ambiguous double " - "braces, legacy preserves old brace escaping"; + "and response post-processing using {{input}}/{{context}} templates"; def.inputs = { RequiredInputPort("input", BlackboardKey{"", "TextBatch"}, "1:1", "preserve", "request"), diff --git a/tests/contract/abi/test_c11_abi_compliance.c b/tests/contract/abi/test_c11_abi_compliance.c index 69cf6a43..a6dc3791 100644 --- a/tests/contract/abi/test_c11_abi_compliance.c +++ b/tests/contract/abi/test_c11_abi_compliance.c @@ -22,12 +22,7 @@ #endif #include "edgeflow/log.h" #include "edgeflow/operator/types.h" - -// Old and canonical include paths must coexist without redefining C contracts. -#include "company_alg_interface.h" -#include "company_alg_log.h" -#include "company_alg_version.h" -#include "operator/company_operator_types.h" +#include "edgeflow/version.h" _Static_assert(sizeof(CompanyAlgBizType) == sizeof(int32_t), "CompanyAlgBizType must remain a 32-bit C ABI type"); diff --git a/tests/contract/architecture/test_layer_header_views.cmake b/tests/contract/architecture/test_layer_header_views.cmake index bfce4891..df671c71 100644 --- a/tests/contract/architecture/test_layer_header_views.cmake +++ b/tests/contract/architecture/test_layer_header_views.cmake @@ -48,9 +48,11 @@ check_header(capability_nodes core/port_definition.h TRUE) check_header(public edgeflow/c_api.h TRUE) check_header(public edgeflow/c_api.hpp TRUE) check_header(public edgeflow/operator/interface.h TRUE) -check_header(public company_alg_interface.h TRUE) -check_header(public company_alg_cpp.hpp TRUE) -check_header(public operator/operator_interface.h TRUE) +check_header(public edgeflow/operator/types.h TRUE) +check_header(public company_alg_interface.h FALSE) +check_header(public company_alg_cpp.hpp FALSE) +check_header(public operator/operator_interface.h FALSE) +check_header(public operator/company_operator_types.h FALSE) check_header(public adapter/biz_adapter_interface.h FALSE) check_header(public adapter/adapter_authoring.h FALSE) check_header(public adapter/adapter_result.h FALSE) diff --git a/tests/contract/architecture/test_llama_cache.cmake b/tests/contract/architecture/test_llama_cache.cmake index 6210d6b0..6c111848 100644 --- a/tests/contract/architecture/test_llama_cache.cmake +++ b/tests/contract/architecture/test_llama_cache.cmake @@ -61,6 +61,7 @@ endfunction() function(check_llama_cache _case_name _apple _blas) set(_case_root "${TEST_ROOT}/${_case_name}") set(_case_source "${_case_root}/source") + file(REMOVE_RECURSE "${_case_root}") file(MAKE_DIRECTORY "${_case_source}/cmake_ext") file(COPY "${_llama_fixture}/CMakeLists.txt" DESTINATION "${_case_source}") file(COPY "${PROJECT_SOURCE_DIR}/cmake_ext/ThirdPartyCacheMetadata.cmake" diff --git a/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_fixture.conf b/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_fixture.conf index 98d6da14..b2f37519 100644 --- a/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_fixture.conf +++ b/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_fixture.conf @@ -4,11 +4,13 @@ "model_paths": { "rerank_model_v1": "rerank_fixture.onnx" }, - "mem_que": { - "type": "rerank_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": {} + "outputs": { + "rerank_out": { + "type": "rerank_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": {} + } } } } diff --git a/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_missing_model.conf b/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_missing_model.conf index 58286948..300a32b7 100644 --- a/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_missing_model.conf +++ b/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_missing_model.conf @@ -4,11 +4,13 @@ "model_paths": { "rerank_model_v1": "missing_rerank_fixture.onnx" }, - "mem_que": { - "type": "rerank_out", - "meta_num": 0, - "metadata_type_id": 0, - "capacities": {} + "outputs": { + "rerank_out": { + "type": "rerank_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": {} + } } } } diff --git a/tests/integration/demo/test_demo_runner.cpp b/tests/integration/demo/test_demo_runner.cpp index e8b7159e..3aa6b44c 100644 --- a/tests/integration/demo/test_demo_runner.cpp +++ b/tests/integration/demo/test_demo_runner.cpp @@ -897,7 +897,6 @@ TEST(DemoRunnerTest, GenericControlCommandChangesCustomNodeOutput) { options.config_path = (temporary.path / "pipeline.conf").string(); options.dataset_path = (temporary.path / "input.txt").string(); options.output_dir = (temporary.path / "results").string(); - options.no_default_control = true; options.control_cmd = 2000000041; options.control_file = (temporary.path / "control.json").string(); const auto* demo = DemoRegistry::Instance().Find("keyword_match"); @@ -1016,27 +1015,30 @@ TEST(DemoRunnerTest, ExampleControlIsExplicitAndFileControlTakesPrecedence) { EXPECT_EQ(ops.Deinit(), 0); } -TEST(DemoRunnerTest, ExampleControlCliAndCompatibilityFlag) { +TEST(DemoRunnerTest, ExampleControlCliAndRejectsRemovedFlag) { std::string error; - for (const char* flag : {"--example-control", "--no-default-control"}) { + { DemoOptions options; - const char* args[] = {"alg_demo", flag}; + const char* args[] = {"alg_demo", "--example-control"}; ASSERT_EQ(ParseCommandLine(2, const_cast(args), &options, &error), 0) << error; - EXPECT_EQ(options.example_control, - std::string(flag) == "--example-control"); - EXPECT_EQ(options.no_default_control, - std::string(flag) == "--no-default-control"); + EXPECT_TRUE(options.example_control); } - for (bool reverse : {false, true}) { + { + DemoOptions options; + const char* args[] = {"alg_demo"}; + ASSERT_EQ(ParseCommandLine(1, const_cast(args), &options, &error), + 0) + << error; + EXPECT_FALSE(options.example_control); + } + { DemoOptions options; - const char* args[] = { - "alg_demo", reverse ? "--no-default-control" : "--example-control", - reverse ? "--example-control" : "--no-default-control"}; - EXPECT_EQ(ParseCommandLine(3, const_cast(args), &options, &error), + const char* args[] = {"alg_demo", "--no-default-control"}; + EXPECT_EQ(ParseCommandLine(2, const_cast(args), &options, &error), 2); - EXPECT_NE(error.find("conflicts"), std::string::npos); + EXPECT_NE(error.find("Unknown CLI option"), std::string::npos); } } diff --git a/tests/integration/operator/test_operator_api.cpp b/tests/integration/operator/test_operator_api.cpp index ad45e5c6..1eb2340e 100644 --- a/tests/integration/operator/test_operator_api.cpp +++ b/tests/integration/operator/test_operator_api.cpp @@ -1163,8 +1163,8 @@ TEST_F(OperatorApiTest, ConcurrentDifferentHandles) { EXPECT_EQ(ops_.Destroy(handle2), 0); } -// 21. mem_que 配置校验与异常 Fail-Closed 测试 -TEST_F(OperatorApiTest, MemQueConfigValidationFailClosed) { +// 21. outputs 配置校验与异常 Fail-Closed 测试 +TEST_F(OperatorApiTest, OutputsConfigValidationFailClosed) { ScopedTempDirectory temp_dir; std::filesystem::path root = temp_dir.path(); std::filesystem::create_directories(root / "configs"); @@ -1176,15 +1176,6 @@ TEST_F(OperatorApiTest, MemQueConfigValidationFailClosed) { std::string conf_file = "configs/test.conf"; std::filesystem::path conf_path = root / conf_file; - // 1. 缺失 mem_que 对象 -> -2 - { - std::ofstream ofs(conf_path); - ofs << R"({ - "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json" - } - })"; - } const std::string root_str = root.string(); CreateParam param{}; param.model_path = root_str.c_str(); @@ -1194,16 +1185,47 @@ TEST_F(OperatorApiTest, MemQueConfigValidationFailClosed) { param.max_frame_depth = 25; void* handle = nullptr; - EXPECT_EQ(ops_.Create(&handle, ¶m), -2); - // 2. mem_que.type 与业务不匹配 -> -2 + // 0. 未知字段 mem_que 严格拒绝 -> -2 { std::ofstream ofs(conf_path); ofs << R"({ "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", "mem_que": { - "type": "doc_out" + "type": "keyword_out" + } + } + })"; + } + EXPECT_EQ(ops_.Create(&handle, ¶m), -2); + EXPECT_NE(std::string(GetOperatorLastError()) + .find("Unknown field in conf data: 'mem_que'"), + std::string::npos); + EXPECT_EQ(std::string(GetOperatorLastError()).find("is no longer supported"), + std::string::npos); + + // 1. 缺失 outputs 对象 -> -2 + { + std::ofstream ofs(conf_path); + ofs << R"({ + "data": { + "pipe_path": "configs/pipeline_keyword_match_rules.json" + } + })"; + } + EXPECT_EQ(ops_.Create(&handle, ¶m), -2); + + // 2. outputs.keyword_out.type 与业务不匹配 -> -2 + { + std::ofstream ofs(conf_path); + ofs << R"({ + "data": { + "pipe_path": "configs/pipeline_keyword_match_rules.json", + "outputs": { + "keyword_out": { + "type": "doc_out" + } } } })"; @@ -1216,10 +1238,12 @@ TEST_F(OperatorApiTest, MemQueConfigValidationFailClosed) { ofs << R"({ "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": 123 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": 123 + } } } })"; @@ -1232,10 +1256,12 @@ TEST_F(OperatorApiTest, MemQueConfigValidationFailClosed) { ofs << R"({ "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", - "mem_que": { - "type": "keyword_out", - "capacities": { - "unknown_field_xyz": 100 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "capacities": { + "unknown_field_xyz": 100 + } } } } @@ -1248,10 +1274,12 @@ TEST_F(OperatorApiTest, MemQueConfigValidationFailClosed) { std::ofstream ofs(conf_path); ofs << R"({ "pipe_path": "configs/pipeline_keyword_match_rules.json", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": 0 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": 0 + } } })"; } @@ -1264,10 +1292,12 @@ TEST_F(OperatorApiTest, MemQueConfigValidationFailClosed) { "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", "model_path": "models/unused.bin", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": 0 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": 0 + } } } })"; @@ -1280,10 +1310,12 @@ TEST_F(OperatorApiTest, MemQueConfigValidationFailClosed) { ofs << R"({ "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": 0 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": 0 + } } }, "comment": "not part of the runtime contract" @@ -1880,10 +1912,12 @@ TEST_F(OperatorApiTest, ModelPathNonExistentFileAllowedWhileEscapeRejected) { "embed_model_v1": "models/not_deployed_embed.bin", "llm_model_v1": "models/not_deployed_llm.bin" }, - "mem_que": { - "type": "doc_out", - "meta_num": 0, - "metadata_type_id": 0 + "outputs": { + "doc_out": { + "type": "doc_out", + "meta_num": 0, + "metadata_type_id": 0 + } } } })"; @@ -1920,10 +1954,12 @@ TEST_F(OperatorApiTest, ModelPathNonExistentFileAllowedWhileEscapeRejected) { "model_paths": { "asr_model_v1": "deployment/asr_model_will_arrive_later.bin" }, - "mem_que": { - "type": "audio_out", - "meta_num": 0, - "metadata_type_id": 0 + "outputs": { + "audio_out": { + "type": "audio_out", + "meta_num": 0, + "metadata_type_id": 0 + } } } })"; @@ -2007,7 +2043,11 @@ TEST_F(OperatorApiTest, ModelPathNonExistentFileAllowedWhileEscapeRejected) { conf << R"({ "data": { "pipe_path": "configs/missing_pipeline.json", - "mem_que": {"type": "keyword_out"} + "outputs": { + "keyword_out": { + "type": "keyword_out" + } + } } })"; conf.close(); @@ -2045,7 +2085,7 @@ TEST_F(OperatorApiTest, ModelPathNonExistentFileAllowedWhileEscapeRejected) { invalid_conf << nlohmann::json( {{"data", {{"pipe_path", pipe_path}, - {"mem_que", {{"type", "keyword_out"}}}}}}); + {"outputs", {{"keyword_out", {{"type", "keyword_out"}}}}}}}}); invalid_conf.close(); EXPECT_EQ(llm_edgeflow::OperatorConfigResolver::Resolve( root_string.c_str(), "configs/invalid_pipe.conf", &resolved, @@ -2063,10 +2103,10 @@ TEST_F(OperatorApiTest, DotDotPrefixedControlFileNamesStayWithinRoot) { std::filesystem::copy_file(std::filesystem::path(GetConfDir()) / "configs/pipeline_keyword_match_rules.json", root / "..configs/pipeline.json"); - std::ofstream(root / "..configs/pipeline.conf") - << nlohmann::json({{"data", - {{"pipe_path", "..configs/pipeline.json"}, - {"mem_que", {{"type", "keyword_out"}}}}}}); + std::ofstream(root / "..configs/pipeline.conf") << nlohmann::json( + {{"data", + {{"pipe_path", "..configs/pipeline.json"}, + {"outputs", {{"keyword_out", {{"type", "keyword_out"}}}}}}}}); llm_edgeflow::ResolvedOperatorConfig resolved; std::string error; @@ -2097,9 +2137,10 @@ TEST_F(OperatorApiTest, VariableResultsUsePoolCapacityAndRollbackOnFailure) { std::ofstream(temp.path() / "pipeline.conf") << nlohmann::json( {{"data", {{"pipe_path", "pipeline.json"}, - {"mem_que", - {{"type", "keyword_out"}, - {"capacities", {{"match_result_json", capacity}}}}}}}}); + {"outputs", + {{"keyword_out", + {{"type", "keyword_out"}, + {"capacities", {{"match_result_json", capacity}}}}}}}}}}); const auto root = temp.path().string(); CreateParam param{}; param.model_path = root.c_str(); @@ -2160,10 +2201,12 @@ TEST_F(OperatorApiTest, MetadataTypeIdOutOfInt32RangeIsRejected) { conf << R"({ "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": 3000000000 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": 3000000000 + } } } })"; @@ -2183,10 +2226,12 @@ TEST_F(OperatorApiTest, MetadataTypeIdOutOfInt32RangeIsRejected) { conf << R"({ "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": -3000000000 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": -3000000000 + } } } })"; @@ -2206,10 +2251,12 @@ TEST_F(OperatorApiTest, MetadataTypeIdOutOfInt32RangeIsRejected) { conf << R"({ "data": { "pipe_path": "configs/pipeline_keyword_match_rules.json", - "mem_que": { - "type": "keyword_out", - "meta_num": 0, - "metadata_type_id": 1.5 + "outputs": { + "keyword_out": { + "type": "keyword_out", + "meta_num": 0, + "metadata_type_id": 1.5 + } } } })"; diff --git a/tests/integration/pipeline/test_pipeline_catalog_validator.cpp b/tests/integration/pipeline/test_pipeline_catalog_validator.cpp index 5214fbf7..835db1e8 100644 --- a/tests/integration/pipeline/test_pipeline_catalog_validator.cpp +++ b/tests/integration/pipeline/test_pipeline_catalog_validator.cpp @@ -683,7 +683,6 @@ TEST(PipelineValidatorTest, ExplainCapsVerificationAttemptsAtEight) { {"outputs", {{"output", "llm_raw_answer"}}}}}, {"config", {{"bind_model", "missing_model"}, - {"template_syntax", "standard"}, {"prompt_template", "Hello {{input}}"}, {"strip_markdown", true}, {"temperature", 0.1}, @@ -760,7 +759,6 @@ TEST(PipelineValidatorTest, ExplainRejectsInvalidModelCandidates) { {"outputs", {{"output", "llm_raw_answer"}}}}}, {"config", {{"bind_model", "missing_model"}, - {"template_syntax", "standard"}, {"prompt_template", "Hello {{input}}"}, {"strip_markdown", true}, {"temperature", 0.1}, diff --git a/tests/support/control_test_utils.h b/tests/support/control_test_utils.h index 0c110ccb..872b55b5 100644 --- a/tests/support/control_test_utils.h +++ b/tests/support/control_test_utils.h @@ -29,11 +29,12 @@ inline void WriteControlTestPipeline(const std::filesystem::path& directory) { const nlohmann::json conf = { {"data", {{"pipe_path", "pipeline.json"}, - {"mem_que", - {{"type", "keyword_out"}, - {"meta_num", 0}, - {"metadata_type_id", 0}, - {"capacities", {{"match_result_json", 2047}}}}}}}}; + {"outputs", + {{"keyword_out", + {{"type", "keyword_out"}, + {"meta_num", 0}, + {"metadata_type_id", 0}, + {"capacities", {{"match_result_json", 2047}}}}}}}}}}; std::ofstream(directory / "pipeline.json") << pipeline.dump(2); std::ofstream(directory / "pipeline.conf") << conf.dump(2); } diff --git a/tests/tooling/test_dev_recipe.py b/tests/tooling/test_dev_recipe.py index fa272760..c5ddf520 100644 --- a/tests/tooling/test_dev_recipe.py +++ b/tests/tooling/test_dev_recipe.py @@ -155,7 +155,22 @@ def test_external_destination_preserves_deployment_and_dataset_paths(self): def test_multi_output_rejected_before_generation(self): conf_path = self.root / "configs/pipeline_keyword_match_rules.conf" conf = json.loads(conf_path.read_text()) - conf["data"]["outputs"] = [{"name": "only_one_slot"}] + conf["data"]["outputs"] = {"slot1": {}, "slot2": {}} + conf_path.write_text(json.dumps(conf)) + self.assert_prepare_rejected_without_writes() + + def test_missing_outputs_deployment_rejected_without_writes(self): + conf_path = self.root / "configs/pipeline_keyword_match_rules.conf" + conf = json.loads(conf_path.read_text()) + del conf["data"]["outputs"] + conf_path.write_text(json.dumps(conf)) + self.assert_prepare_rejected_without_writes() + + def test_legacy_mem_que_deployment_rejected_as_missing_outputs(self): + conf_path = self.root / "configs/pipeline_keyword_match_rules.conf" + conf = json.loads(conf_path.read_text()) + del conf["data"]["outputs"] + conf["data"]["mem_que"] = {"type": "keyword_out"} conf_path.write_text(json.dumps(conf)) self.assert_prepare_rejected_without_writes() diff --git a/tests/tooling/test_pipeline_studio.py b/tests/tooling/test_pipeline_studio.py index 17dd3da4..27c6e6be 100644 --- a/tests/tooling/test_pipeline_studio.py +++ b/tests/tooling/test_pipeline_studio.py @@ -203,19 +203,7 @@ def tearDown(self): self.temporary.cleanup() def _valid_fingerprint(self): - if hasattr(self.service, "tool_fingerprint"): - try: - fp = self.service.tool_fingerprint - return fp() if callable(fp) else fp - except Exception: - pass - if hasattr(SHOW, "tool_fingerprint"): - try: - fp = SHOW.tool_fingerprint - return fp() if callable(fp) else fp - except Exception: - pass - return "valid_tool_fingerprint" + return self.service.get_tool_fingerprint() def test_preview_fix_requires_expected_revision(self): valid_fp = self._valid_fingerprint() @@ -326,7 +314,7 @@ def test_saved_pair_runs_the_selected_pipeline_with_explicit_arguments(self): self.assertEqual(conf["data"]["model_paths"], {}) command = shlex.split(saved["command"]) self.assertEqual(command[:3], ["cd", str(ROOT), "&&"]) - self.assertIn("--no-default-control", command) + self.assertNotIn("--no-default-control", command) self.assertFalse(Path(command[command.index("--config") + 1]).is_absolute()) output = Path(command[command.index("--output-dir") + 1]) try: @@ -517,9 +505,9 @@ def fail_conf(path, flags, mode=0o777): self.assertEqual(unrelated.read_text(), "keep") def test_native_deployment_rejection_rolls_back_the_pair(self): - profile, mem_que = self.service.profile_inputs(self.keyword, "keyword_match_rules") - mem_que["capacities"]["match_result_json"] = 0 - with mock.patch.object(self.service, "profile_inputs", return_value=(profile, mem_que)): + profile, outputs = self.service.profile_inputs(self.keyword, "keyword_match_rules") + outputs["keyword_out"]["capacities"]["match_result_json"] = 0 + with mock.patch.object(self.service, "profile_inputs", return_value=(profile, outputs)): with self.assertRaises(SHOW.StudioError) as error: self.service.save_solution("pipeline_invalid_pool.json", self.keyword, "keyword_match_rules") self.assertEqual(error.exception.code, "DEPLOYMENT_VALIDATION_FAILED") @@ -657,7 +645,7 @@ def test_resolve_conf_exposes_model_sources_defaults_and_native_pool_errors(self code, direct = self.command("resolve-conf", str(changed.relative_to(ROOT)), "--root", str(ROOT)) self.assertEqual(code, 0, direct) self.assertEqual(direct["configuration"]["model_paths"][0]["source"], "pipeline.models.model_path") - conf["data"]["mem_que"]["capacities"]["entities_json"] = 0 + conf["data"]["outputs"]["entity_out"]["capacities"]["entities_json"] = 0 changed.write_text(json.dumps(conf)) code, rejected = self.command("resolve-conf", str(changed.relative_to(ROOT)), "--root", str(ROOT)) self.assertEqual(code, 1) diff --git a/tests/unit/nodes/test_common_nodes.cpp b/tests/unit/nodes/test_common_nodes.cpp index cf42385a..f0bcc9b0 100644 --- a/tests/unit/nodes/test_common_nodes.cpp +++ b/tests/unit/nodes/test_common_nodes.cpp @@ -744,7 +744,7 @@ TEST_F(CommonNodesTest, TextTemplateNodeMissingVariableFail) { ASSERT_NE(node, nullptr); // allow_dynamic_attributes is false by default - nlohmann::json cfg = {{"template", "Hello {user_name}, welcome!"}, + nlohmann::json cfg = {{"template", "Hello {{user_name}}, welcome!"}, {"allow_dynamic_attributes", true}}; EXPECT_TRUE(InitNodeForTest(*node, cfg, session_ctx_.get())); @@ -1050,8 +1050,8 @@ TEST_F(CommonNodesTest, PromptRendersOriginalTemplateAndIsolatesRequests) { const nlohmann::json config = { {"bind_model", "prompt_contract"}, {"prompt_prefix", "system {input}"}, - {"template_syntax", "legacy"}, - {"prompt_template", "{{literal}} <{input}>|{context}|{input}"}, + {"prompt_template", + "{\"key\": \"val\"} <{{input}}>|{{context}}|{{input}}"}, {"strip_markdown", true}, {"stop_words", {"END"}}, {"max_tokens", 23}}; @@ -1069,12 +1069,12 @@ TEST_F(CommonNodesTest, PromptRendersOriginalTemplateAndIsolatesRequests) { ASSERT_NE(output, nullptr); ASSERT_EQ(output->size(), 3U); EXPECT_EQ((*output)[0].data, - "system {input}\n{literal} |C{input}\nTAIL|literal {context}"); EXPECT_EQ((*output)[1].data, - "system {input}\n{literal} |OTHER|second"); + "system {input}\n{\"key\": \"val\"} |OTHER|second"); EXPECT_EQ((*output)[2].data, - "system {input}\n{literal} |C{input}\nTAIL|last"); + "system {input}\n{\"key\": \"val\"} |C{input}\nTAIL|last"); EXPECT_EQ((*output)[0].req_id, 17U); EXPECT_EQ((*output)[2].sub_id, 6U); EXPECT_EQ(model->last_options.max_tokens, 23); @@ -1085,7 +1085,7 @@ TEST_F(CommonNodesTest, PromptRendersOriginalTemplateAndIsolatesRequests) { next.Publish("context", TextBatch{}); ASSERT_EQ(node->Process(&next), 0); EXPECT_EQ(next.Read("output")->front().data, - "system {input}\n{literal} ||next"); + "system {input}\n{\"key\": \"val\"} ||next"); } TEST_F(CommonNodesTest, PromptDefaultsMatchDirectInitializationAndNativePlan) { @@ -1139,7 +1139,7 @@ TEST_F(CommonNodesTest, PromptStandardSyntaxMatchesTextTemplateNode) { model, "v1")); const std::string value = "opaque {{input}} {context}"; const std::vector> cases = { - {"{{context}}|{ context }", value + "|" + value}, + {"{{context}}|{{ context }}", value + "|" + value}, {R"({"nested":{"context":"{{ context }}"}})", "{\"nested\":{\"context\":\"" + value + "\"}}"}}; for (const auto& [pattern, expected] : cases) { @@ -1148,11 +1148,10 @@ TEST_F(CommonNodesTest, PromptStandardSyntaxMatchesTextTemplateNode) { auto custom = NodeRegistry::Instance().Create("PromptGuidedLlmNode"); ASSERT_TRUE( InitNodeForTest(*common, {{"template", pattern}}, session_ctx_.get())); - ASSERT_TRUE(InitNodeForTest(*custom, - {{"bind_model", "prompt_contract"}, - {"template_syntax", "standard"}, - {"prompt_template", pattern}}, - session_ctx_.get())); + ASSERT_TRUE(InitNodeForTest( + *custom, + {{"bind_model", "prompt_contract"}, {"prompt_template", pattern}}, + session_ctx_.get())); AlgContext common_ctx; common_ctx.Publish("context_text", TextBatch{{17, 0, value}}); ASSERT_EQ(common->Process(&common_ctx), 0); @@ -1169,47 +1168,41 @@ TEST_F(CommonNodesTest, PromptStandardSyntaxMatchesTextTemplateNode) { } } -TEST_F(CommonNodesTest, PromptDoubleBracesRequireExplicitMigration) { +TEST_F(CommonNodesTest, + PromptTemplateUnificationAcceptsJsonAndRejectsMalformedPlaceholders) { auto model = std::make_shared(); ASSERT_TRUE(session_ctx_->GetModelManager().RegisterModel("prompt_contract", model, "v1")); + // 1. Malformed templates are rejected at validation and Init for (const std::string pattern : - {"{{input}}", "{{context}}", "{{literal}}", "{{\"value\": 1}}", "}}"}) { + {"{{unclosed", "{{unknown}}", "{{}}", "{{invalid name}}"}) { SCOPED_TRACE(pattern); - for (bool explicit_auto : {false, true}) { - auto doc = CustomPipeline("entity_extract"); - auto& config = doc["pipeline"][0]["config"]; - config["prompt_template"] = pattern; - config.erase("template_syntax"); - if (explicit_auto) config["template_syntax"] = "auto"; - const auto result = PipelineValidator::ValidateAndPlan(doc); - EXPECT_FALSE(result.report.ok); - EXPECT_NE(result.report.ToJson().dump().find("template_syntax=standard"), - std::string::npos); - EXPECT_NE(result.report.ToJson().dump().find("template_syntax=legacy"), - std::string::npos); - config["bind_model"] = "prompt_contract"; - auto node = NodeRegistry::Instance().Create("PromptGuidedLlmNode"); - EXPECT_FALSE(InitNodeForTest(*node, config, session_ctx_.get())); - } + auto doc = CustomPipeline("entity_extract"); + auto& config = doc["pipeline"][0]["config"]; + config["prompt_template"] = pattern; + const auto result = PipelineValidator::ValidateAndPlan(doc); + EXPECT_FALSE(result.report.ok); + config["bind_model"] = "prompt_contract"; + auto node = NodeRegistry::Instance().Create("PromptGuidedLlmNode"); + EXPECT_FALSE(InitNodeForTest(*node, config, session_ctx_.get())); } - for (const auto& [syntax, pattern, expected] : - std::vector>{ - {"auto", "{input}", "value"}, - {"standard", "{{ input }}|{input}", "value|value"}, - {"legacy", "{{input}}|{input}|{{\"value\": 1}}", - "{input}|value|{\"value\": 1}"}}) { - SCOPED_TRACE(syntax); + + // 2. Valid templates with JSON literal braces and {{input}} substitution + for (const auto& [pattern, expected] : + std::vector>{ + {"{\"text\": \"{{input}}\"}", "{\"text\": \"value\"}"}, + {"{{ input }}|{literal_braces}", "value|{literal_braces}"}, + {"prefix: {{input}} | suffix", "prefix: value | suffix"}}) { + SCOPED_TRACE(pattern); auto node = NodeRegistry::Instance().Create("PromptGuidedLlmNode"); - ASSERT_TRUE(InitNodeForTest(*node, - {{"bind_model", "prompt_contract"}, - {"template_syntax", syntax}, - {"prompt_template", pattern}}, - session_ctx_.get())); + ASSERT_TRUE(InitNodeForTest( + *node, + {{"bind_model", "prompt_contract"}, {"prompt_template", pattern}}, + session_ctx_.get())); AlgContext ctx; ctx.Publish("input", TextBatch{{17, 4, "value"}}); ASSERT_EQ(node->Process(&ctx), 0); - EXPECT_EQ(model->prompts.front().data, expected); + EXPECT_EQ(model->prompts.back().data, expected); } } @@ -1262,7 +1255,7 @@ TEST_F(CommonNodesTest, PromptContextIsExplicitAndRequiredWhenUsed) { EXPECT_EQ(model->prompts.front().data, "input only"); ASSERT_TRUE(InitNodeForTest(*node, {{"bind_model", "prompt_contract"}, - {"prompt_template", "{input}|{context}"}}, + {"prompt_template", "{{input}}|{{context}}"}}, session_ctx_.get())); for (bool wrong_type : {false, true}) { AlgContext missing; @@ -1291,20 +1284,16 @@ TEST_F(CommonNodesTest, PromptConfigurationRejectedByValidatorAndInit) { {{"top_k", -1}}, {{"top_p", 0}}, {{"repetition_penalty", 101}}, - {{"prompt_template", "{unknown}"}}, - {{"prompt_template", "{input"}}, - {{"prompt_template", "}"}}, {{"prompt_template", ""}}, - {{"template_syntax", "invalid"}}, - {{"template_syntax", "standard"}, {"prompt_template", "{{unknown}}"}}, - {{"template_syntax", "standard"}, {"prompt_template", "{{input}"}}, - {{"template_syntax", "standard"}, {"prompt_template", "{{}}"}}}; + {{"prompt_template", "{{unknown}}"}}, + {{"prompt_template", "{{input"}}, + {{"prompt_template", "{{}}"}}, + {{"template_syntax", "standard"}}}; for (const auto& bad : bad_configs) { SCOPED_TRACE(bad.dump()); auto doc = CustomPipeline("entity_extract"); nlohmann::json config = {{"bind_model", "entity_llm"}, - {"prompt_template", "{input}"}, - {"template_syntax", "auto"}}; + {"prompt_template", "{{input}}"}}; config.update(bad); doc["pipeline"][0]["config"] = config; const auto preflight = PipelineValidator::ValidateAndPlan(doc); diff --git a/tests/unit/nodes/test_text_template_node.cpp b/tests/unit/nodes/test_text_template_node.cpp index 8784fe59..56f03535 100644 --- a/tests/unit/nodes/test_text_template_node.cpp +++ b/tests/unit/nodes/test_text_template_node.cpp @@ -14,6 +14,7 @@ #include "core/common_contracts.h" #include "core/node_registry.h" #include "core/pipeline.h" +#include "core/pipeline_catalog.h" #include "core/session_context.h" #include "tests/support/node_process_pause.h" #include "tests/support/node_test_utils.h" @@ -79,7 +80,7 @@ TEST_F(TextTemplateNodeTest, MissingRequiredVariableFailsClosed) { auto node = NodeRegistry::Instance().Create("TextTemplateNode"); ASSERT_NE(node, nullptr); - nlohmann::json cfg = {{"template", "Hello {user_name}, welcome!"}, + nlohmann::json cfg = {{"template", "Hello {{user_name}}, welcome!"}, {"allow_dynamic_attributes", true}, {"missing_variable_policy", "fail"}}; EXPECT_TRUE(InitNodeForTest(*node, cfg, session_ctx_.get())); @@ -100,7 +101,7 @@ TEST_F(TextTemplateNodeTest, DynamicAttributeRendered) { auto node = NodeRegistry::Instance().Create("TextTemplateNode"); ASSERT_NE(node, nullptr); - nlohmann::json cfg = {{"template", "Hello {user_name}, welcome!"}, + nlohmann::json cfg = {{"template", "Hello {{user_name}}, welcome!"}, {"allow_dynamic_attributes", true}}; EXPECT_TRUE(InitNodeForTest(*node, cfg, session_ctx_.get())); @@ -117,6 +118,46 @@ TEST_F(TextTemplateNodeTest, DynamicAttributeRendered) { EXPECT_EQ((*out)[0].data, "Hello Alice, welcome!"); } +// 4. Single Braces Treated As Literal Text +TEST_F(TextTemplateNodeTest, SingleBraceTreatedAsLiteral) { + auto node = NodeRegistry::Instance().Create("TextTemplateNode"); + ASSERT_NE(node, nullptr); + + // Single braces like {user_name} and JSON objects {"k": 1} must remain + // literal text. Only double braces {{var}} are treated as template variables. + nlohmann::json cfg = { + {"template", + "Literal: {user_name}, JSON: {\"key\": 1}, Var: {{user_name}}"}, + {"allow_dynamic_attributes", true}}; + EXPECT_TRUE(InitNodeForTest(*node, cfg, session_ctx_.get())); + + AlgContext ctx; + TextAttributesBatch attrs; + attrs.emplace_back( + 1, 0, + std::unordered_map{{"user_name", "Alice"}}); + ctx.Publish("attributes", attrs); + + EXPECT_EQ(node->Process(&ctx), 0); + const auto* out = ctx.Read("text"); + ASSERT_NE(out, nullptr); + EXPECT_EQ((*out)[0].data, + "Literal: {user_name}, JSON: {\"key\": 1}, Var: Alice"); +} + +// 5. Malformed Placeholders Rejected At Initialization +TEST_F(TextTemplateNodeTest, MalformedPlaceholderFailsInit) { + auto node = NodeRegistry::Instance().Create("TextTemplateNode"); + ASSERT_NE(node, nullptr); + + for (const std::string bad_tmpl : + {"Hello {{unclosed", "Hello {{}}", "Hello {{invalid name}}"}) { + nlohmann::json cfg = {{"template", bad_tmpl}, + {"allow_dynamic_attributes", true}}; + EXPECT_FALSE(InitNodeForTest(*node, cfg, session_ctx_.get())); + } +} + TEST_F(TextTemplateNodeTest, TruncatePreservesUtf8CodePointBoundaries) { auto node = NodeRegistry::Instance().Create("TextTemplateNode"); ASSERT_NE(node, nullptr); @@ -269,8 +310,9 @@ TEST_F(TextTemplateNodeTest, UnconnectedBuiltinUsesDeclaredMissingPolicy) { ctx.Publish("input_sentences", TextBatch{{1, 3, "hello"}}); ASSERT_EQ(pipeline.Execute(&ctx), 0); ASSERT_NE(ctx.Read("rendered_text"), nullptr); - EXPECT_EQ(ctx.Read("rendered_text")->front().data, - "Q=hello|V=" + (policy == "empty" ? "" : "{" + variable + "}")); + EXPECT_EQ( + ctx.Read("rendered_text")->front().data, + "Q=hello|V=" + (policy == "empty" ? "" : "{{" + variable + "}}")); } } } @@ -421,4 +463,17 @@ TEST_F(TextTemplateNodeTest, DirectConcurrentProcessAndControl) { expect_batch(subsequent, "NEW"); } +TEST_F(TextTemplateNodeTest, + CatalogDefinitionStatesOnlyDoubleBracesSubstitute) { + auto def_opt = PipelineCatalog::FindNode("TextTemplateNode"); + ASSERT_TRUE(def_opt.has_value()); + const auto& def = *def_opt; + EXPECT_NE(def.description.find("{{name}} substitutes variables"), + std::string::npos); + EXPECT_NE( + def.description.find("single {name} and JSON braces remain literal"), + std::string::npos); + EXPECT_EQ(def.description.find("and {name} substitute"), std::string::npos); +} + } // namespace llm_edgeflow diff --git a/tests/unit/operator/test_operator_biz_bridge_registry.cpp b/tests/unit/operator/test_operator_biz_bridge_registry.cpp index 0c152627..4859ecd2 100644 --- a/tests/unit/operator/test_operator_biz_bridge_registry.cpp +++ b/tests/unit/operator/test_operator_biz_bridge_registry.cpp @@ -6,6 +6,7 @@ #include "adapter/biz_adapter_registry.h" #include "adapter/biz_results.h" #include "adapter/operator/operator_biz_bridge_registry.h" +#include "adapter/operator/operator_process_binding.h" #include "adapter/operator/operator_value_type_registry.h" #include "adapter/text_carrier.h" @@ -52,8 +53,8 @@ TEST(OperatorBizBridgeRegistryTest, AllRegisteredBusinessesSelfRegistered) { EXPECT_FALSE(desc->output_slots.empty()); EXPECT_NE(desc->convert_sample_input, nullptr); for (const auto& slot : desc->output_slots) { - EXPECT_TRUE(slot.convert_output || (desc->output_slots.size() == 1 && - desc->convert_sample_output)); + EXPECT_NE(slot.convert_output, nullptr); + EXPECT_FALSE(slot.key_suffix.empty()); } EXPECT_NE(desc->create_shadow_output_dto, nullptr); } @@ -230,14 +231,14 @@ TEST(OperatorBizBridgeRegistryTest, EXPECT_EQ(r.GlobalInit(), -6); } - // 2. Only convert_sample_output changed -> conflict + // 2. Only convert_output changed -> conflict { OperatorBizBridgeRegistry r; EXPECT_TRUE(r.RegisterBridge(*orig_desc)); OperatorBizBridgeDescriptor conflict = *orig_desc; - conflict.convert_sample_output = [](const void*, void*, - const ResolvedOutputPoolSpec&, - std::string*) -> int { return -99; }; + conflict.output_slots.front().convert_output = + [](const void*, void*, const ResolvedOutputPoolSpec&, + std::string*) -> int { return -99; }; EXPECT_FALSE(r.RegisterBridge(conflict)); EXPECT_TRUE(r.HasConflict()); EXPECT_EQ(r.GlobalInit(), -6); @@ -312,7 +313,8 @@ TEST(OperatorBizBridgeRegistryTest, CompanyOperatorKeywordOutput out_struct{}; out_struct.match_result_json = &out_cs; - ASSERT_EQ(desc->convert_sample_output(&out_dto, &out_struct, spec, &err), + ASSERT_EQ(desc->output_slots.front().convert_output(&out_dto, &out_struct, + spec, &err), 0); EXPECT_EQ(out_struct.request_id, 1000 + i); EXPECT_EQ(out_struct.is_hit, out_dto.is_hit); @@ -359,7 +361,8 @@ TEST(OperatorBizBridgeRegistryTest, CompanyOperatorEntityOutput out_struct{}; out_struct.entities_json = &out_cs; - ASSERT_EQ(desc->convert_sample_output(&out_dto, &out_struct, spec, &err), + ASSERT_EQ(desc->output_slots.front().convert_output(&out_dto, &out_struct, + spec, &err), 0); EXPECT_EQ(out_struct.request_id, 2000 + i); EXPECT_STREQ(out_struct.entities_json->data, @@ -407,7 +410,8 @@ TEST(OperatorBizBridgeRegistryTest, out_struct.intent_name = &intent_cs; out_struct.answer_text = &answer_cs; - ASSERT_EQ(desc->convert_sample_output(&out_dto, &out_struct, spec, &err), + ASSERT_EQ(desc->output_slots.front().convert_output(&out_dto, &out_struct, + spec, &err), 0); EXPECT_EQ(out_struct.request_id, 3000 + i); EXPECT_FLOAT_EQ(out_struct.confidence, 0.95f); @@ -459,7 +463,8 @@ TEST(OperatorBizBridgeRegistryTest, out_struct.matched_policy_clause = &p_cs; out_struct.audit_verdict_json = &v_cs; - ASSERT_EQ(desc->convert_sample_output(&out_dto, &out_struct, spec, &err), + ASSERT_EQ(desc->output_slots.front().convert_output(&out_dto, &out_struct, + spec, &err), 0); EXPECT_EQ(out_struct.request_id, 4000 + i); EXPECT_STREQ(out_struct.risk_level->data, "SAFE"); @@ -512,7 +517,8 @@ TEST(OperatorBizBridgeRegistryTest, out_struct.transcribed_text = &t_cs; out_struct.intent_slot_json = &slot_cs; - ASSERT_EQ(desc->convert_sample_output(&out_dto, &out_struct, spec, &err), + ASSERT_EQ(desc->output_slots.front().convert_output(&out_dto, &out_struct, + spec, &err), 0); EXPECT_EQ(out_struct.request_id, 5000 + i); EXPECT_STREQ(out_struct.transcribed_text->data, @@ -571,7 +577,8 @@ TEST(OperatorBizBridgeRegistryTest, } CompanyOperatorRerankOutput out_struct{}; - ASSERT_EQ(desc->convert_sample_output(&out_dto, &out_struct, spec, &err), + ASSERT_EQ(desc->output_slots.front().convert_output(&out_dto, &out_struct, + spec, &err), 0); EXPECT_EQ(out_struct.request_id, 6000 + i); EXPECT_EQ(out_struct.count, 8); @@ -621,7 +628,8 @@ TEST(OperatorBizBridgeRegistryTest, CompanyOdOutput out_struct{}; out_struct.result_json = &res_cs; - ASSERT_EQ(desc->convert_sample_output(&out_dto, &out_struct, spec, &err), + ASSERT_EQ(desc->output_slots.front().convert_output(&out_dto, &out_struct, + spec, &err), 0); EXPECT_EQ(out_struct.request_id, 7000 + i); EXPECT_EQ(out_struct.detected_box_count, out_dto.detected_box_count); @@ -658,12 +666,73 @@ TEST(OperatorBizBridgeRegistryTest, descriptor.input_slots.front().key_suffix = "output_only"; if (mutation == 1) descriptor.input_slots.front().convert_output = - descriptor.convert_sample_output; + descriptor.output_slots.front().convert_output; EXPECT_FALSE(invalid.RegisterBridge(descriptor)); EXPECT_TRUE(invalid.HasConflict()); } } +TEST(OperatorBizBridgeRegistryTest, + IsolatedRegistryRejectsAdapterNameMismatch) { + OperatorBizBridgeRegistry local_reg; + const auto& global_reg = OperatorBizBridgeRegistry::Instance(); + for (const auto biz_type : RegisteredBizTypes()) { + const auto* desc = global_reg.GetBridge(biz_type); + ASSERT_NE(desc, nullptr); + OperatorBizBridgeDescriptor d = *desc; + if (biz_type == ALG_BIZ_TYPE_DOC_QA) { + const auto adapter = BizAdapterRegistry::Instance().GetAdapter(biz_type); + ASSERT_NE(adapter, nullptr); + ASSERT_FALSE(adapter->GetDescriptor().biz_definitions.empty()); + const std::string declared_biz_name = + adapter->GetDescriptor().biz_definitions.front().biz_name; + ASSERT_EQ(declared_biz_name, "smart_doc_qa_v1"); + // Formerly accepted via fallback to declared Pipeline biz_name; must now + // be strictly rejected to enforce 1:1 AdapterName matching. + d.adapter_name = declared_biz_name; + } + EXPECT_TRUE(local_reg.RegisterBridge(d)); + } + std::string diagnostic; + EXPECT_EQ(local_reg.GlobalInit(&diagnostic), -6); + EXPECT_NE(diagnostic.find("does not match BizAdapter"), std::string::npos); + EXPECT_NE(diagnostic.find("smart_doc_qa_v1"), std::string::npos); + EXPECT_NE(diagnostic.find("'DocQA'"), std::string::npos); +} + +TEST( + OperatorBizBridgeRegistryTest, + IsolatedRegistryRejectsDeclaredPipelineBizNameAsAdapterNameForAllBizTypes) { + const auto& global_reg = OperatorBizBridgeRegistry::Instance(); + for (const auto biz_type : RegisteredBizTypes()) { + const auto* orig_desc = global_reg.GetBridge(biz_type); + ASSERT_NE(orig_desc, nullptr); + const auto adapter = BizAdapterRegistry::Instance().GetAdapter(biz_type); + ASSERT_NE(adapter, nullptr); + ASSERT_FALSE(adapter->GetDescriptor().biz_definitions.empty()); + const std::string declared_biz_name = + adapter->GetDescriptor().biz_definitions.front().biz_name; + ASSERT_NE(declared_biz_name, adapter->AdapterName()); + + OperatorBizBridgeRegistry local_reg; + for (const auto b : RegisteredBizTypes()) { + auto d = *global_reg.GetBridge(b); + if (b == biz_type) { + d.adapter_name = declared_biz_name; + } + ASSERT_TRUE(local_reg.RegisterBridge(d)); + } + std::string diagnostic; + EXPECT_EQ(local_reg.GlobalInit(&diagnostic), -6); + EXPECT_NE(diagnostic.find(std::string("does not match BizAdapter '") + + adapter->AdapterName() + "'"), + std::string::npos) + << "Failed for biz_type " << static_cast(biz_type) << ": " + << diagnostic; + EXPECT_NE(diagnostic.find(declared_biz_name), std::string::npos); + } +} + TEST(OperatorBizBridgeRegistryTest, MultipleOutputSlotsMayReuseTypeWithDistinctKeysAndConverters) { OperatorBizBridgeRegistry local_reg; @@ -672,12 +741,9 @@ TEST(OperatorBizBridgeRegistryTest, ASSERT_NE(orig_desc, nullptr); OperatorBizBridgeDescriptor descriptor = *orig_desc; - descriptor.output_slots.front().convert_output = - descriptor.convert_sample_output; descriptor.output_slots.push_back(descriptor.output_slots.front()); descriptor.output_slots.back().logical_name = "secondary_output"; descriptor.output_slots.back().key_suffix = "secondary"; - descriptor.convert_sample_output = nullptr; ASSERT_TRUE(local_reg.RegisterBridge(descriptor)); EXPECT_FALSE(local_reg.HasConflict()); ASSERT_NE(local_reg.GetBridge(descriptor.biz_type), nullptr); @@ -816,7 +882,8 @@ TEST(OperatorBizBridgeRegistryTest, TextCarrierSharedBridgeAndTypedBuilder) { CompanyOperatorEntityOutput out_struct{}; out_struct.entities_json = &out_cs; - ASSERT_EQ(desc->convert_sample_output(&out_dto, &out_struct, spec, &err), + ASSERT_EQ(desc->output_slots.front().convert_output(&out_dto, &out_struct, + spec, &err), 0); EXPECT_EQ(out_struct.request_id, 4242u); EXPECT_EQ(out_struct.status_code, 0); @@ -850,8 +917,10 @@ TEST(OperatorBizBridgeRegistryTest, << diagnostic; } -// RFC-0053: Typed builder supports custom logical input slot names -TEST(OperatorBizBridgeRegistryTest, TypedBuilderSupportsCustomInputSlotName) { +// RFC-0053 / RFC-0056: Typed builder supports custom logical slot names while +// preserving canonical output key_suffix and supporting explicit key overrides. +TEST(OperatorBizBridgeRegistryTest, + TypedBuilderSupportsCustomSlotNamesAndPreservesCanonicalOutputKey) { auto desc = MakeTypedSingleSlotBizBridge< CompanyEntityInputStruct, EntityResult, CompanyOperatorEntityInput, CompanyOperatorEntityOutput, &ConvertTextCarrierInput, @@ -861,6 +930,14 @@ TEST(OperatorBizBridgeRegistryTest, TypedBuilderSupportsCustomInputSlotName) { EXPECT_EQ(desc.input_slots.front().logical_name, "custom_in"); EXPECT_EQ(desc.input_slots.front().type_suffix, "entity_in"); + EXPECT_TRUE(desc.input_slots.front().key_suffix.empty()); + + // F1 Regression Guard: logical_name is custom_out, but type_suffix and + // key_suffix must remain the canonical host output suffix ("entity_out"). + EXPECT_EQ(desc.output_slots.front().logical_name, "custom_out"); + EXPECT_EQ(desc.output_slots.front().type_suffix, "entity_out"); + EXPECT_EQ(desc.output_slots.front().key_suffix, "entity_out"); + EXPECT_EQ(desc.output_slots.front().KeySuffix(), "entity_out"); ProcessLocalShadowStorage storage; std::string text = "custom slot query"; @@ -892,6 +969,139 @@ TEST(OperatorBizBridgeRegistryTest, TypedBuilderSupportsCustomInputSlotName) { // 3. When out_internal_dto is null, it converts successfully without // dereferencing null EXPECT_EQ(desc.convert_sample_input(slots_custom, storage, nullptr, &err), 0); + + // 4. Output sample conversion succeeds via slot convert_output + ASSERT_NE(desc.output_slots.front().convert_output, nullptr); + EntityResult out_dto{8888, "{\"status\":\"ok\"}", 0}; + char out_buf[128] = {0}; + CompanyString out_cs{0, out_buf}; + CompanyOperatorEntityOutput out_struct{}; + out_struct.entities_json = &out_cs; + ResolvedOutputPoolSpec pool_spec = MakeDefaultOutputPoolSpec("entity_out"); + ASSERT_EQ(desc.output_slots.front().convert_output(&out_dto, &out_struct, + pool_spec, &err), + 0); + EXPECT_EQ(out_struct.request_id, 8888u); + EXPECT_STREQ(out_struct.entities_json->data, "{\"status\":\"ok\"}"); + + // 5. Output binding resolution: external map key uses canonical KeySuffix() + // ("entity_out") and binds to the custom logical slot ("custom_out"). + { + operator_api::NamedIoBatch outputs(1); + outputs[0]["req_0.entity_out"] = std::shared_ptr(); + std::vector> bindings; + std::string bind_err; + EXPECT_EQ(ResolveOperatorOutputs(outputs, desc, &bindings, &bind_err), 0); + ASSERT_EQ(bindings.size(), 1u); + ASSERT_EQ(bindings[0].size(), 1u); + EXPECT_EQ(bindings[0][0].key, "req_0.entity_out"); + EXPECT_EQ(bindings[0][0].logical_name, "custom_out"); + } + + // 6. External map key with custom logical slot name fails to match + // KeySuffix() + { + operator_api::NamedIoBatch mismatched_outputs(1); + mismatched_outputs[0]["req_0.custom_out"] = std::shared_ptr(); + std::vector> bindings; + std::string bind_err; + EXPECT_EQ( + ResolveOperatorOutputs(mismatched_outputs, desc, &bindings, &bind_err), + -4); + } + + // 7. Overload 2 (non-template function pointer) also sets canonical + // key_suffix + ConvertSampleInputFn dummy_in_fn = + [](const std::unordered_map&, + ProcessLocalShadowStorage&, const void**, + std::string*) -> int { return 0; }; + ConvertSampleOutputFn dummy_out_fn = [](const void*, void*, + const ResolvedOutputPoolSpec&, + std::string*) -> int { return 0; }; + auto desc2 = + MakeTypedSingleSlotBizBridge( + ALG_BIZ_TYPE_ENTITY_EXTRACT, "EntityExtract", + "CompanyEntityInputStruct", "test.custom_slot2", "custom_in", + "custom_out", dummy_in_fn, dummy_out_fn); + EXPECT_EQ(desc2.output_slots.front().logical_name, "custom_out"); + EXPECT_EQ(desc2.output_slots.front().type_suffix, "entity_out"); + EXPECT_EQ(desc2.output_slots.front().key_suffix, "entity_out"); + EXPECT_EQ(desc2.output_slots.front().KeySuffix(), "entity_out"); + + // 8. Explicit custom external key override is preserved and respected + desc.output_slots.front().key_suffix = "custom_external_out"; + EXPECT_EQ(desc.output_slots.front().KeySuffix(), "custom_external_out"); + { + operator_api::NamedIoBatch custom_key_outputs(1); + custom_key_outputs[0]["req_0.custom_external_out"] = + std::shared_ptr(); + std::vector> bindings; + std::string bind_err; + EXPECT_EQ( + ResolveOperatorOutputs(custom_key_outputs, desc, &bindings, &bind_err), + 0); + ASSERT_EQ(bindings.size(), 1u); + ASSERT_EQ(bindings[0].size(), 1u); + EXPECT_EQ(bindings[0][0].key, "req_0.custom_external_out"); + EXPECT_EQ(bindings[0][0].logical_name, "custom_out"); + } + + // 9. MakeSingleSlotBizBridge supports explicit output_key_suffix parameter + auto desc_single_param = MakeSingleSlotBizBridge( + ALG_BIZ_TYPE_ENTITY_EXTRACT, "EntityExtract", "CompanyEntityInputStruct", + "test.single_param", "custom_in", "custom_out", "single_custom_key"); + EXPECT_EQ(desc_single_param.output_slots.front().logical_name, "custom_out"); + EXPECT_EQ(desc_single_param.output_slots.front().key_suffix, + "single_custom_key"); + EXPECT_EQ(desc_single_param.output_slots.front().KeySuffix(), + "single_custom_key"); + + // 10. MakeTypedSingleSlotBizBridge (Overload 1) supports explicit + // output_key_suffix parameter and binds correctly + auto desc_typed_param = MakeTypedSingleSlotBizBridge< + CompanyEntityInputStruct, EntityResult, CompanyOperatorEntityInput, + CompanyOperatorEntityOutput, &ConvertTextCarrierInput, + &ConvertTextCarrierOutput>(ALG_BIZ_TYPE_ENTITY_EXTRACT, "EntityExtract", + "CompanyEntityInputStruct", "test.typed_param", + "custom_in", "custom_out", "direct_param_key"); + EXPECT_EQ(desc_typed_param.output_slots.front().logical_name, "custom_out"); + EXPECT_EQ(desc_typed_param.output_slots.front().type_suffix, "entity_out"); + EXPECT_EQ(desc_typed_param.output_slots.front().key_suffix, + "direct_param_key"); + EXPECT_EQ(desc_typed_param.output_slots.front().KeySuffix(), + "direct_param_key"); + { + operator_api::NamedIoBatch param_outputs(1); + param_outputs[0]["req_0.direct_param_key"] = std::shared_ptr(); + std::vector> bindings; + std::string bind_err; + EXPECT_EQ(ResolveOperatorOutputs(param_outputs, desc_typed_param, &bindings, + &bind_err), + 0); + ASSERT_EQ(bindings.size(), 1u); + ASSERT_EQ(bindings[0].size(), 1u); + EXPECT_EQ(bindings[0][0].key, "req_0.direct_param_key"); + EXPECT_EQ(bindings[0][0].logical_name, "custom_out"); + } + + // 11. MakeTypedSingleSlotBizBridge (Overload 2) supports explicit + // output_key_suffix parameter + auto desc2_typed_param = + MakeTypedSingleSlotBizBridge( + ALG_BIZ_TYPE_ENTITY_EXTRACT, "EntityExtract", + "CompanyEntityInputStruct", "test.typed2_param", "custom_in", + "custom_out", dummy_in_fn, dummy_out_fn, "direct_param2_key"); + EXPECT_EQ(desc2_typed_param.output_slots.front().logical_name, "custom_out"); + EXPECT_EQ(desc2_typed_param.output_slots.front().type_suffix, "entity_out"); + EXPECT_EQ(desc2_typed_param.output_slots.front().key_suffix, + "direct_param2_key"); + EXPECT_EQ(desc2_typed_param.output_slots.front().KeySuffix(), + "direct_param2_key"); } } // namespace diff --git a/tools/pipeline_studio/README.md b/tools/pipeline_studio/README.md index 0fa82ca6..cecc1d03 100644 --- a/tools/pipeline_studio/README.md +++ b/tools/pipeline_studio/README.md @@ -184,14 +184,14 @@ Pipeline JSON 描述算法连线;`.conf` 描述部署路径和输出容量;P ```bash ./build/alg_pipeline_tool validate configs/pipeline_first_solution.json ./build/alg_pipeline_tool plan configs/pipeline_first_solution.json -./build/alg_demo --profile keyword_match_rules --config configs/pipeline_first_solution.conf --no-default-control --output-dir results/first-solution +./build/alg_demo --profile keyword_match_rules --config configs/pipeline_first_solution.conf --output-dir results/first-solution ``` CLI 的 `--config` 覆盖 Profile 原配置,因此不需要新增 Profile。也可以不带 Profile, 显式传入业务、配置和数据集: ```bash -./build/alg_demo --biz keyword_match --config configs/pipeline_first_solution.conf --dataset data/corpus_keyword_match.txt --chip cpu --batch-size 2 --no-default-control --output-dir results/first-solution +./build/alg_demo --biz keyword_match --config configs/pipeline_first_solution.conf --dataset data/corpus_keyword_match.txt --chip cpu --batch-size 2 --output-dir results/first-solution ``` 只有原 Profile 已指向本次方案时,才能直接用它证明本次修改已运行。 @@ -201,7 +201,7 @@ CLI 的 `--config` 覆盖 Profile 原配置,因此不需要新增 Profile。 和业务字段,不只看退出码。无 Profile 运行时,结果子目录改为业务名 `keyword_match`。 复用其他 `.conf` 时,还要核对 `data.model_paths` 的模型路径覆盖和输出池容量是否适合 -当前方案;Pipeline 校验不代表部署资源可加载。`--no-default-control` 是兼容选项;Demo 默认不发送内置 +当前方案;Pipeline 校验不代表部署资源可加载。Demo 默认不发送内置 热更新覆盖所选规则或提示词,显式 `--control-file` 仍会执行,应只在需要该更新时提供。 新增节点命令可用 `--control-cmd --control-file ` 经同一 Demo 下发; diff --git a/tools/pipeline_studio/server.py b/tools/pipeline_studio/server.py index 46189573..31d9e7e6 100755 --- a/tools/pipeline_studio/server.py +++ b/tools/pipeline_studio/server.py @@ -181,9 +181,6 @@ def get_tool_fingerprint() -> str: return hashlib.sha256(str(PIPELINE_TOOL).encode()).hexdigest()[:16] -tool_fingerprint = get_tool_fingerprint - - class WorkbenchService: """State and filesystem boundary behind /api/v1.""" @@ -322,9 +319,6 @@ def profiles(self) -> dict[str, Any]: def get_tool_fingerprint(self) -> str: return get_tool_fingerprint() - def tool_fingerprint(self) -> str: - return self.get_tool_fingerprint() - def validate(self, pipeline: Any, explain: bool = False) -> dict[str, Any]: args = ["validate", "--stdin"] if explain: @@ -472,7 +466,7 @@ def check_revisions() -> tuple[bytes, bytes]: old_json, _ = check_revisions() profile = managed["profile"] encoded = (json.dumps(pipeline, ensure_ascii=False, indent=2) + "\n").encode() - conf = self.run_conf(pipeline, managed["mem_que"], path, managed["model_root"]) + conf = self.run_conf(pipeline, managed["outputs"], path, managed["model_root"]) conf_encoded = (json.dumps(conf, ensure_ascii=False, indent=2) + "\n").encode() staging = Path(tempfile.mkdtemp(prefix=".studio-save-", dir=self.config_root)) preserve_backup = False @@ -481,7 +475,7 @@ def check_revisions() -> tuple[bytes, bytes]: staged_conf = staging / "pipeline.conf" backup_json = staging / "previous.json" staged_json.write_bytes(encoded) - staged_conf.write_text(json.dumps(self.run_conf(pipeline, managed["mem_que"], staged_json, managed["model_root"]))) + staged_conf.write_text(json.dumps(self.run_conf(pipeline, managed["outputs"], staged_json, managed["model_root"]))) configuration = self.resolve_run_conf(staged_conf, profile) # Native validation used the staged JSON; installed paths have # the same model mappings and normalized node configuration. @@ -522,7 +516,7 @@ def profile_inputs(self, pipeline: Any, profile_name: str) -> tuple[dict, Any]: or set(conf) != {"data"} or not isinstance(conf["data"], dict) or not isinstance(conf["data"].get("pipe_path"), str) - or not isinstance(conf["data"].get("mem_que"), dict) + or not isinstance(conf["data"].get("outputs"), dict) ): raise StudioError( "INVALID_PROFILE_CONFIG", "Profile .conf 必须仅包含 data 对象" @@ -539,13 +533,13 @@ def profile_inputs(self, pipeline: Any, profile_name: str) -> tuple[dict, Any]: curr_biz = pipeline.get("biz_name") if orig_biz != curr_biz: raise StudioError("PROFILE_MISMATCH", "Profile 与业务契约不匹配") - return copy.deepcopy(profile), copy.deepcopy(data["mem_que"]) + return copy.deepcopy(profile), copy.deepcopy(data["outputs"]) - def run_conf(self, pipeline: Any, mem_que: Any, pipe_path: Path, model_root: str) -> dict[str, Any]: + def run_conf(self, pipeline: Any, outputs: Any, pipe_path: Path, model_root: str) -> dict[str, Any]: if not isinstance(model_root, str) or not model_root or Path(model_root).is_absolute(): raise StudioError("INVALID_MODEL_ROOT", "模型目录必须是项目内的相对路径(例如 models 或 .)") try: - return SELECTION.build_run_conf(pipeline, mem_que, pipe_path, model_root, PROJECT_ROOT) + return SELECTION.build_run_conf(pipeline, outputs, pipe_path, model_root, PROJECT_ROOT) except (ValueError, TypeError, KeyError) as error: raise StudioError("INVALID_DEPLOYMENT_PATH", str(error)) from error @@ -559,7 +553,7 @@ def resolve_run_conf(self, conf_path: Path, profile: dict[str, Any]) -> dict[str @staticmethod def demo_command(profile: dict[str, Any], conf_path: Path, output_dir: Path) -> list[str]: return [ - str(DEMO_BINARY), "--no-default-control", "--biz", str(profile["biz"]), + str(DEMO_BINARY), "--biz", str(profile["biz"]), "--config", str(conf_path), "--dataset", str(PROJECT_ROOT / profile["dataset"]), "--output-dir", str(output_dir), "--batch-size", str(profile.get("batch_size", 1)), "--device-id", str(profile.get("device_id", 0)), "--chip", str(profile.get("chip", "ax650")), @@ -577,8 +571,8 @@ def save_solution(self, requested: str, pipeline: Any, profile_name: str, model_ raise StudioError("SYMLINK_REJECTED", "拒绝写入符号链接方案") if target.exists(): raise StudioError("FILE_EXISTS", f"另存目标已存在:{target.name}", 409) - profile, mem_que = self.profile_inputs(pipeline, profile_name) - conf = self.run_conf(pipeline, mem_que, path, model_root) + profile, outputs = self.profile_inputs(pipeline, profile_name) + conf = self.run_conf(pipeline, outputs, path, model_root) encoded = (json.dumps(pipeline, ensure_ascii=False, indent=2) + "\n").encode() conf_encoded = (json.dumps(conf, ensure_ascii=False, indent=2) + "\n").encode() created = [] @@ -601,7 +595,7 @@ def save_solution(self, requested: str, pipeline: Any, profile_name: str, model_ raise raise StudioError("SAVE_FAILED", str(error), 500) from error self.generated_solutions[path.name] = { - "profile": profile, "mem_que": mem_que, "model_root": model_root, + "profile": profile, "outputs": outputs, "model_root": model_root, "conf_revision": revision_for(conf_encoded), } return self.solution_result(path, pipeline, conf, encoded, profile, model_root, configuration) @@ -620,10 +614,10 @@ def start_run(self, pipeline: Any, profile_name: str, model_root: str = "models" report = self.validate(pipeline) if not report.get("ok"): raise StudioError("VALIDATION_FAILED", json.dumps(report, ensure_ascii=False)) - profile, mem_que = self.profile_inputs(pipeline, profile_name) + profile, outputs = self.profile_inputs(pipeline, profile_name) # Check the selected paths before creating a job. The worker only changes # pipe_path to its own temporary document under the same deployment root. - conf = self.run_conf(pipeline, mem_que, "build/pipeline.json", model_root) + conf = self.run_conf(pipeline, outputs, "build/pipeline.json", model_root) with self.job_lock: if any(job["status"] in ("queued", "running") for job in self.jobs.values()): raise StudioError("RUN_BUSY", "同一工作台最多运行一个任务", 409) diff --git a/tools/pipeline_studio/web/app.js b/tools/pipeline_studio/web/app.js index 9d053ffc..c543ca21 100644 --- a/tools/pipeline_studio/web/app.js +++ b/tools/pipeline_studio/web/app.js @@ -230,8 +230,8 @@ function renderInspector(node) { container.replaceChildren(); for (const field of definition?.config_fields || []) { const dep = (definition?.model_dependencies || []).find(d => d.config_field === field.name); - const modelRef = Boolean(dep) || field.semantic === "model_ref" || field.name === definition.model_config_field; - const requiredCap = dep ? dep.capability : (definition.model_capability || null); + const modelRef = Boolean(dep) || field.semantic === "model_ref"; + const requiredCap = dep ? dep.capability : null; const choices = modelRef ? compatibleModels(state.pipeline.models, state.catalog.models, requiredCap).map(model => model.model_id) : null; appendConfigField(container, field, node.config || {}, choices); } diff --git a/tools/pipeline_studio/web/workbench.js b/tools/pipeline_studio/web/workbench.js index 40bfd8ff..90f69e24 100644 --- a/tools/pipeline_studio/web/workbench.js +++ b/tools/pipeline_studio/web/workbench.js @@ -7,8 +7,6 @@ export function compatibleModels(models = [], modelDefinitions = [], target = nu requiredCapability = target.capability; } else if (Array.isArray(target.model_dependencies) && target.model_dependencies.length > 0) { requiredCapability = target.model_dependencies[0].capability; - } else if (target.model_capability) { - requiredCapability = target.model_capability; } } if (!requiredCapability) return [...models]; @@ -31,9 +29,6 @@ export function modelBoundNodeIds(nodes = [], nodeDefinitions = []) { const def = definitionByType.get(node.node_type); if (!def) continue; const fields = (def.model_dependencies || []).map(d => d.config_field); - if (fields.length === 0 && def.model_config_field) { - fields.push(def.model_config_field); - } for (const field of fields) { const modelId = node.config?.[field]; if (typeof modelId === "string" && modelId.length > 0) { @@ -212,7 +207,7 @@ export function upsertModel(pipeline, catalog, previousId, model) { for (const node of pipeline.pipeline) { const nodeDefinition = catalog.nodes.find(item => item.node_type === node.node_type); if (previousId && nodeDefinition) { - const deps = nodeDefinition.model_dependencies || (nodeDefinition.model_config_field ? [{ config_field: nodeDefinition.model_config_field, capability: nodeDefinition.model_capability }] : []); + const deps = nodeDefinition.model_dependencies || []; for (const dep of deps) { if (node.config?.[dep.config_field] === previousId && dep.capability !== definition.capability) { throw new Error("所选模型能力与引用节点不兼容"); @@ -227,7 +222,7 @@ export function upsertModel(pipeline, catalog, previousId, model) { for (const node of pipeline.pipeline) { const nodeDefinition = catalog.nodes.find(item => item.node_type === node.node_type); if (nodeDefinition && node.config) { - const deps = nodeDefinition.model_dependencies || (nodeDefinition.model_config_field ? [{ config_field: nodeDefinition.model_config_field }] : []); + const deps = nodeDefinition.model_dependencies || []; for (const dep of deps) { if (node.config[dep.config_field] === previousId) { node.config[dep.config_field] = model.model_id; @@ -242,7 +237,7 @@ export function removeModel(pipeline, catalog, id) { const used = pipeline.pipeline.some(node => { const nodeDefinition = catalog.nodes.find(item => item.node_type === node.node_type); if (!nodeDefinition || !node.config) return false; - const deps = nodeDefinition.model_dependencies || (nodeDefinition.model_config_field ? [{ config_field: nodeDefinition.model_config_field }] : []); + const deps = nodeDefinition.model_dependencies || []; return deps.some(dep => node.config[dep.config_field] === id); }); if (used) throw new Error("模型仍被节点使用,请先更换绑定"); diff --git a/tools/verify_selection.py b/tools/verify_selection.py index a3c73b23..8e261cfe 100644 --- a/tools/verify_selection.py +++ b/tools/verify_selection.py @@ -67,7 +67,7 @@ def within(root, relative): return path -def build_run_conf(pipeline, mem_que, pipe_path, model_root, bundle_root): +def build_run_conf(pipeline, outputs, pipe_path, model_root, bundle_root): """Map the selected Pipeline's model paths into an explicit deployment root.""" bundle_root = Path(bundle_root).resolve() model_root = within(bundle_root, model_root) @@ -75,7 +75,7 @@ def build_run_conf(pipeline, mem_que, pipe_path, model_root, bundle_root): model_paths = {model["model_id"]: str(within(model_root, model["model_path"]).relative_to(bundle_root)) for model in pipeline.get("models", [])} return {"data": {"pipe_path": str(pipeline_path), - "model_paths": model_paths, "mem_que": mem_que}} + "model_paths": model_paths, "outputs": outputs}} def validate_manifest(manifest): @@ -201,20 +201,20 @@ def effect_inputs(spec_path, conf_path, demo): conf = read_json(conf_path) # The evaluator deliberately regenerates model_paths from the selected # Pipeline; only deployment output capacities are inherited. - mem_que = conf["data"]["mem_que"] + outputs = conf["data"]["outputs"] demo = Path(demo).resolve() sdk_candidates = list(demo.parent.glob("libcompany_alg_sdk.*")) sdk_files = sorted({path.resolve() for path in sdk_candidates if path.is_file()}) - identity = {"spec": spec, "dataset_sha256": file_digest(dataset), "mem_que": mem_que, + identity = {"spec": spec, "dataset_sha256": file_digest(dataset), "outputs": outputs, "demo_sha256": file_digest(demo), "sdk": {p.name: file_digest(p) for p in sdk_files}, - "chip": "cpu", "device_id": 0, "no_default_control": True} - return spec, dataset, mem_que, identity + "chip": "cpu", "device_id": 0} + return spec, dataset, outputs, identity def evaluate(pipeline, selection, tool, model_root, spec_path, conf_path, demo): if not selection["ok"]: raise ValueError("Configuration, build or assets are not verified") - spec, dataset, mem_que, test_inputs = effect_inputs(spec_path, conf_path, demo) + spec, dataset, outputs, test_inputs = effect_inputs(spec_path, conf_path, demo) test_fingerprint = digest(test_inputs) if spec["biz_name"] != pipeline["biz_name"]: raise ValueError("Effect specification business mismatch") @@ -228,12 +228,11 @@ def evaluate(pipeline, selection, tool, model_root, spec_path, conf_path, demo): temporary = Path(directory) relative = temporary.relative_to(bundle_root) (temporary / "pipeline.json").write_text(json.dumps(pipeline)) - generated_conf = build_run_conf(pipeline, mem_que, relative / "pipeline.json", model_root, bundle_root) + generated_conf = build_run_conf(pipeline, outputs, relative / "pipeline.json", model_root, bundle_root) (temporary / "pipeline.conf").write_text(json.dumps(generated_conf)) command = [str(Path(demo).resolve()), "--biz", biz, "--config", str(relative / "pipeline.conf"), "--dataset", str(dataset), "--output-dir", str(temporary / "results"), - "--chip", "cpu", "--device-id", "0", "--batch-size", "1", "--depth", "1", - "--no-default-control"] + "--chip", "cpu", "--device-id", "0", "--batch-size", "1", "--depth", "1"] process = subprocess.run(command, cwd=bundle_root, text=True, capture_output=True, timeout=1800, check=False) if process.returncode: raise ValueError("Effect run failed: " + (process.stdout + process.stderr)[-3000:])