diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e713d7b..22b3d068 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,8 @@ cmake_minimum_required(VERSION 3.16) project(LLMEdgeFlow VERSION 10.0.0 LANGUAGES C CXX) -set(LLM_EDGEFLOW_ABI_VERSION "5.0.0") -set(LLM_EDGEFLOW_ABI_VERSION_MAJOR 5) +set(LLM_EDGEFLOW_ABI_VERSION "6.0.0") +set(LLM_EDGEFLOW_ABI_VERSION_MAJOR 6) # 严格禁止 In-Source 构建 (防止误用 cmake . 污染源码目录并触发重复下载阻塞) if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") diff --git a/README.md b/README.md index 75d1d896..15bfac70 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ Smoke 验证执行链路;真实模型的业务效果需使用目标数据集 该命令统一执行格式与静态检查、配置构建及 CTest 测试。环境需具备 clang-format 18,以及架构图检查所需的 Java 17+;详细流程见 [CONTRIBUTING.md](CONTRIBUTING.md),测试组织见 [tests/README.md](tests/README.md),Agent 开发约束见 [AGENTS.md](AGENTS.md)。 -当前产品版本为 **v10.0.0**,公共 **ABI major 为 5**。接口边界见[架构设计](doc/architecture.md),版本记录见 [Changelog](doc/CHANGELOG.md)。 +当前产品版本为 **v10.0.0**,公共 **ABI major 为 6**。接口边界见[架构设计](doc/architecture.md),版本记录见 [Changelog](doc/CHANGELOG.md)。 ## 许可证 diff --git a/cmake_ext/IndividualTests.cmake b/cmake_ext/IndividualTests.cmake index b72bffb8..727901d9 100644 --- a/cmake_ext/IndividualTests.cmake +++ b/cmake_ext/IndividualTests.cmake @@ -118,10 +118,6 @@ add_executable(test_operator_value_registry ${EDGEFLOW_SOURCE_test_operator_valu target_link_libraries(test_operator_value_registry PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main edgeflow_test_allocation_failure) add_test(NAME OperatorValueRegistryTest COMMAND test_operator_value_registry) -add_executable(test_operator_biz_bridge_registry ${EDGEFLOW_SOURCE_test_operator_biz_bridge_registry}) -target_link_libraries(test_operator_biz_bridge_registry PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main) -add_test(NAME OperatorBizBridgeRegistryTest COMMAND test_operator_biz_bridge_registry) - add_executable(test_doc_qa_rerank ${EDGEFLOW_SOURCE_test_doc_qa_rerank}) target_link_libraries(test_doc_qa_rerank PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main) add_test(NAME DocQaRerankTest COMMAND test_doc_qa_rerank) @@ -282,9 +278,21 @@ add_executable(test_operator_golden ${EDGEFLOW_SOURCE_test_operator_golden}) target_link_libraries(test_operator_golden PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main) add_test(NAME OperatorGoldenTest COMMAND test_operator_golden) -add_executable(test_adapter_purity ${EDGEFLOW_SOURCE_test_adapter_purity}) -target_link_libraries(test_adapter_purity PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main) -add_test(NAME AdapterPurityTest COMMAND test_adapter_purity) +add_executable(test_io_converters ${EDGEFLOW_SOURCE_test_io_converters}) +target_link_libraries(test_io_converters PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main) +add_test(NAME IoConverterTest COMMAND test_io_converters) + +add_executable(test_io_binding_registry ${EDGEFLOW_SOURCE_test_io_binding_registry}) +target_link_libraries(test_io_binding_registry PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main) +add_test(NAME IoBindingRegistryTest COMMAND test_io_binding_registry) + +add_executable(test_text_converters ${EDGEFLOW_SOURCE_test_text_converters}) +target_link_libraries(test_text_converters PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main) +add_test(NAME TextConvertersTest COMMAND test_text_converters) + +add_executable(test_complex_converters ${EDGEFLOW_SOURCE_test_complex_converters}) +target_link_libraries(test_complex_converters PRIVATE llm_edgeflow::internal_runtime GTest::gtest GTest::gtest_main) +add_test(NAME ComplexConvertersTest COMMAND test_complex_converters) # Keep each individual executable's Registry environment aligned with the # corresponding sharded runner. Conflict and catalog-isolation executables are @@ -336,7 +344,10 @@ set(EDGEFLOW_INDIVIDUAL_TESTS_WITH_RUNTIME_FIXTURES test_function_node test_parameter_binding test_operator_golden - test_adapter_purity) + test_io_converters + test_io_binding_registry + test_text_converters + test_complex_converters) foreach(test_target IN LISTS EDGEFLOW_INDIVIDUAL_TESTS_WITH_RUNTIME_FIXTURES) target_sources(${test_target} PRIVATE $ @@ -354,7 +365,7 @@ set_tests_properties( RuntimeControlAndHotSwapTest EngineFaultToleranceAndLifecycleTest AdapterContractSecurityTest PipelineConfigTest RegistryConflictNodeTest RegistryConflictModelTest RegistryReentrantTest OperatorApiTest - OperatorOutputPoolTest OperatorValueRegistryTest OperatorBizBridgeRegistryTest + OperatorOutputPoolTest OperatorValueRegistryTest DocQaRerankTest PipelineStudioTest PipelineStudioServerTest DemoRunnerTest CatalogContractSsotTest TypedBlackboardContractsTest @@ -365,6 +376,7 @@ set_tests_properties( VectorTopKNodeTest TextRerankNodeTest TextTemplateNodeTest LlmGenerateNodeTest AsrTranscribeNodeTest OcrDetectNodeTest TextRuleMatchNodeTest StructuredJsonParseNodeTest TextCorpusSourceNodeTest - CommonNodesTest FunctionNodeTest ParameterBindingTest OperatorGoldenTest AdapterPurityTest + CommonNodesTest FunctionNodeTest ParameterBindingTest OperatorGoldenTest + IoConverterTest IoBindingRegistryTest TextConvertersTest ComplexConvertersTest PROPERTIES WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" ) diff --git a/cmake_ext/TestInventory.cmake b/cmake_ext/TestInventory.cmake index 37bbc60f..4e65931c 100644 --- a/cmake_ext/TestInventory.cmake +++ b/cmake_ext/TestInventory.cmake @@ -44,9 +44,12 @@ set(EDGEFLOW_REQUIRED_CONTRACT_TESTS OperatorApiTest OperatorOutputPoolTest OperatorValueRegistryTest - OperatorBizBridgeRegistryTest OperatorGoldenTest AdapterPurityTest + IoConverterTest + IoBindingRegistryTest + TextConvertersTest + ComplexConvertersTest DocQaRerankTest PipelineStudioTest DemoRunnerTest @@ -83,6 +86,10 @@ set(EDGEFLOW_SOURCE_test_concurrency_and_edge_cases "${PROJECT_SOURCE_DIR}/tests set(EDGEFLOW_SOURCE_test_different_io_modalities "${PROJECT_SOURCE_DIR}/tests/integration/runtime/test_different_io_modalities.cpp") set(EDGEFLOW_SOURCE_test_runtime_control_and_hot_swap "${PROJECT_SOURCE_DIR}/tests/integration/runtime/test_runtime_control_and_hot_swap.cpp") set(EDGEFLOW_SOURCE_test_adapter_purity "${PROJECT_SOURCE_DIR}/tests/unit/adapter/test_adapter_purity.cpp") +set(EDGEFLOW_SOURCE_test_io_converters "${PROJECT_SOURCE_DIR}/tests/unit/adapter/test_io_converters.cpp") +set(EDGEFLOW_SOURCE_test_io_binding_registry "${PROJECT_SOURCE_DIR}/tests/unit/adapter/test_io_binding_registry.cpp") +set(EDGEFLOW_SOURCE_test_text_converters "${PROJECT_SOURCE_DIR}/tests/unit/adapter/test_text_converters.cpp") +set(EDGEFLOW_SOURCE_test_complex_converters "${PROJECT_SOURCE_DIR}/tests/unit/adapter/test_complex_converters.cpp") set(EDGEFLOW_SOURCE_test_dag_pipeline "${PROJECT_SOURCE_DIR}/tests/unit/core/test_dag_pipeline.cpp") set(EDGEFLOW_SOURCE_test_definition_schema_validation "${PROJECT_SOURCE_DIR}/tests/unit/core/test_definition_schema_validation.cpp") set(EDGEFLOW_SOURCE_test_framework_core "${PROJECT_SOURCE_DIR}/tests/unit/core/test_framework_core.cpp") @@ -119,7 +126,6 @@ set(EDGEFLOW_SOURCE_test_text_rerank_node "${PROJECT_SOURCE_DIR}/tests/unit/node set(EDGEFLOW_SOURCE_test_text_rule_match_node "${PROJECT_SOURCE_DIR}/tests/unit/nodes/test_text_rule_match_node.cpp") set(EDGEFLOW_SOURCE_test_text_template_node "${PROJECT_SOURCE_DIR}/tests/unit/nodes/test_text_template_node.cpp") set(EDGEFLOW_SOURCE_test_vector_top_k_node "${PROJECT_SOURCE_DIR}/tests/unit/nodes/test_vector_top_k_node.cpp") -set(EDGEFLOW_SOURCE_test_operator_biz_bridge_registry "${PROJECT_SOURCE_DIR}/tests/unit/operator/test_operator_biz_bridge_registry.cpp") set(EDGEFLOW_SOURCE_test_operator_output_pool "${PROJECT_SOURCE_DIR}/tests/unit/operator/test_operator_output_pool.cpp") set(EDGEFLOW_SOURCE_test_operator_value_registry "${PROJECT_SOURCE_DIR}/tests/unit/operator/test_operator_value_registry.cpp") include(${CMAKE_CURRENT_LIST_DIR}/CustomNodeTests.cmake) diff --git a/cmake_ext/Tests.cmake b/cmake_ext/Tests.cmake index b695eb5d..7817398f 100644 --- a/cmake_ext/Tests.cmake +++ b/cmake_ext/Tests.cmake @@ -156,9 +156,12 @@ set(EDGEFLOW_TEST_ADAPTER_SRCS ${EDGEFLOW_SOURCE_test_operator_api} ${EDGEFLOW_SOURCE_test_operator_output_pool} ${EDGEFLOW_SOURCE_test_operator_value_registry} - ${EDGEFLOW_SOURCE_test_operator_biz_bridge_registry} ${EDGEFLOW_SOURCE_test_operator_golden} - ${EDGEFLOW_SOURCE_test_adapter_purity}) + ${EDGEFLOW_SOURCE_test_adapter_purity} + ${EDGEFLOW_SOURCE_test_io_converters} + ${EDGEFLOW_SOURCE_test_io_binding_registry} + ${EDGEFLOW_SOURCE_test_text_converters} + ${EDGEFLOW_SOURCE_test_complex_converters}) add_executable(edgeflow_test_adapter_runner ${EDGEFLOW_TEST_ADAPTER_SRCS} $ @@ -310,14 +313,18 @@ edgeflow_add_runner_test(OperatorOutputPoolTest edgeflow_test_adapter_runner "OperatorOutputPoolTest.*" "${_edgeflow_tier2}") edgeflow_add_runner_test(OperatorValueRegistryTest edgeflow_test_adapter_runner "OperatorValueRegistryTest.*" "${_edgeflow_tier2}") -edgeflow_add_runner_test(OperatorBizBridgeRegistryTest - edgeflow_test_adapter_runner "OperatorBizBridgeRegistryTest.*" - "${_edgeflow_tier2}") edgeflow_add_runner_test(OperatorGoldenTest edgeflow_test_adapter_runner "OperatorGoldenTest.*" "${_edgeflow_tier2}") edgeflow_add_runner_test(AdapterPurityTest edgeflow_test_adapter_runner - "AdapterPurityTest.*:RequestResultsTest.*:AdapterResultTest.*:ReadMultiWayResultsTest.*:OneToOneTextAdapterTest.*" - "${_edgeflow_tier2}") + "AdapterPurityTest.*" "${_edgeflow_tier2}") +edgeflow_add_runner_test(IoConverterTest edgeflow_test_adapter_runner + "IoConverterTest.*" "${_edgeflow_tier1}") +edgeflow_add_runner_test(IoBindingRegistryTest edgeflow_test_adapter_runner + "IoBindingRegistryTest.*" "${_edgeflow_tier1}") +edgeflow_add_runner_test(TextConvertersTest edgeflow_test_adapter_runner + "TextConvertersTest.*" "${_edgeflow_tier1}") +edgeflow_add_runner_test(ComplexConvertersTest edgeflow_test_adapter_runner + "ComplexConvertersTest.*" "${_edgeflow_tier1}") edgeflow_add_runner_test(DocQaRerankTest edgeflow_test_tooling_runner "DocQaRerankPipelineTest.*" "${_edgeflow_tier1}") diff --git a/cmake_ext/edgeflow_sdk.map.in b/cmake_ext/edgeflow_sdk.map.in index e88908f8..7614dea5 100644 --- a/cmake_ext/edgeflow_sdk.map.in +++ b/cmake_ext/edgeflow_sdk.map.in @@ -11,7 +11,7 @@ LLM_EDGEFLOW_@LLM_EDGEFLOW_ABI_VERSION_MAJOR@ { AlgBase_logPrint; _ZN12llm_edgeflow12operator_api30Get_LLM_EDGEFLOW_OperatorTableEv; _ZN12llm_edgeflow12operator_api20GetOperatorLastErrorEv; - _ZN12llm_edgeflow12operator_api29ValidateOperatorConfigBindingEPKcS2_iPcm; + _ZN12llm_edgeflow12operator_api29ValidateOperatorConfigBindingEPKcS2_S2_Pcm; local: *; }; diff --git a/configs/pipeline_audio_asr_cabi.json b/configs/pipeline_audio_asr_cabi.json new file mode 100644 index 00000000..55451243 --- /dev/null +++ b/configs/pipeline_audio_asr_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_audio_asr_cpu.json", + "io_binding": "audio_asr_intent.cabi.v1" + } +} diff --git a/configs/pipeline_audio_asr_cpu.conf b/configs/pipeline_audio_asr_cpu.conf index 6a3f8c7a..c2024fb5 100644 --- a/configs/pipeline_audio_asr_cpu.conf +++ b/configs/pipeline_audio_asr_cpu.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_audio_asr_cpu.json", + "pipe_path": "pipeline_audio_asr_cpu.json", + "io_binding": "audio_asr_intent.operator.v1", "model_paths": { "asr_model_v1": "models/ggml-base.bin" }, diff --git a/configs/pipeline_cross_rerank_cabi.json b/configs/pipeline_cross_rerank_cabi.json new file mode 100644 index 00000000..9ea57aae --- /dev/null +++ b/configs/pipeline_cross_rerank_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_cross_rerank_cpu.json", + "io_binding": "cross_rerank.cabi.v1" + } +} diff --git a/configs/pipeline_cross_rerank_cpu.conf b/configs/pipeline_cross_rerank_cpu.conf index 8aa88f0f..90df28ce 100644 --- a/configs/pipeline_cross_rerank_cpu.conf +++ b/configs/pipeline_cross_rerank_cpu.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_cross_rerank_cpu.json", + "pipe_path": "pipeline_cross_rerank_cpu.json", + "io_binding": "cross_rerank.operator.v1", "model_paths": { "rerank_model_v1": "./models/ms_marco_tinybert_l2_v2_quantized.onnx" }, diff --git a/configs/pipeline_dialogue_audit_cabi.json b/configs/pipeline_dialogue_audit_cabi.json new file mode 100644 index 00000000..56b4c688 --- /dev/null +++ b/configs/pipeline_dialogue_audit_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_dialogue_audit_default.json", + "io_binding": "compliance_audit.cabi.v1" + } +} diff --git a/configs/pipeline_dialogue_audit_default.conf b/configs/pipeline_dialogue_audit_default.conf index e51afa2f..96210421 100644 --- a/configs/pipeline_dialogue_audit_default.conf +++ b/configs/pipeline_dialogue_audit_default.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_dialogue_audit_default.json", + "pipe_path": "pipeline_dialogue_audit_default.json", + "io_binding": "compliance_audit.operator.v1", "model_paths": { "embed_model_v2": "./models/bge_base_zh_v1.5.onnx", "rerank_model_v1": "./models/ms_marco_tinybert_l2_v2_quantized.onnx", diff --git a/configs/pipeline_dialogue_audit_kite.conf b/configs/pipeline_dialogue_audit_kite.conf index befd14d8..af8e1935 100644 --- a/configs/pipeline_dialogue_audit_kite.conf +++ b/configs/pipeline_dialogue_audit_kite.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_dialogue_audit_kite.json", + "pipe_path": "pipeline_dialogue_audit_kite.json", + "io_binding": "compliance_audit.operator.v1", "model_paths": { "embed_model_v2": "models/bge_base_zh_v1.5.onnx", "rerank_model_v1": "models/ms_marco_tinybert_l2_v2_quantized.onnx", diff --git a/configs/pipeline_doc_qa_cabi.json b/configs/pipeline_doc_qa_cabi.json new file mode 100644 index 00000000..fee2ebe6 --- /dev/null +++ b/configs/pipeline_doc_qa_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_doc_qa_cpu.json", + "io_binding": "doc_qa.cabi.v1" + } +} diff --git a/configs/pipeline_doc_qa_cpu.conf b/configs/pipeline_doc_qa_cpu.conf index 55962ba3..c7b10130 100644 --- a/configs/pipeline_doc_qa_cpu.conf +++ b/configs/pipeline_doc_qa_cpu.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_doc_qa_cpu.json", + "pipe_path": "pipeline_doc_qa_cpu.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_onnx": "./models/bge_base_zh_v1.5.onnx", "llm_model_llamacpp": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" diff --git a/configs/pipeline_doc_qa_default.conf b/configs/pipeline_doc_qa_default.conf index 02cacdda..0e25e0f3 100644 --- a/configs/pipeline_doc_qa_default.conf +++ b/configs/pipeline_doc_qa_default.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_doc_qa_default.json", + "pipe_path": "pipeline_doc_qa_default.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_v1": "./models/bge_base_zh_v1.5.onnx", "llm_model_v1": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" diff --git a/configs/pipeline_doc_qa_kite.conf b/configs/pipeline_doc_qa_kite.conf index 39294d50..27a6af4a 100644 --- a/configs/pipeline_doc_qa_kite.conf +++ b/configs/pipeline_doc_qa_kite.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_doc_qa_kite.json", + "pipe_path": "pipeline_doc_qa_kite.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_v1": "models/bge_base_zh_v1.5.onnx", "llm_model_v1": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" diff --git a/configs/pipeline_doc_qa_kite_generated_embeddings.conf b/configs/pipeline_doc_qa_kite_generated_embeddings.conf index 9f664844..4af62b36 100644 --- a/configs/pipeline_doc_qa_kite_generated_embeddings.conf +++ b/configs/pipeline_doc_qa_kite_generated_embeddings.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_doc_qa_kite_generated_embeddings.json", + "pipe_path": "pipeline_doc_qa_kite_generated_embeddings.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "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" diff --git a/configs/pipeline_doc_qa_rerank_cabi.json b/configs/pipeline_doc_qa_rerank_cabi.json new file mode 100644 index 00000000..b7abd729 --- /dev/null +++ b/configs/pipeline_doc_qa_rerank_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_doc_qa_rerank_cpu.json", + "io_binding": "doc_qa.cabi.v1" + } +} diff --git a/configs/pipeline_doc_qa_rerank_cpu.conf b/configs/pipeline_doc_qa_rerank_cpu.conf index 916de12d..c7d26f46 100644 --- a/configs/pipeline_doc_qa_rerank_cpu.conf +++ b/configs/pipeline_doc_qa_rerank_cpu.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_doc_qa_rerank_cpu.json", + "pipe_path": "pipeline_doc_qa_rerank_cpu.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_onnx": "./models/bge_base_zh_v1.5.onnx", "rerank_model_onnx": "./models/ms_marco_tinybert_l2_v2_quantized.onnx", diff --git a/configs/pipeline_doc_qa_rerank_default.conf b/configs/pipeline_doc_qa_rerank_default.conf index ab550a16..34ada649 100644 --- a/configs/pipeline_doc_qa_rerank_default.conf +++ b/configs/pipeline_doc_qa_rerank_default.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_doc_qa_rerank_default.json", + "pipe_path": "pipeline_doc_qa_rerank_default.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_v1": "./models/bge_base_zh_v1.5.onnx", "rerank_model_v1": "./models/ms_marco_tinybert_l2_v2_quantized.onnx", diff --git a/configs/pipeline_doc_qa_rerank_kite.conf b/configs/pipeline_doc_qa_rerank_kite.conf index d7f1ea5f..e8314c3e 100644 --- a/configs/pipeline_doc_qa_rerank_kite.conf +++ b/configs/pipeline_doc_qa_rerank_kite.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_doc_qa_rerank_kite.json", + "pipe_path": "pipeline_doc_qa_rerank_kite.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_onnx": "models/bge_base_zh_v1.5.onnx", "rerank_model_onnx": "models/ms_marco_tinybert_l2_v2_quantized.onnx", diff --git a/configs/pipeline_entity_extract_cabi.json b/configs/pipeline_entity_extract_cabi.json new file mode 100644 index 00000000..f87ba5d9 --- /dev/null +++ b/configs/pipeline_entity_extract_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_entity_extract_cpu.json", + "io_binding": "entity_extract.cabi.v1" + } +} diff --git a/configs/pipeline_entity_extract_cpu.conf b/configs/pipeline_entity_extract_cpu.conf index 180869a8..a7de58fe 100644 --- a/configs/pipeline_entity_extract_cpu.conf +++ b/configs/pipeline_entity_extract_cpu.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_entity_extract_cpu.json", + "pipe_path": "pipeline_entity_extract_cpu.json", + "io_binding": "entity_extract.operator.v1", "model_paths": { "entity_llm": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, diff --git a/configs/pipeline_entity_extract_default.conf b/configs/pipeline_entity_extract_default.conf index 9769d5d5..8ccbb6fb 100644 --- a/configs/pipeline_entity_extract_default.conf +++ b/configs/pipeline_entity_extract_default.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_entity_extract_default.json", + "pipe_path": "pipeline_entity_extract_default.json", + "io_binding": "entity_extract.operator.v1", "model_paths": { "entity_llm": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, diff --git a/configs/pipeline_entity_extract_kite.conf b/configs/pipeline_entity_extract_kite.conf index ebe6f45c..8e649a88 100644 --- a/configs/pipeline_entity_extract_kite.conf +++ b/configs/pipeline_entity_extract_kite.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_entity_extract_kite.json", + "pipe_path": "pipeline_entity_extract_kite.json", + "io_binding": "entity_extract.operator.v1", "model_paths": { "entity_llm": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, diff --git a/configs/pipeline_keyword_match_cabi.json b/configs/pipeline_keyword_match_cabi.json new file mode 100644 index 00000000..8929db60 --- /dev/null +++ b/configs/pipeline_keyword_match_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.cabi.v1" + } +} diff --git a/configs/pipeline_keyword_match_rules.conf b/configs/pipeline_keyword_match_rules.conf index 4b7435f1..a0bb0f14 100644 --- a/configs/pipeline_keyword_match_rules.conf +++ b/configs/pipeline_keyword_match_rules.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "outputs": { "keyword_out": { "type": "keyword_out", diff --git a/configs/pipeline_ocr_doc_qa_cabi.json b/configs/pipeline_ocr_doc_qa_cabi.json new file mode 100644 index 00000000..fa47ac17 --- /dev/null +++ b/configs/pipeline_ocr_doc_qa_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_ocr_doc_qa_kite.json", + "io_binding": "ocr_doc_qa.cabi.v1" + } +} diff --git a/configs/pipeline_ocr_doc_qa_kite.conf b/configs/pipeline_ocr_doc_qa_kite.conf index 3b98f689..bfb76166 100644 --- a/configs/pipeline_ocr_doc_qa_kite.conf +++ b/configs/pipeline_ocr_doc_qa_kite.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_ocr_doc_qa_kite.json", + "pipe_path": "pipeline_ocr_doc_qa_kite.json", + "io_binding": "ocr_doc_qa.operator.v1", "model_paths": { "ocr_model_v1": "models/SmolVLM-256M-Instruct-Q8_0.gguf", "llm_model_v1": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf" diff --git a/configs/pipeline_translate_cabi.json b/configs/pipeline_translate_cabi.json new file mode 100644 index 00000000..587266a2 --- /dev/null +++ b/configs/pipeline_translate_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_translate_cpu.json", + "io_binding": "translate.cabi.v1" + } +} diff --git a/configs/pipeline_translate_cpu.conf b/configs/pipeline_translate_cpu.conf index 53be9f6c..68e140ab 100644 --- a/configs/pipeline_translate_cpu.conf +++ b/configs/pipeline_translate_cpu.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_translate_cpu.json", + "pipe_path": "pipeline_translate_cpu.json", + "io_binding": "translate.operator.v1", "model_paths": { "translate_llm": "./models/qwen2.5-0.5b-instruct-q4_k_m.gguf" }, diff --git a/demo/biz/audio_asr_demo.cpp b/demo/biz/audio_asr_demo.cpp index 8160d3ba..4acc64c2 100644 --- a/demo/biz/audio_asr_demo.cpp +++ b/demo/biz/audio_asr_demo.cpp @@ -161,6 +161,6 @@ int RunAudioAsrDemo(const DemoOptions& options) { } REGISTER_DEMO_BIZ("audio_asr", "语音识别与意图槽位抽取", RunAudioAsrDemo, - ALG_BIZ_TYPE_AUDIO_ASR_INTENT); + "audio_asr_intent.operator.v1"); } // namespace alg_demo diff --git a/demo/biz/cross_rerank_demo.cpp b/demo/biz/cross_rerank_demo.cpp index 7fe21dc9..049e17e5 100644 --- a/demo/biz/cross_rerank_demo.cpp +++ b/demo/biz/cross_rerank_demo.cpp @@ -135,6 +135,6 @@ int RunCrossRerankDemo(const DemoOptions& options) { } REGISTER_DEMO_BIZ("cross_rerank", "纯语义精排打分业务", RunCrossRerankDemo, - ALG_BIZ_TYPE_CROSS_RERANK); + "cross_rerank.operator.v1"); } // namespace alg_demo diff --git a/demo/biz/dialogue_audit_demo.cpp b/demo/biz/dialogue_audit_demo.cpp index 3ea26e33..143a583f 100644 --- a/demo/biz/dialogue_audit_demo.cpp +++ b/demo/biz/dialogue_audit_demo.cpp @@ -157,6 +157,6 @@ int RunDialogueAuditDemo(const DemoOptions& options) { } REGISTER_DEMO_BIZ("dialogue_audit", "智能对话风控质检业务", - RunDialogueAuditDemo, ALG_BIZ_TYPE_COMPLIANCE_AUDIT); + RunDialogueAuditDemo, "compliance_audit.operator.v1"); } // namespace alg_demo diff --git a/demo/biz/doc_qa_demo.cpp b/demo/biz/doc_qa_demo.cpp index ca6debf7..d6682dc9 100644 --- a/demo/biz/doc_qa_demo.cpp +++ b/demo/biz/doc_qa_demo.cpp @@ -141,6 +141,6 @@ int RunDocQaDemo(const DemoOptions& options) { } REGISTER_DEMO_BIZ("doc_qa", "智能长文档问答业务", RunDocQaDemo, - ALG_BIZ_TYPE_DOC_QA); + "doc_qa.operator.v1"); } // namespace alg_demo diff --git a/demo/biz/entity_extract_demo.cpp b/demo/biz/entity_extract_demo.cpp index 58915102..14d95097 100644 --- a/demo/biz/entity_extract_demo.cpp +++ b/demo/biz/entity_extract_demo.cpp @@ -120,11 +120,11 @@ int RunEntityExtractDemo(const DemoOptions& options) { } REGISTER_DEMO_BIZ("entity_extract", "实体/名词提取业务", RunEntityExtractDemo, - ALG_BIZ_TYPE_ENTITY_EXTRACT); + "entity_extract.operator.v1"); int RunTranslateDemo(const DemoOptions& options) { return RunEntityExtractDemo(options); } REGISTER_DEMO_BIZ("translate", "JSON 字符串翻译", RunTranslateDemo, - ALG_BIZ_TYPE_TRANSLATE); + "translate.operator.v1"); } // namespace alg_demo diff --git a/demo/biz/keyword_match_demo.cpp b/demo/biz/keyword_match_demo.cpp index ca9002f5..b9e05a9c 100644 --- a/demo/biz/keyword_match_demo.cpp +++ b/demo/biz/keyword_match_demo.cpp @@ -134,6 +134,6 @@ int RunKeywordMatchDemo(const DemoOptions& options) { } REGISTER_DEMO_BIZ("keyword_match", "关注词匹配业务", RunKeywordMatchDemo, - ALG_BIZ_TYPE_KEYWORD_MATCH); + "keyword_match.operator.v1"); } // namespace alg_demo diff --git a/demo/biz/ocr_doc_qa_demo.cpp b/demo/biz/ocr_doc_qa_demo.cpp index 4f675aa9..8e957538 100644 --- a/demo/biz/ocr_doc_qa_demo.cpp +++ b/demo/biz/ocr_doc_qa_demo.cpp @@ -139,6 +139,6 @@ int RunOcrDocQaDemo(const DemoOptions& options) { } REGISTER_DEMO_BIZ("ocr_doc_qa", "智能多模态图文票据问答", RunOcrDocQaDemo, - ALG_BIZ_TYPE_OCR_DOC_QA); + "ocr_doc_qa.operator.v1"); } // namespace alg_demo diff --git a/demo/common/demo_registry.cpp b/demo/common/demo_registry.cpp index ed85631e..c9b1a13d 100644 --- a/demo/common/demo_registry.cpp +++ b/demo/common/demo_registry.cpp @@ -24,8 +24,8 @@ bool DemoRegistry::Register(DemoDescriptor descriptor) { return false; } - if (descriptor.biz_type == ALG_BIZ_TYPE_UNKNOWN) { - std::cerr << "[DemoRegistry ERROR] Unknown biz_type for biz: " + if (descriptor.expected_binding_id.empty()) { + std::cerr << "[DemoRegistry ERROR] Empty expected_binding_id for biz: " << descriptor.biz_name << std::endl; has_conflict_ = true; return false; diff --git a/demo/common/demo_registry.h b/demo/common/demo_registry.h index 490c430f..b6829859 100644 --- a/demo/common/demo_registry.h +++ b/demo/common/demo_registry.h @@ -18,15 +18,16 @@ struct DemoDescriptor { std::string biz_name; // 业务标识名 (如 entity_extract, doc_qa) std::string display_title; // 终端展示标题 (如 "实体/名词提取业务") DemoRunFunction run = nullptr; - CompanyAlgBizType biz_type = ALG_BIZ_TYPE_UNKNOWN; + std::string + expected_binding_id; // 权威接入绑定 ID (如 entity_extract.operator.v1) DemoDescriptor() = default; DemoDescriptor(std::string name, std::string title, DemoRunFunction func, - CompanyAlgBizType type) + std::string binding_id) : biz_name(std::move(name)), display_title(std::move(title)), run(func), - biz_type(type) {} + expected_binding_id(std::move(binding_id)) {} }; class DemoRegistry { @@ -35,7 +36,7 @@ class DemoRegistry { /** * @brief 注册业务 Demo 描述符 - * @param descriptor 业务描述符 (拒绝空名、空函数、未知类型或重复注册) + * @param descriptor 业务描述符 (拒绝空名、空函数、空绑定或重复注册) * @return true 注册成功, false 注册失败 (冲突或非法) */ bool Register(DemoDescriptor descriptor); @@ -80,13 +81,14 @@ class DemoRegistry { class DemoRegisterHelper { public: DemoRegisterHelper(const char* name, const char* title, DemoRunFunction func, - CompanyAlgBizType type) { - DemoRegistry::Instance().Register({name, title, func, type}); + const char* expected_binding_id) { + DemoRegistry::Instance().Register( + {name, title, func, expected_binding_id ? expected_binding_id : ""}); } }; -#define REGISTER_DEMO_BIZ(biz_name, title, run_func, biz_type) \ - static ::alg_demo::DemoRegisterHelper g_demo_reg_##run_func( \ - biz_name, title, run_func, biz_type); +#define REGISTER_DEMO_BIZ(biz_name, title, run_func, expected_binding_id) \ + static ::alg_demo::DemoRegisterHelper g_demo_reg_##run_func( \ + biz_name, title, run_func, expected_binding_id); } // namespace alg_demo diff --git a/demo/common/operator_runner.h b/demo/common/operator_runner.h index ec7d2cc2..95a69f44 100644 --- a/demo/common/operator_runner.h +++ b/demo/common/operator_runner.h @@ -21,12 +21,11 @@ namespace alg_demo { /** - * @brief 将 Demo 业务名映射为标准 CompanyAlgBizType 枚举 - * 从 DemoRegistry 获取业务自注册的权威类型 + * @brief 从 DemoRegistry 获取业务自注册的权威接入绑定 ID */ -inline CompanyAlgBizType DemoBizToBizType(std::string_view demo_biz) { +inline std::string DemoBizToExpectedBindingId(std::string_view demo_biz) { const auto* desc = DemoRegistry::Instance().Find(demo_biz); - return desc ? desc->biz_type : ALG_BIZ_TYPE_UNKNOWN; + return desc ? desc->expected_binding_id : ""; } /** @@ -80,8 +79,8 @@ inline bool ResolveModelRootAndConfig(const std::string& conf_path, inline bool ValidateConfigBizMatch(const std::string& conf_path, std::string_view expected_biz, std::string* error_msg) { - CompanyAlgBizType expected_type = DemoBizToBizType(expected_biz); - if (expected_type == ALG_BIZ_TYPE_UNKNOWN) { + std::string expected_binding = DemoBizToExpectedBindingId(expected_biz); + if (expected_binding.empty()) { if (error_msg) { *error_msg = "Unknown demo biz: " + std::string(expected_biz); } @@ -94,8 +93,8 @@ inline bool ValidateConfigBizMatch(const std::string& conf_path, char err_buf[512] = {0}; int ret = llm_edgeflow::operator_api::ValidateOperatorConfigBinding( - model_root.c_str(), cfg_rel.c_str(), static_cast(expected_type), - err_buf, sizeof(err_buf)); + model_root.c_str(), cfg_rel.c_str(), expected_binding.c_str(), err_buf, + sizeof(err_buf)); if (ret != 0) { if (error_msg) { diff --git a/demo/fixtures/mock/pipeline_audio_asr_intent.conf b/demo/fixtures/mock/pipeline_audio_asr_intent.conf index 90b0f0ee..d9b4e273 100644 --- a/demo/fixtures/mock/pipeline_audio_asr_intent.conf +++ b/demo/fixtures/mock/pipeline_audio_asr_intent.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "demo/fixtures/mock/pipeline_audio_asr_intent.json", + "pipe_path": "pipeline_audio_asr_intent.json", + "io_binding": "audio_asr_intent.operator.v1", "model_paths": { "asr_model_v1": "./models/paraformer_asr_npu.bin" }, diff --git a/demo/fixtures/mock/pipeline_audio_asr_intent_cabi.json b/demo/fixtures/mock/pipeline_audio_asr_intent_cabi.json new file mode 100644 index 00000000..ec608b09 --- /dev/null +++ b/demo/fixtures/mock/pipeline_audio_asr_intent_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_audio_asr_intent.json", + "io_binding": "audio_asr_intent.cabi.v1" + } +} diff --git a/demo/fixtures/mock/pipeline_dialogue_audit.conf b/demo/fixtures/mock/pipeline_dialogue_audit.conf index bcd04d30..23564392 100644 --- a/demo/fixtures/mock/pipeline_dialogue_audit.conf +++ b/demo/fixtures/mock/pipeline_dialogue_audit.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "demo/fixtures/mock/pipeline_dialogue_audit.json", + "pipe_path": "pipeline_dialogue_audit.json", + "io_binding": "compliance_audit.operator.v1", "model_paths": { "embed_model_v2": "./models/bge_m3_npu.bin", "rerank_model_v1": "./models/bge_reranker_large_npu.bin", diff --git a/demo/fixtures/mock/pipeline_dialogue_audit_cabi.json b/demo/fixtures/mock/pipeline_dialogue_audit_cabi.json new file mode 100644 index 00000000..cc37d772 --- /dev/null +++ b/demo/fixtures/mock/pipeline_dialogue_audit_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_dialogue_audit.json", + "io_binding": "compliance_audit.cabi.v1" + } +} diff --git a/demo/fixtures/mock/pipeline_doc_qa.conf b/demo/fixtures/mock/pipeline_doc_qa.conf index 4390e142..814e6de3 100644 --- a/demo/fixtures/mock/pipeline_doc_qa.conf +++ b/demo/fixtures/mock/pipeline_doc_qa.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "demo/fixtures/mock/pipeline_doc_qa.json", + "pipe_path": "pipeline_doc_qa.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_v1": "./models/bge_base_npu.bin", "llm_model_v1": "./models/qwen_1.5b_npu.bin" diff --git a/demo/fixtures/mock/pipeline_doc_qa_cabi.json b/demo/fixtures/mock/pipeline_doc_qa_cabi.json new file mode 100644 index 00000000..4fca4249 --- /dev/null +++ b/demo/fixtures/mock/pipeline_doc_qa_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_doc_qa.json", + "io_binding": "doc_qa.cabi.v1" + } +} diff --git a/demo/fixtures/mock/pipeline_doc_qa_custom.conf b/demo/fixtures/mock/pipeline_doc_qa_custom.conf index 29d6be4f..8e2af7fc 100644 --- a/demo/fixtures/mock/pipeline_doc_qa_custom.conf +++ b/demo/fixtures/mock/pipeline_doc_qa_custom.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "demo/fixtures/mock/pipeline_doc_qa_custom.json", + "pipe_path": "pipeline_doc_qa_custom.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "llm_model_v1": "demo/fixtures/mock/artifacts/neutral-llm.fixture" }, diff --git a/demo/fixtures/mock/pipeline_doc_qa_custom_cabi.json b/demo/fixtures/mock/pipeline_doc_qa_custom_cabi.json new file mode 100644 index 00000000..7ab3661d --- /dev/null +++ b/demo/fixtures/mock/pipeline_doc_qa_custom_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_doc_qa_custom.json", + "io_binding": "doc_qa.cabi.v1" + } +} diff --git a/demo/fixtures/mock/pipeline_doc_qa_rerank.conf b/demo/fixtures/mock/pipeline_doc_qa_rerank.conf index eacaa060..2e36e34a 100644 --- a/demo/fixtures/mock/pipeline_doc_qa_rerank.conf +++ b/demo/fixtures/mock/pipeline_doc_qa_rerank.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "demo/fixtures/mock/pipeline_doc_qa_rerank.json", + "pipe_path": "pipeline_doc_qa_rerank.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_v1": "./models/bge_base_npu.bin", "rerank_model_v1": "./models/bge_reranker_large_npu.bin", diff --git a/demo/fixtures/mock/pipeline_doc_qa_rerank_cabi.json b/demo/fixtures/mock/pipeline_doc_qa_rerank_cabi.json new file mode 100644 index 00000000..7cd70ab5 --- /dev/null +++ b/demo/fixtures/mock/pipeline_doc_qa_rerank_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_doc_qa_rerank.json", + "io_binding": "doc_qa.cabi.v1" + } +} diff --git a/demo/fixtures/mock/pipeline_entity_extract.conf b/demo/fixtures/mock/pipeline_entity_extract.conf index 165faebf..bc64830e 100644 --- a/demo/fixtures/mock/pipeline_entity_extract.conf +++ b/demo/fixtures/mock/pipeline_entity_extract.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "demo/fixtures/mock/pipeline_entity_extract.json", + "pipe_path": "pipeline_entity_extract.json", + "io_binding": "entity_extract.operator.v1", "model_paths": { "entity_llm": "./models/qwen_0_6b_npu.bin" }, diff --git a/demo/fixtures/mock/pipeline_entity_extract_cabi.json b/demo/fixtures/mock/pipeline_entity_extract_cabi.json new file mode 100644 index 00000000..edb511c1 --- /dev/null +++ b/demo/fixtures/mock/pipeline_entity_extract_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_entity_extract.json", + "io_binding": "entity_extract.cabi.v1" + } +} diff --git a/demo/fixtures/mock/pipeline_entity_extract_custom.conf b/demo/fixtures/mock/pipeline_entity_extract_custom.conf index ea9245e7..9ea9bf93 100644 --- a/demo/fixtures/mock/pipeline_entity_extract_custom.conf +++ b/demo/fixtures/mock/pipeline_entity_extract_custom.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "demo/fixtures/mock/pipeline_entity_extract_custom.json", + "pipe_path": "pipeline_entity_extract_custom.json", + "io_binding": "entity_extract.operator.v1", "model_paths": { "entity_llm": "demo/fixtures/mock/artifacts/neutral-llm.fixture" }, diff --git a/demo/fixtures/mock/pipeline_entity_extract_custom_cabi.json b/demo/fixtures/mock/pipeline_entity_extract_custom_cabi.json new file mode 100644 index 00000000..ba1e2170 --- /dev/null +++ b/demo/fixtures/mock/pipeline_entity_extract_custom_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_entity_extract_custom.json", + "io_binding": "entity_extract.cabi.v1" + } +} diff --git a/demo/fixtures/mock/pipeline_ocr_doc_qa.conf b/demo/fixtures/mock/pipeline_ocr_doc_qa.conf index 3c2295cf..3c5fe5e8 100644 --- a/demo/fixtures/mock/pipeline_ocr_doc_qa.conf +++ b/demo/fixtures/mock/pipeline_ocr_doc_qa.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { - "pipe_path": "demo/fixtures/mock/pipeline_ocr_doc_qa.json", + "pipe_path": "pipeline_ocr_doc_qa.json", + "io_binding": "ocr_doc_qa.operator.v1", "model_paths": { "ocr_model_v1": "./models/ch_ppocr_v4_det_rec_npu.bin", "llm_model_v1": "./models/qwen_1.5b_npu.bin" diff --git a/demo/fixtures/mock/pipeline_ocr_doc_qa_cabi.json b/demo/fixtures/mock/pipeline_ocr_doc_qa_cabi.json new file mode 100644 index 00000000..b64478cb --- /dev/null +++ b/demo/fixtures/mock/pipeline_ocr_doc_qa_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_ocr_doc_qa.json", + "io_binding": "ocr_doc_qa.cabi.v1" + } +} diff --git a/demo/fixtures/mock/pipeline_translate.conf b/demo/fixtures/mock/pipeline_translate.conf new file mode 100644 index 00000000..5b6f4a9b --- /dev/null +++ b/demo/fixtures/mock/pipeline_translate.conf @@ -0,0 +1,17 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_translate.json", + "io_binding": "translate.operator.v1", + "outputs": { + "entity_out": { + "type": "entity_out", + "meta_num": 0, + "metadata_type_id": 0, + "capacities": { + "entities_json": 2047 + } + } + } + } +} diff --git a/demo/fixtures/mock/pipeline_translate.json b/demo/fixtures/mock/pipeline_translate.json new file mode 100644 index 00000000..bcdfe5d5 --- /dev/null +++ b/demo/fixtures/mock/pipeline_translate.json @@ -0,0 +1,37 @@ +{ + "biz_name": "translate_v1", + "models": [ + { + "capability": "llm", + "model_type": "test_biz_llm", + "backend": "test_causal_lm_backend", + "model_config": { + "max_batch_size": 2, + "max_seq_len": 512 + }, + "backend_config": {}, + "model_id": "translate_llm", + "model_path": "demo/fixtures/mock/artifacts/neutral-llm.fixture" + } + ], + "pipeline": [ + { + "id": "node_0_LlmGenerateNode", + "node_type": "LlmGenerateNode", + "depends_on": [], + "ports": { + "inputs": { + "prompt": "input_sentences" + }, + "outputs": { + "text": "llm_answers" + } + }, + "config": { + "bind_model": "translate_llm", + "max_tokens": 64, + "temperature": 0.0 + } + } + ] +} diff --git a/demo/fixtures/mock/pipeline_translate_cabi.json b/demo/fixtures/mock/pipeline_translate_cabi.json new file mode 100644 index 00000000..0feda0e3 --- /dev/null +++ b/demo/fixtures/mock/pipeline_translate_cabi.json @@ -0,0 +1,7 @@ +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_translate.json", + "io_binding": "translate.cabi.v1" + } +} diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 25b2d070..290411e0 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2026-09-16 输入输出转换独立化与接入绑定架构(RFC-0059) + +- **输入输出转换解耦与注册中心**: + - 将一体化业务适配器(`IBizAdapter`)与桥接(`OperatorBizBridge`)彻底解构为独立的 `InputConverter`、`OutputConverter` 及编排绑定 `IoBinding`。 + - 引入 `IoConverterRegistry` 与 `IoBindingRegistry` 单例注册中心,支持细粒度输入校验、归一化与多槽位输出组装。 + - 发布部署配置 Schema 1,在 `.conf` 中引入显式 `io_binding` 配置,统一接入与算子运行期绑定解析。 +- **C ABI 与 Operator 架构收敛**: + - 公共 C ABI 结构体 `CompanyAlgParamCreate` 移除历史 `biz_type` 字段,以 deployment config 中的 `io_binding` 作为唯一定位依据;C ABI major 升级为 6(`SOVERSION 6`,产品版本保持 v10.0.0)。 + - 下线历史 `CompanyAlgBizType`、`BizAdapterRegistry`、`OperatorBizBridgeRegistry` 及其桥接实现。 + - Operator 与 C ABI 统一基于共享算法运行时进行零拷贝转换与有界输出池生命周期管理,保持严格分层与向下依赖。 + ## 2026-09-15 上线前代码与测试精简(批次 0、1、2) - **测试装配与覆盖收口(批次 0 / V1)**: diff --git a/doc/README.md b/doc/README.md index 2d3f8046..5f261f7c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -47,6 +47,7 @@ | --- | --- | | 尚待完成的开发者试用与生产验收 | [方案开发者验收计划](plans/solution_developer_acceptance.md) | | 上线前精简代码、兼容分支与重复测试 | [代码与测试精简实施计划(2026-09-15)](plans/prelaunch_simplification_2026-09-15.md) | +| 独立复用输入、输出转换器,隔离外部结构与内部流程 | [输入输出转换独立化:详细设计与实施计划(2026-09-15)](plans/adapter_io_layout_design_2026-09-15.md)(待实施) | | 降低方案编排心智负担的实施顺序与验收 | [RFC-0057:Pipeline 编排体验](rfcs/0057-pipeline-composition-experience.md)(In Implementation) | | 架构与接口为何这样设计 | [RFC 索引](rfcs/README.md),优先列出进行中的 RFC | | 用户可感知的版本变化 | [Changelog](CHANGELOG.md) | diff --git a/doc/architecture.md b/doc/architecture.md index 3f8f985f..3ddb3d0c 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -170,7 +170,7 @@ C++ Operator API:NamedIoBatch + Operator 镜像 C 结构 ─┘ weak lifetime token,避免 Destroy 后解引用已释放句柄或池。 - 值类型表、业务桥接表和内存池只属于接入适配层,不得进入 Blackboard、Node、Model 或 Backend。 - 目标共享库输出名称为 `company_alg_sdk`,产品 VERSION 为 10.0.0, - SOVERSION/C ABI major 为 5。 + SOVERSION/C ABI major 为 6。 - v4 Create 和配置预检都以必填部署根 `model_path` 加相对 `cfg_file_name` 解析; `.conf` 的 `data.outputs` 按逻辑槽位归一化输出类型、分配方案、参数与容量; 最外层的独立配置读取组件按固定枚举提取配置并返回字符串,注册方案在 Create diff --git a/doc/developer_guide.md b/doc/developer_guide.md index 6e9fa533..3f94daf6 100644 --- a/doc/developer_guide.md +++ b/doc/developer_guide.md @@ -64,7 +64,7 @@ Create 期固定池分配,Process 只向空输出槽位提交池化 shared_ptr Blackboard、Node、Model 或 Backend 才能识别 Operator 结构的方案均违反分层要求。 目标交付共享库为 `company_alg_sdk`,产品 VERSION 为 10.0.0, -SOVERSION/C ABI major 为 5。 +SOVERSION/C ABI major 为 6。 其正式动态符号面固定为 6 个 `Alg_*`、3 个 `AlgBase_*` 和 3 个 Operator 入口; 仓库内 Node、Registry、Model、Backend 和第三方运行时是隐藏实现,不得被外部扩展直接链接。 Operator v4 的 Create 和配置预检都使用部署根 `model_path` 加相对 diff --git a/doc/plans/adapter_io_layout_design_2026-09-15.md b/doc/plans/adapter_io_layout_design_2026-09-15.md new file mode 100644 index 00000000..268954f6 --- /dev/null +++ b/doc/plans/adapter_io_layout_design_2026-09-15.md @@ -0,0 +1,698 @@ +# 输入、输出转换独立化:详细设计与实施计划 + +- 更新日期:2026-09-15。 +- 源码基线:`e6aa776c959aa84f5eec20bf38e73c313718e01a`。 +- 状态:**已实施**。 +- 目标:输入转换器与输出转换器独立复用;外部结构变化被限制在接入适配层,Pipeline 与 Node 只处理框架内部数据。 +- 范围:转换器、接入绑定、创建配置、运行时装配、Operator 接入、Catalog、验证及仓内消费者迁移。 +- 实施流程:[CONTRIBUTING.md](../../CONTRIBUTING.md);现行约束:[AGENTS.md](../../AGENTS.md)。 + +本文只维护最终目标设计及实施步骤。文中的新类型、注册项、配置字段和工具命令均为**待实现接口**,不能当作当前可用能力。 +源码实施涉及注册契约、公开 ABI 和运行时边界,必须先按现行流程登记 RFC,引用本文的详细设计和验收要求。 +本次文档交付不提前宣告 RFC 或源码实现完成。 + +**上线前一次迁移:不向旧版本兼容,不保留两套设计。** 仓内调用方、配置、示例、工具与测试同批切换, +创建时必须显式指定接入绑定。删除旧配置自动识别、缺省绑定、旧业务枚举选择、兼容包装及旧注册/执行路径。 +旧配置与旧 ABI 不属于新版本支持范围。C ABI 与 Operator 是两个实际宿主入口,共用一套组件机制,不是新旧版本双轨。 + +## 1. 最终设计 + +### 1.1 三个可组合部分 + +```text +选定的输入转换器 → Pipeline → 选定的输出转换器 +``` + +- 输入转换器:读取明确的外部结构与请求协议,校验并复制数据,发布到框架内部 typed ports。 +- Pipeline:通过内部数据类型和逻辑端口执行算法,保持现有四层依赖方向。 +- 输出转换器:读取框架内部结果,完成响应字段选择、组装、序列化及受容量约束的外部写入。 + +输入转换器不引用输出转换器;输出转换器不读取原始外部输入。两者依靠内部端口、来源编号和明确的请求元信息协作。 +接入绑定声明这三个部分如何配合,并在创建实例时完成兼容性校验。 + +### 1.2 两条目标数据流 + +```text +C ABI 调用方 + → C ABI 通用入口:句柄、批次与调用边界 + → input:外部 C 请求 → 内部数据 + → Pipeline / Nodes + → output:内部结果 → 外部 C 响应 + → C ABI 调用方 + +Operator 调用方 + → Operator 通用入口:命名槽位、宿主类型、输出池 + → input:Operator 外部请求 → 内部数据 + → Pipeline / Nodes + → output:内部结果 → 已租用的 Operator 输出结构 + → Operator 通用入口:发布输出与管理租约 + → Operator 调用方 +``` + +两种入口统一调用 Decode/Encode;宿主结构读取、容量约束和发布方式按入口特化,详见第 6、9 节。 +不为复现旧错误优先级增加额外公开回调、历史阶段或策略开关。 + +### 1.3 Operator 的最终职责 + +Operator 保留槽位解析、ValueType 校验、输出分配策略、池预算、租约、发布、释放和句柄并发控制。 +具体外部字段转换全部由 input/output 承担。 + +**最终目录中不保留业务 `operator/bridges/`。** 当前业务 bridge 的输入字段转换迁入 input,输出字段转换迁入 output, +槽位描述迁入转换器 Definition 与接入绑定。通用 Operator 代码根据已验证的绑定调用组件。 +Operator 输入不再为了复用业务算法而强制构造另一份公共 C 结构;它可以在输入组件内使用自持有的中性快照。 + +输出组件可以在内部暂存自持有响应,以完成整批检查和宿主写入;该对象由输出组件管理, +不成为 Pipeline/Node 的数据类型,也不成为独立业务桥的输入契约。 + +## 2. 复用规则与隔离边界 + +| 复用对象 | 允许的复用 | 判断依据 | +| --- | --- | --- | +| 输入转换器 | 多个业务或 Pipeline 使用同一个转换器 ID | 外部类型、请求 schema、字段语义与产出的内部端口匹配 | +| 输出转换器 | 多个业务或 Pipeline 使用同一个转换器 ID | 所需内部结果、响应 schema、宿主类型与容量策略匹配 | +| 载体读写 helper | 不同转换器共享字符串、数组、PCM、缓冲区访问代码 | 类型及内存布局一致;helper 不替代业务协议校验 | +| 语义转换函数 | 不同宿主类型共享请求解释或结果组装函数 | 中性字段和协议语义一致 | +| Pipeline | 不同输入、输出格式使用同一 Pipeline JSON | 各组合都通过内部契约与数据流校验 | + +相同 `const char*` 或同名 C 字段不保证 payload 协议相同。例如纯文本与 `{"query":"..."}` 可以复用载体读取代码, +但必须分别应用对应的请求解释规则。类型名、schema ID 和 schema 版本都应进入 Definition。 + +隔离必须满足: + +1. 平台结构、外部字段名、输出池对象不进入 Core、Node、Model 或 Backend。 +2. 输入发布到 `AlgContext` 的是拥有自身数据的内部值;保持当前 copy-in 规则,不引入跨调用借用。 +3. 外部请求 ID 保存为请求级元信息;内部 `req_id/sub_id` 继续表示批内来源,输出负责恢复外部编号。 +4. 需要透传的信息必须成为声明过的中性请求元信息,output 不能回读外部指针或不透明宿主对象。 +5. 转换器不可持有请求状态。Definition、绑定计划共享且不可变;快照、编码暂存和租约属于本次调用。 +6. 新外部 schema 由 SDK 转换器完整实现,Demo/Python 只构造载体、调用和显示结果。 + +## 3. 目标源码布局 + +```text +include/adapter/ +├── io_converter.h # 转换器 Definition、调用契约 +├── io_binding.h # 绑定 Definition、只读查询契约 +├── converter_authoring.h # typed helper 与注册入口 +└── ... # 保留仍有使用者的共享契约 + +src/adapter/ +├── input/ +│ ├── README.md +│ ├── text_input.cpp +│ ├── translate_json_input.cpp +│ ├── doc_query_input.cpp +│ ├── rerank_input.cpp +│ ├── audit_input.cpp +│ ├── audio_input.cpp +│ ├── image_query_input.cpp +│ └── ... # 相邻私有头与读取 helper +├── output/ +│ ├── README.md +│ ├── structured_document_output.cpp +│ ├── translation_json_output.cpp +│ ├── keyword_result_output.cpp +│ ├── doc_answer_output.cpp +│ ├── rerank_result_output.cpp +│ ├── audit_result_output.cpp +│ ├── audio_result_output.cpp +│ ├── invoice_result_output.cpp +│ └── ... # 相邻私有头、编码暂存与写入 helper +├── biz/ +│ ├── README.md +│ ├── translate_bindings.cpp +│ ├── doc_qa_bindings.cpp +│ └── ... # 内部业务契约与接入组合声明 +├── operator/ +│ ├── operator_adapter.cpp +│ ├── operator_process_binding.cpp/.h +│ ├── operator_config_resolver.cpp/.h +│ ├── operator_output_pool.cpp/.h +│ └── ... # ValueType、配置、Control 等通用机制 +├── io_converter_registry.cpp/.h +├── io_binding_registry.cpp/.h +├── io_binding_resolver.cpp/.h +├── io_catalog.cpp/.h +├── deployment_io_config.cpp/.h +├── c_api_adapter.cpp +└── shared_algorithm_runtime.cpp/.h +``` + +目录按转换方向区分,文件按协议或转换操作命名。一个文件可以注册多个使用相同实现的宿主特化版本, +不要求为每个注册项新建文件。复杂协议可以在 input/output 内按操作建立子目录,当前八个业务不强制增加层级。 + +`biz/` 只声明内部业务契约和输入/输出组合,不实现字段转换,不包含转换器私有头,使用稳定 ID 引用注册项。 +公共 C 头仍在 `include/edgeflow/`,平台替身仍在 `include/platform_mock/`。 +新 C++ 扩展声明按使用范围进入 `include/adapter/`;注册表内部、快照和响应暂存类型放在 `src/adapter/`。 + +## 4. 组件与接入绑定的数据模型 + +以下为目标字段与约束。实施时复用现有 typed port、诊断和注册设施,不另建通用反射框架。 + +### 4.1 输入、输出 Definition + +| Definition | 必需信息 | +| --- | --- | +| `InputConverterDefinition` | 唯一 `converter_id`、`transport`、请求 `schema_id/schema_version`、外部输入槽与类型、内部逻辑输出端口、批次上限、copy-in 与线程策略 | +| `OutputConverterDefinition` | 唯一 `converter_id`、`transport`、响应 `schema_id/schema_version`、消费的内部逻辑端口、外部输出槽与类型、输出基数、批次上限、容量与线程策略 | + +`transport` 本轮只有 `cabi`、`operator`。每个注册项选择一种入口,ID 可以包含入口后缀, +例如 `plain_text.entity_cabi.v1`、`plain_text.entity_operator.v1`;这些是建议的新 ID,不是当前 Catalog 内容。 +两种入口可共享函数,但外部类型或资源契约不同的注册项必须分别声明。 + +转换器 Definition 不含 `BizType`、`biz_name` 或对端转换器 ID,不持有 Pipeline 实例。 +内部端口使用逻辑名称和 typed Definition。类型、基数、来源与生命周期沿用现有端口机制。 +外部槽位定义包含逻辑槽名、准确的宿主类型 ID、方向、必需性;Operator 槽位还引用 ValueType 的规范后缀。 +单个注册项不得使用一个裸指针入口接收多种未标识布局,也不得按字段内容猜测结构体类型。 + +### 4.2 `IoBindingDefinition` + +一个绑定描述某一入口上完整的外部请求/响应组合: + +| 字段 | 含义 | +| --- | --- | +| `binding_id` | 全局唯一的接入组合 ID | +| `biz_name` | 内部业务契约;与目标 Pipeline 精确匹配 | +| `transport` | `cabi` 或 `operator` | +| `input_converter_id` / `output_converter_id` | 分别选择已注册转换器 | +| `input_ports` | 输入转换器逻辑输出端口 → 实际 Blackboard key | +| `output_ports` | 输出转换器逻辑输入端口 → 实际 Blackboard key | + +绑定 ID 表达完整协议组合。切换输出 schema 使用另一个绑定 ID,不在原 ID 下静默改变响应格式。 +多个绑定可以引用相同 `biz_name`、相同输入 ID 或相同输出 ID。 + +本轮通过已注册绑定声明组合组件,创建配置选择 `binding_id`,不在 Process 请求内自由拼接转换器。 +新增组合通常只需增加声明与配置,不复制输入/输出算法。未来若允许部署文件直接声明组合,仍须走相同解析与验证入口。 + +### 4.3 业务契约只注册一次 + +`BizDefinition` 继续描述 Pipeline 的内部 ingress/egress。注册从 `BizAdapterRegistry::RegisterAdapter` +的附带动作移出,由 `biz/*_bindings.cpp` 的业务声明独立完成,复用 `PipelineCatalog::RegisterBizDefinition`。 + +同一个 `biz_name` 只允许一个 Definition;多个接入绑定引用它,不能重复注册或以“定义一样”为由覆盖。 +转换器注册不注册业务契约。同一 Pipeline 因此可以绑定不同外部结构,无需复制 Pipeline 或改写其 `biz_name`。 +完整外部输入/输出约定由 binding 加两侧 schema 共同描述,Catalog 的内部端口不能替代该约定。 + +### 4.4 注册表与全量审计 + +一个 `IoConverterRegistry` 管理输入、输出两个类型化集合,一个 `IoBindingRegistry` 管理绑定。 +作者入口采用 Spec、普通函数和 typed helper,不引入多层转换器继承体系。 + +全局初始化必须拒绝: + +- 重复 converter ID、binding ID、biz_name,以及缺失的必要回调或 schema 元数据。 +- 绑定引用不存在的业务或转换器、入口类型不一致、端口映射不合法。 +- 必需接入能力缺失,以及绑定的业务契约、宿主类型和组件声明不一致。 +- Operator 槽位类型、方向、规范后缀、分配/重置/释放能力不完整。 + +当前八个业务的 C ABI 与 Operator 接入能力都要完成迁移。生产业务集合由每个 `biz/*_bindings.cpp` +中独立的 `BizExposureDefinition` 声明,字段为 `biz_name`、`max_batch_size` 和 `required_transports`, +与 BizDefinition 同时登记,不从已有绑定反推应有集合。 +每个业务的每种必需入口至少存在一个有效绑定;可以有多个,但调用方必须明确选择 binding_id。 +不建立默认项、业务枚举索引或根据 biz_name 自动选择绑定的逻辑。内部纯 Pipeline 测试契约可不声明生产 exposure。 +已注册但暂未被绑定使用的通用转换器允许存在,不能按“孤儿业务桥”的旧规则拒绝可复用组件库。 +每个声明过的绑定必须完整,即使暂未被选中,也应在全量审计中检查其有效性。 + +## 5. 内部端口绑定与类型安全 + +输入转换器产出逻辑端口,例如 `texts`;绑定将它映射到现有业务实际 key,例如 `input_sentences`。 +输出转换器声明 `answers` 等逻辑端口,由绑定选择实际读取的 key。 +具体 key 与类型必须从当前 Definitions/Catalog 查询,不能依据文件名猜测。 + +创建时将映射解析为不可变的 `InputPortBindings`、`OutputPortBindings`,运行时使用类似 +`bindings.Key("texts")` 的 typed helper。这表示目标接口约束:类型在创建时核对, +运行时不靠 `std::any` 猜类型,不允许通过未检查的字符串访问任意黑板值。 + +请求来源由共享输入机制写入本次 `AlgContext`,复用 `kRawRequestIds` 及已有 provenance 规则。 +外部重复 request ID 仍允许;内部按批内编号和子编号关联,不能用外部 ID 当唯一哈希键。 + +对于输入额外发布、输出实际未消费的现存端口,保留实际行为与业务 Definition: + +- CrossRerank 当前额外发布 `kRerankPairs`,输入转换器明确记录该写入;不删除实际数据,也不悄悄改原 Catalog ingress。 +- ComplianceAudit 的业务 Definition 声明 `kRuleMatches`,当前打包不读取它;输出按实际读取声明 consumes,Pipeline 原 egress 要求仍保留。 + +创建校验分别处理“业务契约要求”和“转换器实际读写”,不复制一份不准确的元数据。 + +## 6. 转换器调用接口与复用实现 + +### 6.1 输入组件:统一 Decode + +两种入口的输入转换器都提供同一形状的回调,按已绑定的具体宿主类型生成 typed wrapper: + +```cpp +int DecodeInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status); +``` + +| 类型/阶段 | 契约 | +| --- | --- | +| `ExternalInputBatchView` | 本次同步调用有效的只读视图,每槽带已解析类型 ID;typed accessor 核对绑定类型后访问,不拥有调用方内存 | +| `InputDecodeOptions` | 创建时确定的批次/宿主限制与诊断上下文,不含输出组件 | +| Decode | 有界读取外部字段、校验完整 payload、复制数据并发布内部值;遇到错误立即停止 | + +仅 Decode 成功后运行 Pipeline;失败时销毁本次 AlgContext,因此无需为恢复旧发布次序另建事务/回放机制。 +输入必须在读取前检查指针、长度和乘法溢出,不能先无界复制再补校验。 +转换器内部可用自持有文本、文档/问题、PCM、图片引用记录复用语义函数,但不建立框架级 PreparedInputBatch 或宿主 Prepare 阶段。 +不创建公共 C shadow DTO 作为 Operator 到内部数据的必经中转。 + +普通文本解释函数可服务多个外部读取器,JSON 解释函数负责自己的 schema。 +每个 schema 明确可选字段空值、长度、内嵌 NUL 和 UTF-8 规则;有业务意义的约束必须保留。 +校验次序以第 9 节新约定为准,不承诺复现旧入口对多个同时非法字段的报错顺序。 + +### 6.2 输出组件:共享语义、宿主特化写入 + +```cpp +int EncodeOutput(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, + AdapterStatus* status); +``` + +输出组件在 Encode 内按实际需要创建局部自持有响应,不公开 ReserveOutputScratch 或 OutputScratch 阶段。 +written_count 在入口置零,成功时返回写入的请求数;运行时按绑定基数验证后才发布成功结果。 + +`ExternalOutputBatchView` 是已有目标内存的借用视图,带槽位类型和容量信息,不授予转换器发布租约的能力。 +输出组件只写入已提供的字段空间,不能分配另一套输出池、返回临时字符串指针或直接给用户 map 发布 `shared_ptr`。 +`OutputEncodeOptions` 保存创建时确定的诊断上下文与调用限制,不含输入转换器或外部输入指针。 +两侧 options 的诊断上下文使用 binding_id、converter_id 和阶段;不保留旧 Adapter 名称的错误格式兼容配置。 + +结果选择、来源关联和响应组装使用一份共享语义实现,按宿主接入不同 writer: + +- C ABI 特化写入调用方固定结构,按声明容量拒绝截断。 +- Operator 特化先用同一 helper 构造自持有响应,再写入已租用的宿主结构;字符串容量由 `ResolvedOutputPoolSpec` 决定。 +- 暂存响应与宿主字段写入都属于该输出组件,其注册项独立于输入转换器和业务绑定。 +- 可复用现有 Integration Result 的存储形状;最终不再通过 `IBizAdapter::ResultTypeName()` 绑定到 Operator 业务桥。 + +**禁止采用固定 C 输出作为 Operator 输出的中转。** 超过 C 数组上限、但在 Operator 池容量范围内的结果仍应成功。 +C ABI 调用失败时输出视为无效,调用方必须丢弃;不承诺历史部分写入位置,也不要求所有输出字节完全不变。 +Operator 通过租约及最后统一发布保证调用方看不到部分结果。 + +### 6.3 接口拆分约束 + +模板定义留在 output 私有头,或在 `.cpp` 显式实例化实际使用的目标类型;不把业务模板暴露给 Core/Node。 +转换器注册不依赖业务绑定,绑定通过 ID 选择组件。 +统一返回码、诊断类别和阶段映射,不为历史返回码/诊断不一致保留特殊分支。 +复用适当的命名错误常量;输入或结果语义非法、容量不足、内部异常分别给出一致类别和具体字段路径。 + +## 7. 唯一创建配置与公开接口迁移 + +### 7.1 选择方式 + +配置只选择完整 `io_binding` ID。输入、输出在绑定声明中分别指定,可以独立复用; +创建后冻结选择,不支持通过 `Alg_Control` 或单次请求切换外部布局。 + +`io_binding` 必填且非空;缺失、未知或不匹配时失败,不根据 biz_type、biz_name、文件名或宿主类型推断。 +不把 `io_binding`、外部类型或转换器参数放进 Pipeline JSON,Pipeline 继续只描述内部业务和数据流。 + +### 7.2 C ABI 创建 + +`CompanyAlgParamCreate` 删除 `biz_type`,保留配置路径、模型根与设备参数。config_file_path 只接受接入配置: + +```json +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_translate_cpu.json", + "io_binding": "translate.cabi.v1" + } +} +``` + +配置 schema 1 是这套接入配置的版本号,独立于 Catalog 和 SDK ABI 版本。 +根只允许 schema_version=1 和 data;data 必须含 pipe_path、io_binding,可选 model_paths。 +`outputs` 属于 Operator 池配置,C ABI 不接受。直接传入原始 Pipeline JSON、旧配置、未知版本/字段或空绑定一律报配置错误。 +不实现旧格式识别、自动升级、尝试其他格式或默认绑定路径。 + +两种入口的 `pipe_path` 均相对接入配置文件目录解析,规范化后限制在该目录及其子目录;模型根与配置根分别处理。 +`model_root_dir` 保持“直接包含模型资产的目录”含义,不要求 Pipeline 文件位于模型目录。 +可选 `model_paths` 先按已声明 model_id 应用覆盖;未知 ID、非法类型拒绝。相对模型路径按非空 model_root_dir 解析且不得逃逸, +model_root_dir 为空时模型路径必须为绝对路径。接入配置可放在 configs/,让 pipe_path 引用同目录 Pipeline。 +接入配置在 Integration 消费完毕,传给 Core 的只能是已加载的纯 Pipeline JSON。 + +不同结构使用不同绑定 ID,调用方必须按所选契约构造对象。创建参数和运行时不再保存旧业务枚举。 +现有 `const void**/void**` 无法证明指针真实指向哪种结构。类型元数据只检查配置一致性,不能宣称已校验任意指针的实际布局; +不得读取未知对象内容进行类型探测,也不能根据 Pipeline 静默切换 C 结构。 + +### 7.3 Operator 创建 + +Operator 使用同一个接入配置 schema,只增加由宿主资源职责决定的必需 outputs: + +```json +{ + "schema_version": 1, + "data": { + "pipe_path": "pipeline_translate_cpu.json", + "io_binding": "translate.operator.v1", + "outputs": { + "entity_out": { + "type": "entity_out", + "capacities": {"entities_json": 4096} + } + } + } +} +``` + +正式槽名、类型、容量字段必须由所选绑定和目标构建的 ValueType 定义核对。 +cfg_file_name 在 model_path 资源根内定位接入配置;配置内部 pipe_path 按配置目录解析,与 C ABI 一致。 +model_paths 按入口传入的资源根解析,Operator 使用 model_path。保留资源逃逸检查、输出分配白名单和池预算。 +所有现有 conf 同批增加 schema_version、io_binding,并迁移相对路径;缺字段的旧配置不会自动获得绑定。 + +输入槽来自输入转换器,输出槽来自输出转换器,输出分配配置必须与后者一致。 +`type` 不能绕过绑定改变外部结构;未知额外槽、重复匹配、缺必需槽保持拒绝。 + +### 7.4 ABI 与调用方一次迁移 + +这次不保留旧 ABI 兼容。更新 COMPANY_ALG_ABI_VERSION、SDK/SOVERSION 和相应导出/版本断言,所有仓内调用方重新编译。 +六个 Alg_* 入口继续承担 C11 调用边界与 noexcept/catch 屏障;其必要性不来自旧版本兼容。 +创建参数删除业务枚举,旧布局不提供转接函数或 V1/V2 双入口;实际业务输入/输出结构按新绑定声明继续使用。 + +公开 C++ 辅助接口 `ValidateOperatorConfigBinding` 的 expected_biz_type 参数改为必填、非空的 +`const char* expected_binding_id`,精确核对配置的绑定,不提供旧整数重载或“0 表示不比较”的模式。 +同步 `scripts/check_sdk_exports.sh` 中的符号、Demo 注册/预检、C11/ABI 测试和所有创建调用。 + +删除 SharedAlgorithmRuntime、ResolvedOperatorConfig、Operator 句柄和 RuntimeOptions 中仅用于旧选择的 biz_type。 +仓内已无消费后删除 CompanyAlgBizType 及其映射 helper;Pipeline 的 biz_name 仍是内部契约 ID,Control 继续按 cmd_id/node_id 路由。 +Demo 通过显式接入绑定及声明的载体匹配,不能取消防止错误宿主结构被传入的检查。 + +## 8. 创建解析、验证与 Catalog + +### 8.1 分工与一次性计划 + +```text +读取唯一接入配置 schema +→ 选择绑定、解析转换器 Definition 与宿主槽位 +→ 解析内部端口映射及输出分配配置 +→ PipelineValidator 验证内部 Pipeline 与选定 I/O 边界 +→ 生成不可变接入计划与 ValidatedPipelinePlan +→ 创建模型/Node 与宿主资源 +→ 返回可用句柄 +``` + +`IoBindingResolver` 位于 Integration,负责 schema、宿主类型、显式绑定 ID、ValueType 和容量分配配置校验。 +它将转换器实际产生/消费的端口映射转换为**中性的内部边界描述**,交给现有 `PipelineValidator`。 + +`PipelineValidator` 仍是内部数据流验证与规划的唯一实现。增加接受可选中性 I/O 边界约束的验证入口, +复用 `ValidatePortFlowContract`、键类型、生产者和 egress 检查,不在 Integration/Studio 再写一套 DAG/端口验证。 +Core 只看到 key、type、cardinality、provenance、lifetime 等中性字段,不包含转换器类、C 类型或 Operator 槽位对象。 + +至少检查: + +1. 输入发布覆盖业务必需 ingress;类型及 flow contract 匹配;额外实际写入也与 Pipeline 生产者一起核对。 +2. 输出必需读取由 Pipeline 或已声明请求元信息提供,每个逻辑端口有唯一合法映射。 +3. 原业务必需 egress 仍完整,即使选定输出只消费其中一部分。 +4. 禁止重复生产者、不兼容基数、错误来源或生命周期。 +5. 本轮生产绑定为每请求一个外部结果;候选展开和多槽位保留内部来源规则。 +6. 静态校验不替代运行时缺值、重复/越界来源和实际容量检查。 + +`ValidatedIoPlan` 的批次上限取输入、输出及生产 exposure 上限的最小值;生产 exposure 明确声明业务允许的上限。 +本轮外部结果基数固定为每请求一个,因此预检所需输出数为输入数;Operator 多输出槽仍是每帧各槽一个对象。 +迁移原 `ValidateBatch`、`EstimateRequiredOutputs` 时复用已有预检 helper,将参数来源换成此计划, +不能因删除 IBizAdapter 而丢失空指针、负容量、数量回写或提前拒绝逻辑。非 1:1 外部结果属于后续单独设计范围。 + +运行时保存 `ValidatedIoPlan`:绑定与组件引用、typed 端口映射、宿主布局、调用限制及不可变验证计划。 +Pipeline 消费同一次验证生成的 `ValidatedPipelinePlan`,不得重复解析、排序或加载模型。 +必要时增加接受现成计划的创建/装配入口,仍由现有 Pipeline 状态机执行失败清理。 + +计划所有权明确如下:创建协调器在堆上构造 `unique_ptr`;验证结束后, +通过接收该 unique_ptr 的 Pipeline 装配入口转移所有权。Node 初始化前计划已位于稳定堆地址, +初始化后禁止复制、重建或移动其内容,不向 Node 传栈上计划或临时 config 指针。 +`ValidatedIoPlan` 自己拥有解析后的端口映射及不可变组件引用,不共同拥有或复制 Pipeline 计划, +也不保留创建协调器的局部引用。SessionContext 同样使用稳定堆对象,装配成功只移动拥有指针。 +失败清理和正常销毁均先停止/等待执行任务,再销毁 Node/执行资源,最后销毁其引用的 plan/session。 +转换器描述符在句柄存续期保持有效;输出池租约不依赖转换器局部响应存储。 + +创建错误按上述阶段确定优先级:先配置/绑定与数据流,再模型/Node/池。非法绑定不得触发模型装载。 +未知/缺失/非法绑定按配置错误返回,注册冲突按注册冲突返回;诊断必须标出 data.io_binding 等相关路径。 +只维护新错误约定,不恢复旧创建入口的字段选择、错误文本或组合错误次序。 + +### 8.2 Catalog 与工具 + +基线 Catalog 为 schema 3,实际查询包含 8 个 `bizs`、12 个 Node、6 个 Model。 +保留业务、Node、Model、Backend 的有效能力,Profile 更新为新接入配置;对外 Catalog 只提供 **schema 4**,增加: + +- `input_converters`:请求 schema、外部类型与内部逻辑输出端口。 +- `output_converters`:响应 schema、内部逻辑输入端口、输出基数及容量策略。 +- `io_bindings`:组件 ID、业务契约、入口、端口映射和有效批次限制,不提供默认绑定字段。 + +Integration 的 `io_catalog` 门面聚合转换器/绑定注册表与 Core `PipelineCatalog` 快照。 +Core 不反向读取 Integration 注册表;命令和 Studio 统一使用聚合入口,不维护两套公开 Catalog 定义。 +Core 快照作为中性数据,由聚合入口统一序列化 schema 4;不提供对外 schema 3 兼容输出或双版本切换。 + +新增目标命令: + +```bash +./build/alg_pipeline_tool validate-io <接入配置路径> --transport cabi +./build/alg_pipeline_tool validate-io --transport operator --model-root <资源根目录> +``` + +两种命令使用与 Create 相同的解析器和验证器,只解析声明与计划,不加载模型、不分配池。 +C ABI 校验必要时也接受 `--model-root`,与创建参数一致。结果输出解析后的 binding、组件 ID、端口映射、外部类型和结构化诊断。 +现有 `validate ` 继续验证纯内部 Pipeline,不声称验证外部协议。 + +同步 CLI、Studio 后端/消费者、Catalog 契约测试及相关 skill;配置字段和注册事实不能依赖文档手工白名单。 + +## 9. 请求生命周期与错误行为 + +### 9.1 Operator Process + +目标时序只有一套: + +1. 检查句柄、加锁,验证输入/输出帧数及批次上限。 +2. 解析全部输入、输出槽位,检查类型、必需槽和空输出 shared_ptr;ValueType 做宿主内存形状检查。 +3. 调用输入 Decode,有界读取和业务校验,发布 copy-in 的内部值;失败即终止,不租用输出池。 +4. 租用全部输出块并登记 guard;池资源取得后才启动 Pipeline,避免无输出空间时进行计算。 +5. 执行 Pipeline。 +6. 调用输出 Encode,检查实际结果数,写入已租用的目标。 +7. 全成功后,Operator 通用代码按两阶段机制发布输出。 + +不强制为错误优先级准备公共 C shadow DTO、自持有输入批次或空的输出 scratch。 +转换器若需要局部临时存储,由回调管理;引用不得逃逸。输入失败不加载额外请求资源、不执行 Pipeline。 +新阶段顺序就是错误优先级依据;同阶段按声明的槽位顺序与帧索引报告首错,不复刻旧校验交错顺序。 + +### 9.2 C ABI Process + +同句柄 Alg_Process/Alg_Control 串行,入口保留异常屏障。先检查句柄、数量、输入指针与调用方输出对象数量容量, +再调用同一个 Decode 机制、Pipeline、Encode,最后核对 written_count 并回写 num_outputs。 +num_outputs 必须非空;容量不足时回写所需数量并返回容量错误,其他失败回写 0;成功回写实际数量。 +输出对象数量容量检查先于执行,不能为得到所需对象数量启动 Pipeline;本轮 1:1 绑定所需数量等于输入数量。 +字符串、数组等字段的实际容量在 Encode 阶段检查,动态结果长度不能在执行前确定。 +字段容量不足时 num_outputs 仍回写所需输出对象数量,不表示所需字节数;诊断应标明失败字段及其容量约束。 + +失败时所有输出视为无效,调用方丢弃其内容,不承诺保留旧的部分写入位置或字段值。 +输入越界读取、字符串静默截断、无效来源被当作成功等行为仍禁止;这些是新实现的正确性要求。 + +### 9.3 输出池与异常 + +- 取得池块前准备租约记录容量,每个取得的块立即受 guard 管理。 +- Pipeline、Encode、数量检查或宿主写入失败,所有已租用块归还,用户输出槽仍为空。 +- 发布时先构造全部临时 `shared_ptr`,成功后统一填入 map;输出组件不能越过发布点。 +- 返回块保留 `reset_external`,支持复用;输出字段指向池内存,不指向快照或临时编码变量。 +- 输入、输出各阶段回调的异常由对应入口原有屏障映射;六个导出保持 `noexcept` 及两类 catch。 +- 调用方须在 Destroy 前等待请求完成并释放输出租约。输出 shared_ptr 不延长句柄/池内存的有效使用期,不允许 Destroy 后访问输出。 + +### 9.4 业务细节 + +| 业务/行为 | 功能与正确性要求 | +| --- | --- | +| Translate | 完整 JSON 请求、query 选择、其他字段处理、translated 响应、转义/内嵌 NUL 语义 | +| Translate 输出 | 完整序列化、转义与异常处理;返回码/诊断一致,无历史特例 | +| DocQA | 三路结果按来源正确对齐,缺失或错配拒绝 | +| Entity/OCR/Audit | 结构化失败/fallback 拒绝,成功响应编号正确 | +| CrossRerank | 候选展开、rank 连续性、original_sub_id、按请求聚合与数量规则 | +| Audio | PCM/采样约束、copy-in、转写与槽位来源关联 | +| 所有业务 | 外部重复 ID 可用,内部重复/越界来源拒绝,无请求共享状态 | + +## 10. 八个业务迁移清单 + +以下契约 ID 来自基线 Catalog。迁移时再次查询目标构建,保留期间合法新增的注册项。 +每个绑定文件注册一次 BizDefinition,并声明供配置显式选择的 C ABI、Operator 绑定。 + +| 原文件词干 | 原 biz_name | 输入实现 | 输出实现 | 绑定声明 | +| --- | --- | --- | --- | --- | +| `translate` | `translate_v1` | `translate_json_input.cpp` | `translation_json_output.cpp` | `translate_bindings.cpp` | +| `entity_extract` | `entity_extract_v1` | `text_input.cpp` | `structured_document_output.cpp` | `entity_extract_bindings.cpp` | +| `keyword_match` | `keyword_match_v1` | `text_input.cpp` | `keyword_result_output.cpp` | `keyword_match_bindings.cpp` | +| `doc_qa` | `smart_doc_qa_v1` | `doc_query_input.cpp` | `doc_answer_output.cpp` | `doc_qa_bindings.cpp` | +| `cross_rerank` | `dense_cross_rerank_scoring` | `rerank_input.cpp` | `rerank_result_output.cpp` | `cross_rerank_bindings.cpp` | +| `compliance_audit` | `dialogue_compliance_audit_v1` | `audit_input.cpp` | `audit_result_output.cpp` | `compliance_audit_bindings.cpp` | +| `audio_asr_intent` | `speech_audio_asr_intent_slot` | `audio_input.cpp` | `audio_result_output.cpp` | `audio_asr_intent_bindings.cpp` | +| `ocr_doc_qa` | `multimodal_ocr_invoice_qa` | `image_query_input.cpp` | `invoice_result_output.cpp` | `ocr_doc_qa_bindings.cpp` | + +每个原 Adapter:业务契约移到绑定文件,转换约束进入两侧 Definition;两入口的读取与业务解释均进入统一 Decode, +复用合适的底层函数;结果处理进入 output 私有 helper,按新执行顺序组织。 +原 bridge 的字段读写迁入两侧组件,槽位与资源声明进入新 Definition。 + +现有八个业务功能及完整请求/响应示例继续作为产品验收依据;配置、创建参数和诊断统一迁移,不保留旧版运行承诺。 +共享源文件不表示协议相同,Entity、Keyword 的载体读取仍按各自类型声明。 + +最终删除已迁空的业务 Adapter 类/注册、八个业务 bridge,以及仅服务旧耦合关系的 +BizAdapterRegistry、OperatorBizBridgeRegistry、IBizAdapter 运行时选择链。 +共享 helper/authoring 模板迁移使用者后按引用决定保留、改名或删除;不增加兼容包装、旧接口重载或双执行路径。 +仓内扩展、示例、测试、生成器使用旧接口的部分必须同范围迁移;源码扩展消费者需要重新编译。 + +## 11. 构建、依赖检查与文档同步 + +### 11.1 构建与初始化 + +新增转换器、绑定、注册表、配置解析采用显式 `target_sources`,纳入 `edgeflow_integration_objects`。 +`shared_algorithm_runtime.cpp` 保持现有 `edgeflow_composition_objects` 归属。 +不按每个转换器建立库,不使用递归 glob 替代明确编译清单。 + +SDK、生产工具和测试工具链接同一组生产注册对象,防止静态裁剪导致 Catalog 与运行时不同。 +测试专用注册只进入测试目标,不能用测试工具绕过生产缺失注册。 +全局初始化取得完整注册快照后审计业务、转换器、绑定和 ValueType,保持冲突原因稳定、幂等和线程安全。 + +### 11.2 层隔离 + +更新 `scripts/check_layer_dependencies.py` 与 `scripts/check_layer_isolation.sh`: + +- input、output、biz 及其私有头保留业务转换代码禁止直接依赖 Node/Model/Backend 的规则。 +- 保留通用装配的必要依赖,不放宽整个 Integration。 +- Core/Node 不可包含新转换器、绑定和宿主私有头;Core 中性验证约束不含 Integration 类型。 +- 增加三个目录嵌套 `.cpp`、私有 `.h` 的正反例,覆盖相邻头间接引入非法依赖。 + +`cmake_ext/LayerHeaderViews.cmake` 按职责构建 include view。扩展头只在 extension/Integration 范围可见, +私有头只在内部范围可见;在现有 `test_layer_header_views.cmake` 增加真实新头的编译正反例。 +同步头清单与导出检查,公共 C11 视图不得引入 STL 或转换器声明。 + +### 11.3 活跃文档与消费者 + +源码实施完成时同步更新,不提前将目标能力写成现状: + +- `AGENTS.md`:Integration 组件职责、注册、完整接入审计,保持四层依赖方向。 +- `doc/dev_guide/source_layout.md`、`business_onboarding.md`、`adapter_templates/README.md`。 +- `doc/developer_guide.md`、`doc/architecture.md` 及受影响架构图源文件。 +- `doc/solutions/translate.md`:完整两入口请求/响应、唯一接入配置与显式绑定示例。 +- `.agents/skills/llm-edgeflow-developer-guide/references/integration.md`、`orchestration.md`。 +- `.agents/skills/pipeline-composer/` 的配置与 Catalog 说明。 +- `.agents/skills/json-prompt-solution/SKILL.md` 及仓内 authoring/recipe 使用者。 +- CLI、Studio、Demo、Catalog 消费者,`tests/README.md`、CTest 与 inventory。 +- `include/platform_mock/alg_types.h`、公开 Operator 辅助声明、Demo registry/runner、SDK 版本与导出清单、C11/ABI 及 real-model E2E 创建调用。 + +所有 SDK 创建调用的配置都迁为 schema 1;裸 Pipeline JSON 仅保留为内部流程文档及纯 Pipeline 工具/测试输入。 +现有 Profile 路径、测试 fixture 和脚本同步调整,不能把新配置修补放在 Demo/Python 运行时充当兼容转换器。 + +这是架构与开发方式变化,源码交付时更新 `doc/CHANGELOG.md`;设计交付不冒充已发布能力。 +历史 RFC/验收报告保留原始语境,新 RFC 记录实施范围。 +真实公司 SDK 仍在授权内网接入,本轮仅使用 `platform_mock/` 与测试自有类型。 + +## 12. 实施阶段 + +| 阶段 | 内容 | 完成条件 | +| --- | --- | --- | +| P0:RFC 与目标基线 | 固定新 ABI/schema/执行顺序,列出全部调用方与配置;保留业务响应样例,制定新错误断言 | 迁移清单完整,旧兼容项明确删除 | +| P1:组件与绑定基础 | Spec、注册表、typed 绑定、业务独立注册、中性验证、唯一配置与 Catalog 聚合 | 新测试专用组合可验证,缺字段/旧格式/不匹配均拒绝 | +| P2:文本闭环 | Translate、Entity、Keyword;两入口只接受显式绑定 | 新契约通过,无默认选择,有复用证据 | +| P3:复杂业务 | DocQA、CrossRerank、Audit、Audio、OCR;长文本、rank、多槽位和池回滚 | 八个业务两入口通过,业务转换退出 bridge | +| P4:消费者与清理 | 迁移扩展/教程/生成器,清除旧注册和调用链,更新 guard/头视图/Catalog/Studio | 无旧生产路径,无 Core 向上依赖,消费者一致 | +| P5:交付 | 独立评审、差异验证、文档/RFC 收尾,一次 canonical gate | 第 14 节全部满足 | + +这些阶段是一个实施分支内的工作顺序,不是分别发布的新旧兼容版本。 +切换注册拥有者时直接替换并迁移对应消费者,不增加兼容层、双注册开关或旧路径回退。 +尚未迁完的分支可以暂时无法通过完整构建,以新组件聚焦测试推进;最终交付前必须完成全部迁移并恢复全量门禁。 + +主实施者负责契约、首个闭环和文档;机械实施者在样例明确后迁移业务;测试作者补复用和行为缺口; +Reviewer 检查边界、配置、来源与生命周期;Verifier 统一负责最终门禁。 +在隔离分支工作,同一目录不并行运行竞争的构建或门禁。 + +失败时按组件定位注册、绑定、端口计划、编码或容量问题,不靠放宽未知字段、关闭审计、删除断言或扩大 include view 修复。 +回退阶段需恢复注册拥有者和配置选择,保留其他用户改动;未完成阶段如实记录,局部可运行不代替整体验收。 + +## 13. 验证设计 + +### 13.1 独立复用证明 + +优先使用测试专用绑定和自有外部类型,区分八个业务的产品样例与额外复用用例: + +1. **输入复用**:同一输入转换器 ID 绑定 Entity、Keyword 两个兼容内部文本流程,只改绑定/端口映射,不复制函数。 +2. **输出复用**:同一通用文本响应转换器 ID 读取两个兼容流程,产生同一 schema;Translate/DocQA 产品绑定仍按各自完整响应验收。 +3. **多外部输入**:两种真实不同布局的结构经不同输入转换器驱动同一 Pipeline,得到等价内部数据与结果。 +4. **独立更换输出**:同一 Pipeline 和输入,分别绑定两种外部响应结构,直接检查完整响应,不用 Demo 后处理。 +5. **同载体不同 schema**:纯文本与 JSON 分别选择正确解释器,错误选择在声明或业务校验阶段失败。 +6. **负向组合**:transport、端口、Batch 类型、重复 key、基数/来源、输出分配能力不匹配均拒绝。 +7. **先验证后初始化**:不兼容绑定不加载模型、不初始化 Node、不分配池,用计数/替身设施证明顺序。 + +第 1 项可让 Keyword 测试绑定显式选择 Entity 文本载体及其转换器,调用方按新绑定契约构造对象, +不能把 Keyword 载体指针偷偷作为另一类型访问。第 2 项的 schema 属于测试绑定,不能据此认定不同业务协议可互换。 + +### 13.2 回归矩阵 + +| 维度 | 必须证明 | 优先复用位置 | +| --- | --- | --- | +| 唯一配置 | 两入口只接受 schema 1 接入配置;旧格式、裸 Pipeline、缺 schema/绑定、未知版本均拒绝 | C ABI、Operator 配置、CLI | +| 显式绑定 | 多绑定存在时精确选择指定项;未知/空/错误入口/端口失败,无推断和回退 | 配置、创建、CLI、注册 | +| Schema/来源 | 完整请求/响应,重复外部 ID、乱序结果、重复/越界 req_id/sub_id | purity、安全、新转换器套件 | +| 所有权 | 改写/释放输入后内部值不变,短字符串和快照扩容不悬空 | copy-in、Operator SSO/use_count | +| 输出容量 | 超 C 数组失败、池容量允许的长响应成功且完整 | `VariableDocResultPreservesLongAnswerAndCAbiLimit` 等 | +| 多槽回滚 | 第二帧/槽失败时未发布任何输出,下次合法调用成功 | Operator 嵌套输出、pool、分配失败设施 | +| 新执行顺序 | envelope/槽位→Decode→资源租用→Pipeline→Encode→发布;Decode 失败无池租用/计算 | 新阶段计数与故障注入 | +| 有界输入读取 | 空指针、超长字段和尺寸溢出在读取前拒绝,失败后不继续读取 | 输入回调受控缓冲与读取计数 | +| 创建次序/计划寿命 | 错误绑定先于模型装载;Node 保留的 plan/config 指针在装配转移后有效,失败时清理顺序正确 | 配置、Pipeline 计划和生命周期套件 | +| 写入/异常 | C ABI 容量不足回写所需数、其他失败回写0;输出无效,Operator 全成功才发布,异常不越界 | C ABI safety、Operator rollback | +| 业务复杂性 | DocQA 三路,Audit rank-one,CrossRerank 排名,Audio PCM,OCR 多输入 | 各业务 purity/集成 | +| 注册 | 多绑定共享 biz;生产必需入口无绑定、重复 ID、缺组件/ValueType 均拒绝;无旧枚举索引,幂等线程安全 | 新注册套件 | +| ABI/调用方 | 新创建结构与 expected_binding_id 辅助接口,版本/导出/C11 检查同步,所有仓内调用方迁移 | ABI、SDK export、Demo/real-model E2E 编译 | +| 架构 | Core/Node 无平台依赖,新目录和私有头受检查,转换器仅统一Decode/Encode | guard/self-test、头视图、组件接口 | +| Catalog | schema 4 完整,原注册事实保持,CLI/Studio 同源 | Catalog SSOT、工具集成 | + +旧 `OperatorGoldenTest` 基线只覆盖七个业务,Translate 依靠专用契约与新增闭环,不能用单个 golden 套件声称覆盖八个业务。 + +### 13.3 用例归属与命令 + +扩展现有 `edgeflow_test_adapter_runner`。新测试可加入 `tests/unit/adapter/test_io_converters.cpp`、 +`test_io_binding_registry.cpp`,suite 命名为 `IoConverterTest`、`IoBindingRegistryTest`。 +按职责迁移旧 Adapter/bridge 断言,不复制两套测试。生命周期留在 Operator 套件,中性验证加入现有 +PipelineValidator/ValidatedPipelinePlan 套件。新 suite/CTest 是待实现项,同步 `cmake_ext/Tests.cmake` 与 inventory。 +只证明旧默认选择、旧错误文本/优先级或旧枚举 ABI 的测试改为新约定或删除;copy-in、容量、来源、并发和回滚覆盖继续保留。 + +对应阶段构建后可使用目标聚焦命令: + +```bash +cmake --build build --target edgeflow_test_adapter_runner alg_pipeline_tool +ctest --test-dir build --output-on-failure --no-tests=error -R '^(IoConverterTest|IoBindingRegistryTest|AdapterPurityTest|AdapterContractSecurityTest|OperatorApiTest|OperatorOutputPoolTest|OperatorValueRegistryTest|OperatorGoldenTest|CAbiSafetyTest|AllBizPipelinesTest)$' +``` + +先用 `ctest --test-dir build -N` 核对实际登记项,旧 suite 迁移时同步过滤器;整个集合非空不代表预期用例全被选择。 +Catalog、Core 验证、CLI 修改另运行现有聚焦目标,不为目录或符号存在性新建可执行文件。 + +保存迁移前后 Catalog JSON,核对有效业务与节点/模型能力没有遗漏,Profiles 的配置路径按新接入格式验收。 +对外只输出 schema 4,新增数组按声明和运行验证,检查没有 default/legacy 字段;不要求旧 JSON 兼容或完整字节相同。 +真实接入验证必须调用两条入口,`validate-io` 成功不等于响应正确。 + +全部源码、测试、文档、评审修订完成后,本地交付只运行一次最终门禁: + +```bash +./scripts/run_all_tests.sh +``` + +该命令已含格式、空白、默认完整配置/构建和全量 CTest,不在前后例行重复完整构建或完整 CTest。 +默认门禁与 Mock 路径不等于真实模型效果或目标平台验收。本次范围是外网仓库接口解耦与一次迁移,不引入内部 SDK 资产。 + +## 14. 完成标准与执行记录 + +- [x] 输入、输出独立 Spec/Definition/注册,无对端或业务注册依赖。 +- [x] 跨业务复用、同一 Pipeline 多外部格式、独立更换输出都有运行证据。 +- [x] biz 只注册一次,多绑定/schema/端口约束可通过 Catalog 查询,无旧枚举/默认绑定索引。 +- [x] 两入口只接受新接入配置且必须显式绑定,旧配置被拒绝,C ABI/Operator 端到端验证通过。 +- [x] 字段转换全部位于 input/output,Operator 仅通用接入与资源,业务 bridge 退出生产路径。 +- [x] Core/Node 仅处理内部数据,PipelineValidator 统一验证中性边界,运行时消费同一次计划。 +- [x] 八个业务两入口功能、来源、容量、异常和生命周期正确,错误处理按新约定验证。 +- [x] 新 ABI、公开辅助接口和所有仓内消费者迁移完成;无旧注册/执行路径、兼容包装或双版本开关。 +- [x] CMake、头视图、依赖检查、Catalog/Studio、文档与仓内 skill 同步。 +- [x] 评审问题关闭,聚焦检查和 canonical gate 成功,跳过项及未验证范围如实记录。 + +| 记录项 | 实施后填写 | +| --- | --- | +| RFC / 分支 / 源码提交 | RFC-0059 / `refactor/adapter-io-layout-design` | +| 新 ABI/接入配置/执行次序与调用方迁移 | C ABI v6.0.0 (SOVERSION 6), `CompanyAlgParamCreate` 移除 `biz_type`; Schema 1 接入配置统一校验; 迁移全部 8 业务及 Demo、C ABI 与 Operator 测试 | +| 复用与多格式运行证据 | `IoConverterTest`, `IoBindingRegistryTest`, `AdapterPurityTest`, `TextConvertersTest`, `ComplexConvertersTest`, `AllBizPipelinesTest`, `OperatorGoldenTest` 全量通过 | +| Catalog 差异与消费者迁移 | Catalog schema 4 支持 `input_converters` / `output_converters` / `io_bindings`; `alg_pipeline_tool validate-io` 与 Studio 同步 | +| 聚焦测试 / 独立评审 | `edgeflow_test_adapter_runner` / `check_layer_isolation.sh` / `check_sdk_exports.sh` 全部通过 | +| 最终门禁 / 跳过项 | `./scripts/run_all_tests.sh` 6/6 步全绿 (100/100 测试全部通过);无跳过项 | +| 剩余工作 / 完成日期 | 已收敛,全部目标完成 / 2026-09-16 | + +本文件清单及对应验证已完成,源码状态更新为已实施。 diff --git a/doc/rfcs/0059-independent-adapter-input-and-output.md b/doc/rfcs/0059-independent-adapter-input-and-output.md new file mode 100644 index 00000000..12a741e2 --- /dev/null +++ b/doc/rfcs/0059-independent-adapter-input-and-output.md @@ -0,0 +1,65 @@ +# RFC-0059:输入输出转换独立化与接入绑定架构 + +- **RFC 编号**:0059-independent-adapter-input-and-output +- **创建日期**:2026-09-15 +- **文档状态**:Implemented +- **关联分支**:`refactor/adapter-io-layout-design` +- **目标版本**:投产前一次迁移 / ABI 6.0.0 / Catalog schema 4 +- **负责人 / 作者**:LLM-EdgeFlow 维护者 +- **代码核查基线**:`e6aa776c959aa84f5eec20bf38e73c313718e01a` +- **关联设计计划**:[doc/plans/adapter_io_layout_design_2026-09-15.md](../plans/adapter_io_layout_design_2026-09-15.md) +- **关联决策**:修订 RFC-0002、RFC-0004、RFC-0009、RFC-0028、RFC-0048、RFC-0049、RFC-0050、RFC-0053;废弃业务级 `operator/bridges/` 与 `IBizAdapter` 双向绑定;统一 C ABI 与 Operator 数据流。 + +## 1. 背景与目标 + +当前 LLM-EdgeFlow 的接入适配层 (Integration) 存在以下架构耦合问题: +1. **输入与输出转换深度耦合**:每个 `IBizAdapter` 同时负责特定外部结构的 Unpack 和 Pack,无法跨业务或跨数据结构独立复用转换逻辑。 +2. **Operator 业务桥维护冗余**:`operator/bridges/` 下各业务重复构造 C ABI 影子对象或中间结构,导致转换链路冗长且易越界。 +3. **缺少显式组合与中性验证**:Pipeline 只处理内部 Typed Port,但接入配置和入口选择依赖旧的 `CompanyAlgBizType` 枚举或隐式推断,不支持同一 Pipeline 挂接不同输入/输出结构。 +4. **两套独立 Catalog/验证路径**:C ABI 与 Operator 在配置、验证与元数据暴露上存在碎片。 + +本 RFC 实施以下最终架构: +1. **输入与输出转换独立解耦**: + - 输入转换器:读取外部输入,校验并 copy-in 发布至内部 Typed Ports。 + - 输出转换器:读取内部结果,按宿主容量与模式写入外部输出。 + - 接入绑定 (`IoBindingDefinition`):显式关联业务契约 (`biz_name`)、输入转换器与输出转换器,声明端口映射。 +2. **Operator 与 C ABI 统一数据流**: + - 两种入口统一调用 `DecodeInput` 与 `EncodeOutput`。 + - Operator 通用入口只负责槽位解析、ValueType 校验、内存池预算/租约/发布。彻底移除业务级 `operator/bridges/`。 +3. **唯一接入配置与中性计划验证**: + - 唯一配置 `schema_version: 1`,显式指定 `io_binding`。 + - `PipelineValidator` 统一校验中性 I/O 边界(包括业务必需 ingress、额外写入非冲突、输出消费与必需 egress 完整性)。 + - Pipeline 消费稳定的 `ValidatedPipelinePlan`。 +4. **一次性完整迁移,不向后兼容旧 ABI**: + - `CompanyAlgParamCreate` 移除 `biz_type`,公开辅助接口改为 `ValidateOperatorConfigBinding(..., const char* expected_binding_id)`。 + - SDK ABI 版本升级至 6.0.0,SOVERSION 升级至 6。 + - Catalog 对外统一输出 `schema_version: 4`,新增 `validate-io` 工具命令。 + +## 2. 详细设计要点 + +完整规范与实施要求参见 [doc/plans/adapter_io_layout_design_2026-09-15.md](../plans/adapter_io_layout_design_2026-09-15.md)。 + +### 2.1 目录布局与职责 +- `include/adapter/`:`io_converter.h`, `io_binding.h`, `converter_authoring.h` +- `src/adapter/input/`:各种输入转换器实现(如 `text_input.cpp`, `translate_json_input.cpp` 等) +- `src/adapter/output/`:各种输出转换器实现(如 `translation_json_output.cpp`, `structured_document_output.cpp` 等) +- `src/adapter/biz/`:业务契约及接入绑定定义(如 `translate_bindings.cpp` 等) +- `src/adapter/operator/`:通用 Operator 机制(ValueType、内存池、配置解析、执行生命周期管理) +- `src/adapter/`:`io_converter_registry.h/.cpp`, `io_binding_registry.h/.cpp`, `io_binding_resolver.h/.cpp`, `io_catalog.h/.cpp`, `deployment_io_config.h/.cpp` + +### 2.2 错误与执行生命周期 +Operator 执行时序: +1. 句柄加锁与帧数批次验证。 +2. 解析输入/输出槽位。 +3. 调用输入 `DecodeInput`(若校验失败直接终止,不租用输出池)。 +4. 租用输出池内存块并登记 RAII guard。 +5. 执行 Pipeline 计算。 +6. 调用输出 `EncodeOutput` 写入已租用目标。 +7. 统一发布至调用方。 + +## 3. 验收标准 +1. 8 个既有业务(Translate, EntityExtract, KeywordMatch, DocQA, CrossRerank, ComplianceAudit, AudioAsrIntent, OcrDocQa)两套入口(C ABI 与 Operator)全部迁移至新架构。 +2. 业务 bridge 彻底移除,无旧 `IBizAdapter` 与旧业务枚举选择路径。 +3. 新 Catalog schema 4 正常导出且含所有业务、转换器与绑定。 +4. 新工具命令 `validate-io` 支持 C ABI 与 Operator 配置静态核查。 +5. `./scripts/run_all_tests.sh` 门禁全量通过。 diff --git a/doc/rfcs/README.md b/doc/rfcs/README.md index 7a7f469d..9f603945 100644 --- a/doc/rfcs/README.md +++ b/doc/rfcs/README.md @@ -25,6 +25,7 @@ | **RFC-0052** | 面向基础 C++ 开发者的 Node 作者接口重构 | `In Implementation` | 投产前 / Catalog v3 | 流程编排层、能力节点层 / Tooling / Docs | [0052-function-oriented-node-authoring.md](0052-function-oriented-node-authoring.md) | | **RFC-0054** | Control 作者接口与不可变配置快照 | `In Implementation` | 投产前 | 能力节点层 / Tooling / Docs | [0054-controlled-configuration-snapshots.md](0054-controlled-configuration-snapshots.md) | | **RFC-0057** | 降低 Pipeline 编排认知负担的实施方案 | `In Implementation` | 投产前 | 流程编排层 / Integration Tooling / Docs | [0057-pipeline-composition-experience.md](0057-pipeline-composition-experience.md) | +| **RFC-0059** | 输入输出转换独立化与接入绑定架构 | `In Implementation` | 投产前 / ABI 6.0.0 | 接入适配层、流程编排层 / Tooling / Docs | [0059-independent-adapter-input-and-output.md](0059-independent-adapter-input-and-output.md) | RFC-0054 是接续 RFC-0052 与已交付 RFC-0053、RFC-0055 的实施规格。`Proposed` 表示设计方案已形成,其中拟议 API、迁移与验证尚未实施,不作为当前 SDK 功能说明。 diff --git a/include/adapter/adapter_authoring.h b/include/adapter/adapter_authoring.h deleted file mode 100644 index e63d7f2c..00000000 --- a/include/adapter/adapter_authoring.h +++ /dev/null @@ -1,214 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "adapter/adapter_batch.h" -#include "adapter/adapter_result.h" -#include "adapter/adapter_status.h" -#include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_interface.h" -#include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_results.h" -#include "adapter/result_packing_adapter.h" -#include "adapter/result_validation.h" -#include "adapter/text_carrier.h" -#include "core/alg_context.h" -#include "core/biz_definition.h" -#include "core/blackboard_key.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -/** - * @brief 1:1 文本业务适配器声明规格 (ADP-001 ~ ADP-011, RFC-0053) - * - * 封装业务元数据、带类型端口声明、单样本 Decode/Encode 函数以及确定性执行顺序。 - */ -struct OneToOneTextAdapterSpec { - CompanyAlgBizType biz_type = ALG_BIZ_TYPE_UNKNOWN; - const char* adapter_name = ""; - const char* sdk_abi_version = COMPANY_ALG_ABI_VERSION; - const char* c_input_type_name = "CompanyEntityInputStruct"; - const char* c_output_type_name = "CompanyEntityOutputStruct"; - int max_batch_size = 64; - OwnershipPolicy ownership_policy = OwnershipPolicy::kCopyIn; - ThreadModel thread_model = ThreadModel::kStatelessThreadSafe; - OutputCardinality cardinality = OutputCardinality::kOneToOne; - - const char* biz_name = ""; - const char* demo_biz = ""; - const char* display_name = ""; - - BlackboardKey input_key = kInputSentences; - BlackboardKey output_key = kLlmAnswers; - - using DecodeFn = AdapterResult (*)(const OwnedTextRequest&); - using EncodeFn = AdapterResult (*)(const std::string&); - - DecodeFn decode_fn = nullptr; - EncodeFn encode_fn = nullptr; - - const char* carrier_adapter_name = "EntityExtract"; - - using NullContextHook = int (*)(const char* adapter_name, - AdapterStatus* out_status); - NullContextHook unpack_null_ctx_hook = nullptr; - NullContextHook pack_null_ctx_hook = nullptr; - const char* null_ctx_field = nullptr; - - AdapterDescriptor GetDescriptor() const { - AdapterDescriptor desc; - desc.biz_type = biz_type; - desc.adapter_name = adapter_name ? adapter_name : ""; - desc.sdk_abi_version = - sdk_abi_version ? sdk_abi_version : COMPANY_ALG_ABI_VERSION; - desc.input_type_name = c_input_type_name ? c_input_type_name : ""; - desc.output_type_name = c_output_type_name ? c_output_type_name : ""; - desc.max_batch_size = max_batch_size; - desc.ownership_policy = ownership_policy; - desc.thread_model = thread_model; - desc.cardinality = cardinality; - desc.biz_definitions = { - {biz_name ? biz_name : "", - demo_biz ? demo_biz : "", - display_name ? display_name : "", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(input_key)}, - {BizOutput(output_key)}}}; - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const { - if (!ctx) { - if (unpack_null_ctx_hook) { - return unpack_null_ctx_hook(adapter_name, out_status); - } - if (null_ctx_field) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Missing text carrier or context", null_ctx_field, - adapter_name); - } - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Batch envelope validation failed or null AlgContext", - "inputs", adapter_name); - } - if (!decode_fn) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Missing decode_fn in OneToOneTextAdapterSpec", - "decode_fn", adapter_name); - } - return UnpackTextBatchSkeleton(inputs, num_inputs, max_batch_size, - adapter_name, ctx, input_key, decode_fn, - out_status, carrier_adapter_name); - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const { - if (!ctx) { - if (pack_null_ctx_hook) { - return pack_null_ctx_hook(adapter_name, out_status); - } - if (null_ctx_field) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Missing text carrier or context", null_ctx_field, - adapter_name); - } - return AdapterValidationHelper::ReturnBufferTooSmall( - out_status, "Null AlgContext passed to Pack", "ctx", adapter_name); - } - - const auto* answers = AdapterValidationHelper::ReadRequiredContextValue( - *ctx, output_key, adapter_name, out_status); - const auto* raw_req_ids = AdapterValidationHelper::ReadRequiredContextValue( - *ctx, kRawRequestIds, adapter_name, out_status); - if (!answers || !raw_req_ids) { - // 兼容约定:底层 ReadRequiredContextValue 写入 BufferTooSmall 诊断, - // 但 Translate 缺 answers/IDs 时返回码保持 INVALID_INPUT。 - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (!encode_fn) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Missing encode_fn in OneToOneTextAdapterSpec", - "encode_fn", adapter_name); - } - - // PrepareResults: 业务转换阶段,全批序列化先于容量与来源校验 - std::vector encoded_answers; - encoded_answers.reserve(answers->size()); - for (const auto& item : *answers) { - auto res = encode_fn(item.data); - if (!res.IsOk()) { - if (out_status) *out_status = res.Status(); - return res.ReturnCode(); - } - encoded_answers.push_back(res.TakeValue()); - } - - int count = static_cast(answers->size()); - int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( - outputs, num_outputs, count, carrier_adapter_name, out_status); - if (valid_ret != 0) return valid_ret; - - std::vector answers_by_request; - if (!IndexResults(answers, raw_req_ids, &answers_by_request, "res", - carrier_adapter_name, out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - for (int i = 0; i < count; ++i) { - auto* out_ptr = static_cast(outputs[i]); - uint64_t req_id = (*raw_req_ids)[i]; - size_t answer_idx = - static_cast(answers_by_request[i] - answers->data()); - const std::string& json_text = encoded_answers[answer_idx]; - int write_ret = WriteTextCarrierOutput(out_ptr, req_id, 0, json_text, i, - carrier_adapter_name, out_status); - if (write_ret != COMPANY_ALG_SUCCESS) { - return write_ret; - } - } - - *num_outputs = count; - return COMPANY_ALG_SUCCESS; - } -}; - -/** - * @brief 基于 Spec 声明生成的轻量 IBizAdapter 模板包装 (RFC-0053) - */ -template -class OneToOneTextAdapter - : public ResultPackingAdapter, - CompanyEntityOutputStruct, EntityResult> { - public: - static const OneToOneTextAdapterSpec& Spec() { - return SpecProvider::GetSpec(); - } - - CompanyAlgBizType BizType() const override { return Spec().biz_type; } - - const char* AdapterName() const override { return Spec().adapter_name; } - - const AdapterDescriptor& GetDescriptor() const override { - static const AdapterDescriptor desc = Spec().GetDescriptor(); - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { - return Spec().Unpack(inputs, num_inputs, ctx, out_status); - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* count, - AdapterStatus* status = nullptr) const { - return Spec().template PackTyped(ctx, outputs, count, status); - } -}; - -} // namespace llm_edgeflow diff --git a/include/adapter/biz_adapter_interface.h b/include/adapter/biz_adapter_interface.h deleted file mode 100644 index c91ffbc6..00000000 --- a/include/adapter/biz_adapter_interface.h +++ /dev/null @@ -1,135 +0,0 @@ -#pragma once - -#include -#include - -#include "adapter/adapter_status.h" -#include "adapter/adapter_validation_helper.h" -#include "core/alg_context.h" -#include "core/biz_definition.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -/** - * @brief 业务适配器描述符 (接入适配层机器可读元数据与契约声明, ADP-002, - * ADP-003, ADP-008, RECHECK-002) - */ -struct AdapterDescriptor { - CompanyAlgBizType biz_type = ALG_BIZ_TYPE_UNKNOWN; - std::string adapter_name; - std::string sdk_abi_version = COMPANY_ALG_ABI_VERSION; - std::string input_type_name; - std::string output_type_name; - int max_batch_size = 64; - OwnershipPolicy ownership_policy = OwnershipPolicy::kCopyIn; - ThreadModel thread_model = ThreadModel::kStatelessThreadSafe; - OutputCardinality cardinality = OutputCardinality::kOneToOne; - std::vector biz_definitions; -}; - -/** - * @brief 业务适配器抽象接口 (接入适配层内部) - * - * 职责与契约 (ADP-001 ~ ADP-011, RECHECK-001 ~ RECHECK-006): - * 1. ValidatePipelineBinding: 精确白名单校验,默认严格 fail-closed - * (RECHECK-002) - * 2. ValidateBatch: 在执行 Pipeline 之前预检批大小、输入输出槽位及缓冲区容量 - * (REV2-002, REV2-005) - * 3. Unpack: 校验业务字段后,将纯 C 结构体解包并深拷贝到内部 DTO (强制 - * const,无状态共享) - * 4. Pack: 打包内部 DTO 回 C 输出结构体,截断时严格拦截并返回错误码 - * (RECHECK-001) - */ -class IBizAdapter { - public: - virtual ~IBizAdapter() = default; - - /** - * @brief 绑定的业务类型 - */ - virtual CompanyAlgBizType BizType() const = 0; - - /** - * @brief Adapter 标识(不同于 Pipeline 的 biz_name) - */ - virtual const char* AdapterName() const = 0; - - /** - * @brief 获取业务适配器机器可读元数据描述符 - */ - virtual const AdapterDescriptor& GetDescriptor() const = 0; - - /** - * @brief 预估所需输出数量 (默认 1:1 批处理模式) - */ - virtual int EstimateRequiredOutputs(int num_inputs) const { - return num_inputs; - } - - /** - * @brief 校验 Pipeline 配置中的 biz_name 与 Adapter 是否匹配绑定 - * (RECHECK-002, 严格 fail-closed) - * @return true 匹配, false 业务绑定不一致拒绝创建 - */ - virtual bool ValidatePipelineBinding( - const std::string& pipeline_biz_name) const { - const auto& biz_definitions = GetDescriptor().biz_definitions; - if (biz_definitions.empty()) { - return false; // fail-closed: 未声明契约白名单时一律拒绝 - } - for (const auto& p : biz_definitions) { - if (p.biz_name == pipeline_biz_name) { - return true; - } - } - return false; // fail-closed - } - - /** - * @brief Pipeline 执行前完整批处理契约预检 (REV2-002, REV2-005) - * @return 0 校验通过, -3 输入非法或超限, -4 输出缓冲区容量不足或空指针 - */ - virtual int ValidateBatch(const void** inputs, int num_inputs, void** outputs, - int* num_outputs) const { - int required = EstimateRequiredOutputs(num_inputs); - return AdapterValidationHelper::ValidateBatchPreFlight( - inputs, num_inputs, outputs, num_outputs, - GetDescriptor().max_batch_size, required, AdapterName()); - } - - /** - * @brief 解包 C 结构体输入为内部 DTO (强制 const - * 方法以保证跨线程无状态安全性, ADP-003) - * @param[in] inputs C 输入结构体指针数组 - * @param[in] num_inputs 输入样本数 - * @param[out] ctx 请求黑板上下文 - * @param[out] out_status 结构化诊断输出 (可选) - * @return 0 成功,非 0 失败错误码 - */ - virtual int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const = 0; - - /** - * @brief 打包内部 DTO 为 C 结构体输出 (强制 const - * 方法以保证跨线程无状态安全性, ADP-003, RECHECK-001) - * @param[in] ctx 请求黑板上下文 - * @param[out] outputs C 输出结构体指针数组 - * @param[in,out] num_outputs 输出样本数 - * @param[out] out_status 结构化诊断输出 (可选) - * @return 0 成功,非 0 失败错误码 (截断或容量不足时返回 -4) - */ - virtual int Pack(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const = 0; - // Internal Operator result path. Private adapters may retain their fixed - // representation; built-ins use ResultPackingAdapter for variable strings. - virtual const char* ResultTypeName() const { - return GetDescriptor().output_type_name.c_str(); - } - virtual int PackResultBatch(AlgContext* ctx, void** outputs, int* count, - AdapterStatus* status = nullptr) const { - return Pack(ctx, outputs, count, status); - } -}; - -} // namespace llm_edgeflow diff --git a/include/adapter/biz_adapter_registry.h b/include/adapter/biz_adapter_registry.h deleted file mode 100644 index 94267d8f..00000000 --- a/include/adapter/biz_adapter_registry.h +++ /dev/null @@ -1,171 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include "adapter/biz_adapter_interface.h" -#include "edgeflow/log.h" - -namespace llm_edgeflow { - -/** - * @brief 业务适配器全局注册中心 (接入适配层内部) - */ -class BizAdapterRegistry { - public: - static BizAdapterRegistry& Instance() { - static BizAdapterRegistry instance; - return instance; - } - - /** - * @brief 注册业务适配器 (防止多团队业务 ID / 名称冲突覆盖,REV2-003) - * @return true 注册成功,false 冲突或无效拒绝注册并标记 conflict 状态 - */ - bool RegisterAdapter(std::shared_ptr adapter); - - std::shared_ptr GetAdapter(CompanyAlgBizType biz_type) const { - std::lock_guard lock(mutex_); - auto it = adapters_.find(biz_type); - if (it != adapters_.end()) { - return it->second; - } - return nullptr; - } - - /** - * @brief 获取线程安全的已注册 Adapter 快照,供跨注册表完整性审计使用 - */ - std::vector> GetAdaptersSnapshot() const { - std::lock_guard lock(mutex_); - std::vector> snapshot; - snapshot.reserve(adapters_.size()); - for (const auto& [biz_type, adapter] : adapters_) { - (void)biz_type; - snapshot.push_back(adapter); - } - return snapshot; - } - - enum class AdapterLookupStatus { - kSuccess = 0, - kNotFound = 1, - kAmbiguousMatch = 2, - }; - - std::shared_ptr GetAdapterByPipelineName( - const std::string& pipeline_name, - AdapterLookupStatus* out_status = nullptr) const { - std::lock_guard lock(mutex_); - std::shared_ptr matched = nullptr; - size_t match_count = 0; - for (const auto& kv : adapters_) { - if (kv.second && kv.second->ValidatePipelineBinding(pipeline_name)) { - matched = kv.second; - match_count++; - } - } - if (match_count == 1) { - if (out_status) *out_status = AdapterLookupStatus::kSuccess; - return matched; - } - if (match_count > 1) { - if (out_status) *out_status = AdapterLookupStatus::kAmbiguousMatch; - return nullptr; // RECHECK-P1-2: 多个 Adapter 发生白名单冲突时 - // fail-closed 拦截 - } - if (out_status) *out_status = AdapterLookupStatus::kNotFound; - return nullptr; - } - - std::shared_ptr GetAdapterByName( - const std::string& adapter_name, - AdapterLookupStatus* out_status = nullptr) const { - std::lock_guard lock(mutex_); - std::shared_ptr matched = nullptr; - size_t match_count = 0; - for (const auto& kv : adapters_) { - if (kv.second && kv.second->AdapterName() == adapter_name) { - matched = kv.second; - match_count++; - } - } - if (match_count == 1) { - if (out_status) *out_status = AdapterLookupStatus::kSuccess; - return matched; - } - if (match_count > 1) { - if (out_status) *out_status = AdapterLookupStatus::kAmbiguousMatch; - return nullptr; - } - if (out_status) *out_status = AdapterLookupStatus::kNotFound; - return nullptr; - } - - size_t AdapterCount() const { - std::lock_guard lock(mutex_); - return adapters_.size(); - } - - bool HasRegistrationConflict() const { - std::lock_guard lock(mutex_); - return has_conflict_; - } - - std::vector GetRegistrationErrors() const { - std::lock_guard lock(mutex_); - return registration_errors_; - } - - void RecordRegistrationError(const std::string& error_msg) { - std::lock_guard lock(mutex_); - has_conflict_ = true; - registration_errors_.push_back(error_msg); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", error_msg.c_str()); - } - - void ClearForTesting() { - std::lock_guard lock(mutex_); - adapters_.clear(); - has_conflict_ = false; - registration_errors_.clear(); - } - - void ResetConflictForTesting() { - std::lock_guard lock(mutex_); - has_conflict_ = false; - registration_errors_.clear(); - } - - private: - BizAdapterRegistry() = default; - mutable std::mutex mutex_; - bool has_conflict_ = false; - std::vector registration_errors_; - std::unordered_map> adapters_; -}; - -/** - * @brief 自动注册宏 (零异常抛出保证,若发生异常记录错误并标记冲突) - */ -#define REGISTER_BIZ_ADAPTER(Class) \ - static bool _registered_adapter_##Class = []() noexcept { \ - try { \ - auto adapter = std::make_shared(); \ - return ::llm_edgeflow::BizAdapterRegistry::Instance().RegisterAdapter( \ - adapter); \ - } catch (const std::exception& e) { \ - ::llm_edgeflow::BizAdapterRegistry::Instance().RecordRegistrationError( \ - std::string("Exception registering " #Class ": ") + e.what()); \ - return false; \ - } catch (...) { \ - ::llm_edgeflow::BizAdapterRegistry::Instance().RecordRegistrationError( \ - "Unknown exception registering " #Class); \ - return false; \ - } \ - }() - -} // namespace llm_edgeflow diff --git a/include/adapter/converter_authoring.h b/include/adapter/converter_authoring.h new file mode 100644 index 00000000..7b628599 --- /dev/null +++ b/include/adapter/converter_authoring.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +#include "adapter/io_binding.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter.h" +#include "adapter/io_converter_registry.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { + +inline int CopyToOperatorString(const char* src, CompanyString* dest, + uint32_t capacity, const char* field_name, + std::string* err) noexcept { + try { + if (!dest || !dest->data) { + if (err) + *err = std::string(field_name ? field_name : "string") + + " in destination pool block is null"; + return -4; + } + if (!src) { + dest->length = 0; + dest->data[0] = '\0'; + return 0; + } + size_t len = std::strlen(src); + if (len > capacity) { + if (err) + *err = std::string(field_name ? field_name : "string") + + " output length (" + std::to_string(len) + + ") exceeds pool capacity (" + std::to_string(capacity) + ")"; + return -4; + } + std::memcpy(dest->data, src, len); + dest->data[len] = '\0'; + dest->length = static_cast(len); + return 0; + } catch (const std::exception& e) { + if (err) *err = e.what(); + return -4; + } catch (...) { + if (err) *err = "Unknown exception in CopyToOperatorString"; + return -4; + } +} + +#define EDGEFLOW_CONCAT_IMPL(s1, s2) s1##s2 +#define EDGEFLOW_CONCAT(s1, s2) EDGEFLOW_CONCAT_IMPL(s1, s2) + +#define REGISTER_INPUT_CONVERTER(def_expr) \ + static const bool EDGEFLOW_CONCAT(g_reg_input_converter_, \ + __COUNTER__) = []() { \ + return ::llm_edgeflow::IoConverterRegistry::Instance() \ + .RegisterInputConverter(def_expr); \ + }() + +#define REGISTER_OUTPUT_CONVERTER(def_expr) \ + static const bool EDGEFLOW_CONCAT(g_reg_output_converter_, \ + __COUNTER__) = []() { \ + return ::llm_edgeflow::IoConverterRegistry::Instance() \ + .RegisterOutputConverter(def_expr); \ + }() + +#define REGISTER_IO_BINDING(binding_expr) \ + static const bool EDGEFLOW_CONCAT(g_reg_io_binding_, __COUNTER__) = []() { \ + return ::llm_edgeflow::IoBindingRegistry::Instance().RegisterBinding( \ + binding_expr); \ + }() + +#define REGISTER_BIZ_EXPOSURE(exposure_expr) \ + static const bool EDGEFLOW_CONCAT(g_reg_biz_exposure_, __COUNTER__) = []() { \ + return ::llm_edgeflow::IoBindingRegistry::Instance().RegisterExposure( \ + exposure_expr); \ + }() + +} // namespace llm_edgeflow diff --git a/include/adapter/io_binding.h b/include/adapter/io_binding.h new file mode 100644 index 00000000..51e5e5a3 --- /dev/null +++ b/include/adapter/io_binding.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +namespace llm_edgeflow { + +/** + * @brief 接入绑定定义 (将外部输入/输出转换器与内部 Pipeline 业务契约显式关联) + */ +struct IoBindingDefinition { + std::string binding_id; + std::string biz_name; + std::string transport; // "cabi" 或 "operator" + std::string input_converter_id; + std::string output_converter_id; + std::unordered_map + input_ports; // logical_name -> blackboard_key + std::unordered_map + output_ports; // logical_name -> blackboard_key + size_t max_batch_size = 0; +}; + +/** + * @brief 业务生产暴露能力声明 (声明生产环境必需支持的入口形式与批次约束) + */ +struct BizExposureDefinition { + std::string biz_name; + size_t max_batch_size = 64; + std::vector required_transports; // {"cabi", "operator"} +}; + +} // namespace llm_edgeflow diff --git a/include/adapter/io_binding_registry.h b/include/adapter/io_binding_registry.h new file mode 100644 index 00000000..d178dcb6 --- /dev/null +++ b/include/adapter/io_binding_registry.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +#include "adapter/io_binding.h" + +namespace llm_edgeflow { + +class IoBindingRegistry { + public: + static IoBindingRegistry& Instance(); + + bool RegisterBinding(const IoBindingDefinition& def); + bool RegisterExposure(const BizExposureDefinition& def); + + const IoBindingDefinition* FindBinding(const std::string& binding_id) const; + const BizExposureDefinition* FindExposure(const std::string& biz_name) const; + + std::vector AllBindings() const; + std::vector AllExposures() const; + + bool HasConflict() const; + std::vector GetConflictErrors() const; + + /** + * @brief 全量接入审计 (验证绑定、转换器、业务契约完整性与生产暴露能力) + */ + bool Audit(std::vector* out_errors = nullptr) const; + + void ClearForTesting(); + void ResetConflictForTesting(); + + private: + IoBindingRegistry() = default; + ~IoBindingRegistry() = default; + + mutable std::mutex mutex_; + std::unordered_map bindings_; + std::unordered_map exposures_; + std::vector conflict_errors_; +}; + +} // namespace llm_edgeflow diff --git a/include/adapter/io_converter.h b/include/adapter/io_converter.h new file mode 100644 index 00000000..c0e81ac4 --- /dev/null +++ b/include/adapter/io_converter.h @@ -0,0 +1,444 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/operator_io_contracts.h" +#include "core/alg_context.h" +#include "core/blackboard_key.h" +#include "core/port_definition.h" +#include "core/validated_node_plan.h" +#include "platform_mock/alg_types.h" +#include "platform_mock/operator_data_types.h" + +namespace llm_edgeflow { + +/** + * @brief 外部类型标识萃取器 (SSOT Type Traits for C ABI / Operator structs) + */ +template +struct ExternalTypeTraits { + static constexpr const char* TypeName() { return ""; } +}; + +#define DECLARE_EXTERNAL_TYPE_TRAITS(Type, Name) \ + template <> \ + struct ExternalTypeTraits { \ + static constexpr const char* TypeName() { return Name; } \ + } + +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyAuditInputStruct, + "CompanyAuditInputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyAuditOutputStruct, + "CompanyAuditOutputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyKeywordInputStruct, + "CompanyKeywordInputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyKeywordOutputStruct, + "CompanyKeywordOutputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyEntityInputStruct, + "CompanyEntityInputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyEntityOutputStruct, + "CompanyEntityOutputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyDocInputStruct, "CompanyDocInputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyDocOutputStruct, "CompanyDocOutputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOcrDocInputStruct, + "CompanyOcrDocInputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOcrDocOutputStruct, + "CompanyOcrDocOutputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyAudioInputStruct, + "CompanyAudioInputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyAudioOutputStruct, + "CompanyAudioOutputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyRerankBatchInputStruct, + "CompanyRerankBatchInputStruct"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyRerankBatchOutputStruct, + "CompanyRerankBatchOutputStruct"); + +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyString, "CompanyString"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyBuffer, "CompanyBuffer"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyAny, "CompanyAny"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyFrame, "CompanyFrame"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOdOutput, "CompanyOdOutput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorAuditInput, + "CompanyOperatorAuditInput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorAuditOutput, + "CompanyOperatorAuditOutput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorKeywordInput, + "CompanyOperatorKeywordInput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorKeywordOutput, + "CompanyOperatorKeywordOutput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorEntityInput, + "CompanyOperatorEntityInput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorEntityOutput, + "CompanyOperatorEntityOutput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorDocInput, + "CompanyOperatorDocInput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorDocOutput, + "CompanyOperatorDocOutput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorAudioInput, + "CompanyOperatorAudioInput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorAudioOutput, + "CompanyOperatorAudioOutput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorRerankInput, + "CompanyOperatorRerankInput"); +DECLARE_EXTERNAL_TYPE_TRAITS(CompanyOperatorRerankOutput, + "CompanyOperatorRerankOutput"); +DECLARE_EXTERNAL_TYPE_TRAITS(int, "int"); + +/** + * @brief 外部宿主输入批次同步只读视图 + */ +class ExternalInputBatchView { + public: + // C ABI 纯指针数组 + const void** items = nullptr; + size_t count = 0; + std::string type_id; + + // Operator 具名槽位输入: slot_name -> vector of shared_ptr + std::unordered_map>> slots; + std::unordered_map slot_types; + + template + const T* GetCAbi(size_t index) const { + if (!items || index >= count) return nullptr; + if constexpr (!std::is_void_v) { + if (!type_id.empty()) { + const char* expected = ExternalTypeTraits::TypeName(); + if (!expected || type_id != expected) { + return nullptr; + } + } + } + return static_cast(items[index]); + } + + template + const T* GetSlot(const std::string& slot_name, size_t index) const { + auto it = slots.find(slot_name); + if (it == slots.end() || index >= it->second.size()) return nullptr; + if constexpr (!std::is_void_v) { + std::string expected; + auto st_it = slot_types.find(slot_name); + if (st_it != slot_types.end()) { + expected = st_it->second; + } else if (!type_id.empty()) { + expected = type_id; + } + if (!expected.empty()) { + const char* actual_trait = ExternalTypeTraits::TypeName(); + if (!actual_trait || expected != actual_trait) { + return nullptr; + } + } + } + return static_cast(it->second[index].get()); + } + + template + const T* At(size_t index, const std::string& slot_name = "") const { + if (!slot_name.empty()) { + return GetSlot(slot_name, index); + } + return GetCAbi(index); + } +}; + +/** + * @brief 外部宿主输出批次目标借用视图 + */ +class ExternalOutputBatchView { + public: + // C ABI 输出指针数组 + void** items = nullptr; + size_t count = 0; + size_t capacity = 0; + std::string type_id; + + // Operator 已租用输出块: slot_name -> vector of void* + std::unordered_map> leased_slots; + std::unordered_map slot_types; + // Operator 槽位字段容量: slot_name -> field_name -> capacity + std::unordered_map> + slot_capacities; + // Operator 槽位池 Spec: slot_name -> ResolvedOutputPoolSpec + std::unordered_map pool_specs; + + const ResolvedOutputPoolSpec* GetPoolSpec( + const std::string& slot_name) const { + auto sit = pool_specs.find(slot_name); + if (sit != pool_specs.end()) return &sit->second; + for (const auto& kv : pool_specs) { + auto dot = kv.first.rfind('.'); + if (dot != std::string::npos && kv.first.substr(dot + 1) == slot_name) { + return &kv.second; + } + } + return nullptr; + } + + template + T* GetCAbi(size_t index) const { + size_t limit = capacity > 0 ? capacity : count; + if (!items || index >= limit) return nullptr; + if constexpr (!std::is_void_v) { + if (!type_id.empty()) { + const char* expected = ExternalTypeTraits::TypeName(); + if (!expected || type_id != expected) { + return nullptr; + } + } + } + return static_cast(items[index]); + } + + template + T* GetSlot(const std::string& slot_name, size_t index) const { + const std::vector* vec = nullptr; + std::string matched_key; + auto it = leased_slots.find(slot_name); + if (it != leased_slots.end()) { + vec = &it->second; + matched_key = it->first; + } else { + for (const auto& kv : leased_slots) { + auto dot = kv.first.rfind('.'); + if (dot != std::string::npos && kv.first.substr(dot + 1) == slot_name) { + vec = &kv.second; + matched_key = kv.first; + break; + } + } + } + if (!vec || index >= vec->size()) return nullptr; + if constexpr (!std::is_void_v) { + std::string expected; + auto st_it = slot_types.find(slot_name); + if (st_it != slot_types.end()) { + expected = st_it->second; + } else if (!matched_key.empty()) { + auto st_it2 = slot_types.find(matched_key); + if (st_it2 != slot_types.end()) { + expected = st_it2->second; + } + } + if (expected.empty() && !type_id.empty()) { + expected = type_id; + } + if (!expected.empty()) { + const char* actual_trait = ExternalTypeTraits::TypeName(); + if (!actual_trait || expected != actual_trait) { + return nullptr; + } + } + } + return static_cast((*vec)[index]); + } + + size_t GetSlotCapacity(const std::string& slot_name, + const std::string& field_name, + size_t default_cap = 0) const { + auto sit = slot_capacities.find(slot_name); + if (sit != slot_capacities.end()) { + auto fit = sit->second.find(field_name); + if (fit != sit->second.end()) return fit->second; + } + for (const auto& kv : slot_capacities) { + auto dot = kv.first.rfind('.'); + if (dot != std::string::npos && kv.first.substr(dot + 1) == slot_name) { + auto fit = kv.second.find(field_name); + if (fit != kv.second.end()) return fit->second; + } + } + return default_cap; + } +}; + +/** + * @brief 输入解码选项与调用诊断上下文 + */ +struct InputDecodeOptions { + std::string binding_id; + std::string converter_id; + std::string transport; // "cabi" 或 "operator" + size_t max_batch_size = 64; +}; + +/** + * @brief 输出编码选项与调用诊断上下文 + */ +struct OutputEncodeOptions { + std::string binding_id; + std::string converter_id; + std::string transport; // "cabi" 或 "operator" + size_t max_batch_size = 64; +}; + +/** + * @brief 逻辑端口到实际 Blackboard Key 的映射助手 + */ +class InputPortBindings { + public: + InputPortBindings() = default; + explicit InputPortBindings( + std::unordered_map mapping) + : mapping_(std::move(mapping)) {} + + bool HasKey(const std::string& logical_name) const { + return mapping_.find(logical_name) != mapping_.end(); + } + + template + BlackboardKey Key(const std::string& logical_name) const { + auto it = mapping_.find(logical_name); + if (it != mapping_.end()) { + return BlackboardKey{it->second.c_str(), + BlackboardTypeTraits::TypeName()}; + } + return BlackboardKey{"", BlackboardTypeTraits::TypeName()}; + } + + const std::string& GetActualKey(const std::string& logical_name) const { + auto it = mapping_.find(logical_name); + if (it != mapping_.end()) { + return it->second; + } + static const std::string kEmpty; + return kEmpty; + } + + const std::unordered_map& All() const { + return mapping_; + } + + private: + std::unordered_map mapping_; +}; + +class OutputPortBindings { + public: + OutputPortBindings() = default; + explicit OutputPortBindings( + std::unordered_map mapping) + : mapping_(std::move(mapping)) {} + + bool HasKey(const std::string& logical_name) const { + return mapping_.find(logical_name) != mapping_.end(); + } + + template + BlackboardKey Key(const std::string& logical_name) const { + auto it = mapping_.find(logical_name); + if (it != mapping_.end()) { + return BlackboardKey{it->second.c_str(), + BlackboardTypeTraits::TypeName()}; + } + return BlackboardKey{"", BlackboardTypeTraits::TypeName()}; + } + + const std::string& GetActualKey(const std::string& logical_name) const { + auto it = mapping_.find(logical_name); + if (it != mapping_.end()) { + return it->second; + } + static const std::string kEmpty; + return kEmpty; + } + + const std::unordered_map& All() const { + return mapping_; + } + + private: + std::unordered_map mapping_; +}; + +/** + * @brief 外部槽位定义 (C ABI 结构体或 Operator 槽位) + */ +struct ExternalSlotDefinition { + std::string slot_name; + std::string type_id; + PortDirection direction = PortDirection::kInput; + bool required = true; + std::string value_type; + std::string type_suffix; // Operator ValueType 规范后缀 (如 "plain_text", + // "entity_out") + std::vector capacity_fields; + std::string key_suffix; // 外部 map key 后缀 (为空时使用 type_suffix) + + ExternalSlotDefinition() = default; + ExternalSlotDefinition(std::string slot_name, std::string type_id, + PortDirection direction = PortDirection::kInput, + bool required = true, std::string value_type = "", + std::string type_suffix = "", + std::vector capacity_fields = {}, + std::string key_suffix = "") + : slot_name(std::move(slot_name)), + type_id(std::move(type_id)), + direction(direction), + required(required), + value_type(std::move(value_type)), + type_suffix(std::move(type_suffix)), + capacity_fields(std::move(capacity_fields)), + key_suffix(std::move(key_suffix)) {} + + const std::string& KeySuffix() const { + return !key_suffix.empty() ? key_suffix : type_suffix; + } +}; + +// 统一输入/输出转换回调函数指针类型 +using DecodeInputFn = int (*)(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status); + +using EncodeOutputFn = int (*)(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status); + +/** + * @brief 输入转换器 Definition + */ +struct InputConverterDefinition { + std::string converter_id; + std::string transport; // "cabi" 或 "operator" + std::string schema_id; + int schema_version = 1; + std::string external_type; + std::vector external_slots; + std::vector logical_ports; // 发布的内部逻辑输出端口 + size_t max_batch_size = 64; + std::string ownership_policy = "copy_in"; + std::string thread_model = "stateless"; + DecodeInputFn decode_fn = nullptr; +}; + +/** + * @brief 输出转换器 Definition + */ +struct OutputConverterDefinition { + std::string converter_id; + std::string transport; // "cabi" 或 "operator" + std::string schema_id; + int schema_version = 1; + std::string external_type; + std::vector logical_ports; // 消费的内部逻辑输入端口 + std::vector external_slots; + std::string cardinality = "1:1"; + size_t max_batch_size = 64; + std::string capacity_policy = "reject_overflow"; + std::string thread_model = "stateless"; + EncodeOutputFn encode_fn = nullptr; +}; + +} // namespace llm_edgeflow diff --git a/include/adapter/io_converter_registry.h b/include/adapter/io_converter_registry.h new file mode 100644 index 00000000..c5fe19c2 --- /dev/null +++ b/include/adapter/io_converter_registry.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include + +#include "adapter/io_converter.h" + +namespace llm_edgeflow { + +class IoConverterRegistry { + public: + static IoConverterRegistry& Instance(); + + bool RegisterInputConverter(const InputConverterDefinition& def); + bool RegisterOutputConverter(const OutputConverterDefinition& def); + + const InputConverterDefinition* FindInputConverter( + const std::string& converter_id) const; + const OutputConverterDefinition* FindOutputConverter( + const std::string& converter_id) const; + + std::vector AllInputConverters() const; + std::vector AllOutputConverters() const; + + bool HasConflict() const; + std::vector GetConflictErrors() const; + + void ClearForTesting(); + void ResetConflictForTesting(); + + private: + IoConverterRegistry() = default; + ~IoConverterRegistry() = default; + + mutable std::mutex mutex_; + std::unordered_map input_converters_; + std::unordered_map output_converters_; + std::vector conflict_errors_; +}; + +} // namespace llm_edgeflow diff --git a/include/adapter/operator_biz_bridge.h b/include/adapter/operator_biz_bridge.h deleted file mode 100644 index 21e45b02..00000000 --- a/include/adapter/operator_biz_bridge.h +++ /dev/null @@ -1,257 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "adapter/operator_io_contracts.h" -#include "edgeflow/c_api.h" -#include "edgeflow/operator/types.h" - -namespace llm_edgeflow { - -/** - * @brief Process 执行期局部影子 DTO 存储器 - * (保证执行期间指针生命周期与地址绝对稳定) - */ -struct ProcessLocalShadowStorage { - std::deque strings; - std::deque> float_vectors; - std::vector> shadow_dtos; - - const char* StoreString(const CompanyString* cs) { - if (!cs || cs->length <= 0 || !cs->data) { - strings.emplace_back(""); - return strings.back().c_str(); - } - strings.emplace_back(cs->data, cs->length); - return strings.back().c_str(); - } - - const char* StoreOptionalString(const CompanyString* cs) { - return cs ? StoreString(cs) : nullptr; - } - - template - T* AllocateShadowDto() { - auto dto = std::make_shared(); - T* raw = dto.get(); - shadow_dtos.push_back(std::move(dto)); - return raw; - } -}; - -using ConvertSampleOutputFn = int (*)(const void* internal_dto, - void* external_output_struct, - const ResolvedOutputPoolSpec& spec, - std::string* err); - -/** - * @brief 业务逻辑槽位定义 - */ -struct OperatorBizSlot { - std::string logical_name; // 业务逻辑槽位名 (业务与方向内唯一) - std::string type_suffix; // 规范类型后缀 - IoDirection direction = IoDirection::kInput; - bool required = true; - // Output key suffix in the returned NamedIoBatch map. - std::string key_suffix{}; - ConvertSampleOutputFn convert_output = nullptr; - - const std::string& KeySuffix() const { return key_suffix; } - - bool operator==(const OperatorBizSlot& other) const { - return logical_name == other.logical_name && - type_suffix == other.type_suffix && direction == other.direction && - required == other.required && key_suffix == other.key_suffix && - convert_output == other.convert_output; - } -}; - -using ConvertSampleInputFn = int (*)( - const std::unordered_map& slots_by_logical_name, - ProcessLocalShadowStorage& storage, const void** out_internal_dto, - std::string* err); - -using CreateShadowOutputDtoFn = void* (*)(ProcessLocalShadowStorage& storage); - -/** - * @brief 业务桥接描述符 - */ -struct OperatorBizBridgeDescriptor { - CompanyAlgBizType biz_type = ALG_BIZ_TYPE_UNKNOWN; - std::string adapter_name; - std::string internal_input_type_name; - std::string internal_output_type_name; - std::string registration_identity; - std::vector input_slots; - std::vector output_slots; - ConvertSampleInputFn convert_sample_input = nullptr; - CreateShadowOutputDtoFn create_shadow_output_dto = nullptr; - - bool operator==(const OperatorBizBridgeDescriptor& other) const { - return biz_type == other.biz_type && adapter_name == other.adapter_name && - internal_input_type_name == other.internal_input_type_name && - internal_output_type_name == other.internal_output_type_name && - registration_identity == other.registration_identity && - input_slots == other.input_slots && - output_slots == other.output_slots && - convert_sample_input == other.convert_sample_input && - create_shadow_output_dto == other.create_shadow_output_dto; - } -}; - -// 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, 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, "", 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(); - }; - return desc; -} - -template -struct OperatorSlotTraits; - -template <> -struct OperatorSlotTraits { - inline static constexpr const char* kTypeSuffix = "entity_in"; - inline static constexpr IoDirection kDirection = IoDirection::kInput; -}; - -template <> -struct OperatorSlotTraits { - inline static constexpr const char* kTypeSuffix = "entity_out"; - inline static constexpr IoDirection kDirection = IoDirection::kOutput; -}; - -template -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_key_suffix = "") { - auto desc = MakeSingleSlotBizBridge( - biz_type, std::move(adapter_name), std::move(internal_input_type_name), - std::move(identity), input_slot, output_slot); - - desc.input_slots.front().type_suffix = - 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, - ProcessLocalShadowStorage& storage, const void** out_internal_dto, - std::string* err) -> int { - const void* payload = nullptr; - auto it = slots.find(OperatorSlotTraits::kTypeSuffix); - if (it != slots.end() && it->second) { - payload = it->second; - } else if (slots.size() == 1 && slots.begin()->second) { - payload = slots.begin()->second; - } - if (!payload) { - if (err) { - *err = std::string("Missing required input slot ") + - OperatorSlotTraits::kTypeSuffix; - } - return -3; - } - const auto* in = static_cast(payload); - const InternalInput* typed_dto = nullptr; - const int ret = ConvertInput(*in, storage, &typed_dto, err); - if (ret == 0 && out_internal_dto) { - *out_internal_dto = typed_dto; - } - return ret; - }; - - 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) { - if (err) *err = "Null internal DTO or external output struct pointer"; - return -4; - } - const auto* in_dto = static_cast(internal_dto); - auto* out = static_cast(external_output_struct); - return ConvertOutput(*in_dto, *out, spec, err); - }; - - return desc; -} - -template -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, - 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); - - desc.input_slots.front().type_suffix = - 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.output_slots.front().convert_output = convert_output; - return desc; -} - -// Source-extension registration and output copy helpers. Registry state and -// output pool management remain private to the Integration implementation. -bool RegisterOperatorBizBridge(OperatorBizBridgeDescriptor descriptor); -int CopyToOperatorString(const char* source, CompanyString* destination, - uint32_t capacity, const char* field_name, - std::string* diagnostic) noexcept; - -/** - * @brief 就地业务自注册宏 (无需在中心维护列表) - */ -#define REGISTER_OPERATOR_BIZ_BRIDGE(BridgeRegisterFn) \ - namespace { \ - struct AutoRegister_##BridgeRegisterFn { \ - AutoRegister_##BridgeRegisterFn() { BridgeRegisterFn(); } \ - }; \ - static AutoRegister_##BridgeRegisterFn g_auto_register_##BridgeRegisterFn; \ - } - -} // namespace llm_edgeflow diff --git a/include/adapter/result_packing_adapter.h b/include/adapter/result_packing_adapter.h deleted file mode 100644 index b4f25292..00000000 --- a/include/adapter/result_packing_adapter.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include - -#include "adapter/biz_adapter_interface.h" - -namespace llm_edgeflow { - -// One business PackTyped implementation serves both public C arrays and the -// Operator's owned variable-length results. Validation cannot drift by facade. -template -class ResultPackingAdapter : public IBizAdapter { - public: - int Pack(AlgContext* ctx, void** outputs, int* count, - AdapterStatus* status = nullptr) const final { - return static_cast(this)->template PackTyped( - ctx, outputs, count, status); - } - const char* ResultTypeName() const final { return Result::kTypeName; } - int PackResultBatch(AlgContext* ctx, void** outputs, int* count, - AdapterStatus* status = nullptr) const final { - return static_cast(this)->template PackTyped( - ctx, outputs, count, status); - } -}; - -template -bool CopyResultString(char (&destination)[N], const char* value, - const char* field, int index, const char* biz, - AdapterStatus* status) { - return AdapterValidationHelper::CheckedStringCopy(destination, N, value, - field, index, biz, status); -} - -inline bool CopyResultString(std::string& destination, const char* value, - const char*, int, const char*, AdapterStatus*) { - destination = value ? value : ""; - return true; -} - -} // namespace llm_edgeflow diff --git a/include/adapter/text_carrier.h b/include/adapter/text_carrier.h index 8b2159aa..35e8bfec 100644 --- a/include/adapter/text_carrier.h +++ b/include/adapter/text_carrier.h @@ -8,7 +8,6 @@ #include "adapter/adapter_status.h" #include "adapter/adapter_validation_helper.h" #include "adapter/biz_results.h" -#include "adapter/operator_biz_bridge.h" #include "edgeflow/c_api.h" #include "edgeflow/operator/types.h" @@ -110,24 +109,4 @@ inline int WriteTextCarrierOutput( return COMPANY_ALG_SUCCESS; } -/** - * @brief 共享文本载体 Operator 单槽位转换函数 - */ -int ConvertTextCarrierInput(const CompanyOperatorEntityInput& in, - ProcessLocalShadowStorage& storage, - const CompanyEntityInputStruct** out_internal_dto, - std::string* err); - -int ConvertTextCarrierOutput(const EntityResult& in_dto, - CompanyOperatorEntityOutput& out, - const ResolvedOutputPoolSpec& spec, - std::string* err); - -/** - * @brief 共享文本载体 Operator 桥接描述符构造 - */ -OperatorBizBridgeDescriptor MakeTextCarrierBridge(CompanyAlgBizType biz_type, - std::string adapter_name, - std::string identity); - } // namespace llm_edgeflow diff --git a/include/core/pipeline.h b/include/core/pipeline.h index a530bd06..0caf05a3 100644 --- a/include/core/pipeline.h +++ b/include/core/pipeline.h @@ -53,6 +53,12 @@ class Pipeline { PipelineDiagnostic* diagnostic = nullptr, ValidationPolicy policy = ValidationPolicy::kStrict); + /** + * @brief 从已验证的管线计划构建整条管线 (接收所有权,避免重复验证与物化) + */ + bool BuildFromPlan(std::unique_ptr plan, + PipelineDiagnostic* diagnostic = nullptr); + /** * @brief 按照拓扑排序/波前序列执行单次批次管线推理 */ diff --git a/include/core/pipeline_validator.h b/include/core/pipeline_validator.h index 663b2515..785c16a4 100644 --- a/include/core/pipeline_validator.h +++ b/include/core/pipeline_validator.h @@ -8,6 +8,7 @@ #include "core/diagnostic_code.h" #include "core/pipeline_config.h" +#include "core/port_definition.h" #include "core/remediation_cause.h" #include "core/validated_node_plan.h" #include "engine/inference_definition.h" @@ -85,19 +86,27 @@ struct ValidatedPipelinePlan { ValidationReport report; }; +struct PipelineIoBoundary { + std::vector input_published_ports; + std::vector output_consumed_ports; +}; + class PipelineValidator { public: static ValidatedPipelinePlan ValidateAndPlan( const nlohmann::json& root, - ValidationPolicy policy = ValidationPolicy::kStrict); + ValidationPolicy policy = ValidationPolicy::kStrict, + const PipelineIoBoundary* io_boundary = nullptr); static ValidationReport Validate( const nlohmann::json& root, - ValidationPolicy policy = ValidationPolicy::kStrict); + ValidationPolicy policy = ValidationPolicy::kStrict, + const PipelineIoBoundary* io_boundary = nullptr); static ValidationReport Explain( const nlohmann::json& root, - ValidationPolicy policy = ValidationPolicy::kStrict); + ValidationPolicy policy = ValidationPolicy::kStrict, + const PipelineIoBoundary* io_boundary = nullptr); }; /** diff --git a/include/edgeflow/operator/interface.h b/include/edgeflow/operator/interface.h index 64286f44..b4c8a840 100644 --- a/include/edgeflow/operator/interface.h +++ b/include/edgeflow/operator/interface.h @@ -57,19 +57,19 @@ COMPANY_ALG_API OperatorFunc Get_LLM_EDGEFLOW_OperatorTable() noexcept; COMPANY_ALG_API const char* GetOperatorLastError() noexcept; /** - * @brief 校验部署配置 .conf 与预期业务类型是否兼容 + * @brief 校验部署配置 .conf 与预期接入绑定是否兼容 * (只读无副作用预检,不抛出任何异常) * @param model_path 模型与配置根目录 * @param cfg_file_name 相对配置文件路径 - * @param expected_biz_type 预期算法业务类型 (CompanyAlgBizType) + * @param expected_binding_id 预期接入绑定 ID (如 "translate.operator.v1") * @param out_error_msg 错误输出信息缓冲区 (可选) * @param error_buf_size 缓冲区容量 * @return 0 校验通过且兼容, -1 参数非法, -2 配置解析或文件不存在/逃逸, -3 - * 业务不匹配 + * 绑定不匹配 */ COMPANY_ALG_API int ValidateOperatorConfigBinding( const char* model_path, const char* cfg_file_name, - int32_t expected_biz_type, char* out_error_msg = nullptr, + const char* expected_binding_id, char* out_error_msg = nullptr, size_t error_buf_size = 0) noexcept; } // namespace llm_edgeflow::operator_api diff --git a/include/platform_mock/alg_types.h b/include/platform_mock/alg_types.h index 526fd012..be60e5a3 100644 --- a/include/platform_mock/alg_types.h +++ b/include/platform_mock/alg_types.h @@ -11,34 +11,11 @@ extern "C" { #endif -// 业务类型枚举 -typedef enum { - ALG_BIZ_TYPE_UNKNOWN = 0, - ALG_BIZ_TYPE_DOC_QA = 1, // 业务 3: 智能长文档问答 (Embedding + LLM) - ALG_BIZ_TYPE_KEYWORD_MATCH = - 2, // 业务 1: 关注词匹配业务 (无模型, Control动态词表) - ALG_BIZ_TYPE_ENTITY_EXTRACT = 3, // 业务 2: 实体/名词提取 (0.6B LLM) - ALG_BIZ_TYPE_COMPLIANCE_AUDIT = - 4, // 业务 4: 智能对话风控质检 (Embedding + Reranker + LLM 多模型多节点) - ALG_BIZ_TYPE_OCR_DOC_QA = 5, // 业务 5: 智能多模态图文票据抽取 (OCR + LLM) - ALG_BIZ_TYPE_AUDIO_ASR_INTENT = - 6, // 业务 6: 语音识别与意图槽位抽取 (Audio ASR + NLU) - ALG_BIZ_TYPE_CROSS_RERANK = - 7, // 业务 7: 纯语义精排打分 (Query-Passage Cross-Encoder) - // Reuses CompanyEntityInputStruct/CompanyEntityOutputStruct: sentence_text - // carries the complete request JSON; entities_json carries the response JSON. - ALG_BIZ_TYPE_TRANSLATE = 8, - // Force a 32-bit ABI representation and keep positive invalid-value probes - // representable when enum sanitization is enabled. - ALG_BIZ_TYPE_MAX_GUARD = INT32_MAX -} CompanyAlgBizType; - // 句柄创建参数结构体 typedef struct { - const char* config_file_path; // 业务配置文件路径 (JSON) + const char* config_file_path; // 接入配置文件路径 (JSON, schema_version 1) const char* model_root_dir; // 直接包含模型 artifact 与 sidecar 的部署目录 int device_id; // 目标加速设备 ID (如 NPU 0, GPU 0) - CompanyAlgBizType biz_type; // 业务类型 } CompanyAlgParamCreate; // 运行时动态控制参数结构体 diff --git a/scripts/check_layer_dependencies.py b/scripts/check_layer_dependencies.py index 6d55fc49..5ef8c425 100644 --- a/scripts/check_layer_dependencies.py +++ b/scripts/check_layer_dependencies.py @@ -136,7 +136,7 @@ def write(path, content=""): target.write_text(content) return target - for header in ("include/adapter/biz_adapter_interface.h", + for header in ("include/adapter/io_converter.h", "include/core/pipeline_validator.h", "include/core/alg_context.h", "include/edgeflow/c_api.h", "include/platform_mock/alg_types.h", @@ -146,12 +146,12 @@ def write(path, content=""): "src/custom_nodes/domain_node.h"): write(header) cases = [ - ("src/engine/runtime/bad.cpp", "adapter/biz_adapter_interface.h"), - ("src/core/bad.cpp", "adapter/biz_adapter_interface.h"), + ("src/engine/runtime/bad.cpp", "adapter/io_converter.h"), + ("src/core/bad.cpp", "adapter/io_converter.h"), ("include/nodes/bad.h", "edgeflow/c_api.h"), ("src/common_nodes/bad.cpp", "core/pipeline_validator.h"), ("src/custom_nodes/bad.cpp", "core/pipeline_validator.h"), - ("include/nodes/bad.h", "../adapter/biz_adapter_interface.h"), + ("include/nodes/bad.h", "../adapter/io_converter.h"), ("src/common_nodes/bad.cpp", "custom_nodes/domain_node.h"), ("include/core/session_context.h", "core/pipeline_validator.h"), ] @@ -179,7 +179,7 @@ def write(path, content=""): file.unlink() # A local helper must not conceal a reverse dependency. write("src/engine/models/demo/model.cpp", '#include "helper.h"\n') - helper = write("src/engine/models/demo/helper.h", '#include "adapter/biz_adapter_interface.h"\n') + helper = write("src/engine/models/demo/helper.h", '#include "adapter/io_converter.h"\n') assert check(root) helper.write_text("") write("include/nodes/good.h", '#include "core/alg_context.h"\n') diff --git a/scripts/check_layer_isolation.sh b/scripts/check_layer_isolation.sh index dc901e59..1cd78783 100755 --- a/scripts/check_layer_isolation.sh +++ b/scripts/check_layer_isolation.sh @@ -117,7 +117,7 @@ if [[ "${1:-}" == "--self-test" ]]; then for CUSTOM_INCLUDE in \ '#include "edgeflow/c_api.h"' \ '# include "../adapter/biz_blackboard_keys.h"' \ - '#include "adapter/biz_adapter_interface.h"' \ + '#include "adapter/io_converter.h"' \ '#include "edgeflow/operator/types.h"' \ '#include "platform_mock/operator_types.h"'; do echo "${CUSTOM_INCLUDE}" > "${TMP_TEST_DIR}/violation_repo/src/custom_nodes/bad_node.cpp" diff --git a/scripts/check_sdk_exports.sh b/scripts/check_sdk_exports.sh index c65352dd..b4a1af78 100755 --- a/scripts/check_sdk_exports.sh +++ b/scripts/check_sdk_exports.sh @@ -24,7 +24,7 @@ EXPECTED_SYMBOLS=$( Alg_Init \ Alg_Process \ _ZN12llm_edgeflow12operator_api20GetOperatorLastErrorEv \ - _ZN12llm_edgeflow12operator_api29ValidateOperatorConfigBindingEPKcS2_iPcm \ + _ZN12llm_edgeflow12operator_api29ValidateOperatorConfigBindingEPKcS2_S2_Pcm \ _ZN12llm_edgeflow12operator_api30Get_LLM_EDGEFLOW_OperatorTableEv ) diff --git a/scripts/dev_recipe.py b/scripts/dev_recipe.py index bdd9dd5c..d962895a 100644 --- a/scripts/dev_recipe.py +++ b/scripts/dev_recipe.py @@ -124,8 +124,8 @@ def native(tool, arguments, root, document=None): raise RecipeError("Native " + arguments[0] + " failed", report) if arguments and arguments[0] == "catalog": schema_version = report.get("schema_version") - if schema_version != 3: - raise RecipeError(f"Unsupported Catalog schema version {schema_version}; dev_recipe requires Catalog v3", report) + if schema_version not in (3, 4): + raise RecipeError(f"Unsupported Catalog schema version {schema_version}; dev_recipe requires Catalog v3 or v4", report) return report @@ -158,7 +158,7 @@ def make_recipe_conf(pipeline_doc, outputs, pipeline_target, root, model_root=No 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, outputs, - pipeline.relative_to(bundle), models, bundle) + pipeline.name, models, bundle) def command(description, argv): @@ -310,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, outputs, preview_pipeline.relative_to(bundle), models, bundle) + preview_conf = VERIFY_SELECTION.build_run_conf(deployment_preview, outputs, preview_pipeline.name, 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/CMakeLists.txt b/src/adapter/CMakeLists.txt index 553a9c0b..14e2f865 100644 --- a/src/adapter/CMakeLists.txt +++ b/src/adapter/CMakeLists.txt @@ -1,28 +1,37 @@ target_sources(edgeflow_integration_objects PRIVATE - biz_adapter_registry.cpp - biz/audio_asr_intent_adapter.cpp - biz/compliance_audit_adapter.cpp - biz/cross_rerank_adapter.cpp - biz/doc_qa_adapter.cpp - biz/entity_extract_adapter.cpp - biz/translate_adapter.cpp - biz/keyword_match_adapter.cpp - biz/ocr_doc_qa_adapter.cpp c_api_adapter.cpp deployment_model_resolver.cpp - text_carrier.cpp - biz/audio_asr_intent_operator_bridge.cpp - biz/compliance_audit_operator_bridge.cpp - biz/cross_rerank_operator_bridge.cpp - biz/doc_qa_operator_bridge.cpp - biz/entity_extract_operator_bridge.cpp - biz/translate_operator_bridge.cpp - biz/keyword_match_operator_bridge.cpp - biz/ocr_doc_qa_operator_bridge.cpp + io_converter_registry.cpp + io_binding_registry.cpp + deployment_io_config.cpp + io_binding_resolver.cpp + io_catalog.cpp + input/text_input.cpp + input/translate_json_input.cpp + input/doc_query_input.cpp + input/rerank_input.cpp + input/audit_input.cpp + input/audio_input.cpp + input/image_query_input.cpp + output/structured_document_output.cpp + output/translation_json_output.cpp + output/keyword_result_output.cpp + output/doc_answer_output.cpp + output/rerank_result_output.cpp + output/audit_result_output.cpp + output/audio_result_output.cpp + output/invoice_result_output.cpp + biz/translate_bindings.cpp + biz/entity_extract_bindings.cpp + biz/keyword_match_bindings.cpp + biz/doc_qa_bindings.cpp + biz/cross_rerank_bindings.cpp + biz/compliance_audit_bindings.cpp + biz/audio_asr_intent_bindings.cpp + biz/ocr_doc_qa_bindings.cpp operator/json_output_config_reader.cpp operator/operator_config_resolver.cpp operator/operator_adapter.cpp - operator/operator_biz_bridge_registry.cpp operator/operator_control_registry.cpp operator/operator_output_pool.cpp operator/operator_process_binding.cpp diff --git a/src/adapter/biz/README.md b/src/adapter/biz/README.md new file mode 100644 index 00000000..d4da3dde --- /dev/null +++ b/src/adapter/biz/README.md @@ -0,0 +1,8 @@ +# Business IO Bindings & Exposures (业务接入绑定与契约暴露) + +本目录包含各个业务场景的 `BizDefinition` 注册、生产接入暴露声明 (`REGISTER_BIZ_EXPOSURE`) 以及显式 I/O 绑定 (`REGISTER_IO_BINDING`)。 + +## 规范与契约 +- 每个业务绑定文件独立声明该业务的内部 Blackboard 契约。 +- 显式声明该业务支持的 C ABI 绑定与 Operator 绑定,将独立输入/输出转换器与业务逻辑端口映射起来。 +- 业务配置通过 `data.io_binding` 显式选择全局唯一的 `binding_id`。 diff --git a/src/adapter/biz/audio_asr_intent_adapter.cpp b/src/adapter/biz/audio_asr_intent_adapter.cpp deleted file mode 100644 index b4af7d70..00000000 --- a/src/adapter/biz/audio_asr_intent_adapter.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#include -#include - -#include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_registry.h" -#include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_input_constraints.h" -#include "adapter/biz_results.h" -#include "adapter/result_packing_adapter.h" -#include "adapter/result_validation.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -inline static constexpr char kAudioAsrBizName[] = - "speech_audio_asr_intent_slot"; - -class AudioAsrIntentAdapter - : public ResultPackingAdapter { - public: - CompanyAlgBizType BizType() const override { - return ALG_BIZ_TYPE_AUDIO_ASR_INTENT; - } - - const char* AdapterName() const override { return "AudioAsrIntent"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - ALG_BIZ_TYPE_AUDIO_ASR_INTENT, - "AudioAsrIntent", - COMPANY_ALG_ABI_VERSION, - "CompanyAudioInputStruct", - "CompanyAudioOutputStruct", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {{kAudioAsrBizName, - "audio_asr", - "语音识别与意图槽位", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(kAudioInputs)}, - {BizOutput(kIntentSlots), BizOutput(kTranscripts)}}}}; - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { - int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); - if (valid_ret != 0 || !ctx) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Batch envelope validation failed or null AlgContext", - "inputs", AdapterName()); - } - - std::vector raw_req_ids; - AudioPcmBatch raw_audios; - - raw_req_ids.reserve(num_inputs); - raw_audios.reserve(num_inputs); - - for (int i = 0; i < num_inputs; ++i) { - auto* in_audio = static_cast(inputs[i]); - if (!AdapterValidationHelper::RequireNotNull("inputs[i]", in_audio, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (!AdapterValidationHelper::RequireRange( - "inputs[i].sample_rate", in_audio->sample_rate, - biz_input::kMinSampleRate, biz_input::kMaxSampleRate, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - if (!AdapterValidationHelper::RequireRange( - "inputs[i].pcm_length", in_audio->pcm_length, 0, - biz_input::kMaxAudioPcmSamples, i, AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (in_audio->pcm_length > 0) { - if (!AdapterValidationHelper::RequireNotNull( - "inputs[i].pcm_buffer", in_audio->pcm_buffer, i, AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - if (!AdapterValidationHelper::CheckedMultiply( - "inputs[i].pcm_buffer", in_audio->pcm_length, sizeof(float), - biz_input::kMaxAudioPcmBytes, i, AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - } - - raw_req_ids.push_back(in_audio->request_id); - - AudioPcmPayload pcm_dto; - if (in_audio->pcm_buffer && in_audio->pcm_length > 0) { - pcm_dto.pcm_data.assign(in_audio->pcm_buffer, - in_audio->pcm_buffer + in_audio->pcm_length); - } - pcm_dto.sample_rate = in_audio->sample_rate; - raw_audios.emplace_back(static_cast(i), 0, std::move(pcm_dto)); - } - - if (!AdapterValidationHelper::PublishContextValue( - *ctx, kRawRequestIds, std::move(raw_req_ids), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kAudioInputs, std::move(raw_audios), AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - return COMPANY_ALG_SUCCESS; - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const { - if (!ctx) { - return AdapterValidationHelper::ReturnBufferTooSmall( - out_status, "Null AlgContext passed to Pack", "ctx", AdapterName()); - } - - const auto* transcripts = AdapterValidationHelper::ReadRequiredContextValue( - *ctx, kTranscripts, AdapterName(), out_status); - if (!transcripts) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - - const auto* intent_slots = ctx->Read(kIntentSlots); - const auto* raw_req_ids = ctx->Read(kRawRequestIds); - - int count = static_cast(transcripts->size()); - int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( - outputs, num_outputs, count, AdapterName(), out_status); - if (valid_ret != 0) return valid_ret; - - std::vector transcripts_by_request; - if (!IndexResults(transcripts, raw_req_ids, &transcripts_by_request, - "transcripts", AdapterName(), out_status)) - return COMPANY_ALG_ERR_INVALID_INPUT; - std::vector intent_slots_by_request; - if (!IndexResults(intent_slots, raw_req_ids, &intent_slots_by_request, - "intent_slots", AdapterName(), out_status)) - return COMPANY_ALG_ERR_INVALID_INPUT; - - for (int i = 0; i < count; ++i) { - auto* out_ptr = static_cast(outputs[i]); - out_ptr->request_id = (*raw_req_ids)[i]; - out_ptr->status_code = intent_slots_by_request[i]->data.status_code; - - const std::string& slot_json = - intent_slots_by_request[i]->data.match_result_json; - - if (!CopyResultString(out_ptr->transcribed_text, - transcripts_by_request[i]->data.c_str(), - "outputs[i].transcribed_text", i, AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - - if (!CopyResultString(out_ptr->intent_slot_json, slot_json.c_str(), - "outputs[i].intent_slot_json", i, AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - } - *num_outputs = count; - return COMPANY_ALG_SUCCESS; - } -}; - -REGISTER_BIZ_ADAPTER(AudioAsrIntentAdapter); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/audio_asr_intent_bindings.cpp b/src/adapter/biz/audio_asr_intent_bindings.cpp new file mode 100644 index 00000000..4a997897 --- /dev/null +++ b/src/adapter/biz/audio_asr_intent_bindings.cpp @@ -0,0 +1,75 @@ +#include "adapter/converter_authoring.h" +#include "adapter/io_binding.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +BizDefinition MakeAudioAsrIntentBizDefinition() { + BizDefinition def; + def.biz_name = "speech_audio_asr_intent_slot"; + def.demo_biz = "audio_asr"; + def.display_name = "语音识别与意图槽位"; + def.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("audio_inputs", "AudioPcmBatch", true, "1:1")}; + def.egress = { + BizPortDefinition("transcripts", "TextBatch", true, "1:1"), + BizPortDefinition("intent_slots", "RuleMatchBatch", true, "1:1")}; + return def; +} + +const bool g_reg_audio_asr_intent_biz = []() { + auto def = MakeAudioAsrIntentBizDefinition(); + if (!PipelineCatalog::FindBiz(def.biz_name)) { + PipelineCatalog::RegisterBizDefinition(def); + } + return true; +}(); + +BizExposureDefinition MakeAudioAsrIntentBizExposure() { + BizExposureDefinition def; + def.biz_name = "speech_audio_asr_intent_slot"; + def.max_batch_size = 64; + def.required_transports = {"cabi", "operator"}; + return def; +} + +IoBindingDefinition MakeAudioAsrIntentCAbiBinding() { + IoBindingDefinition def; + def.binding_id = "audio_asr_intent.cabi.v1"; + def.biz_name = "speech_audio_asr_intent_slot"; + def.transport = "cabi"; + def.input_converter_id = "audio.pcm.cabi.v1"; + def.output_converter_id = "audio_result.plain.cabi.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"audio_inputs", "audio_inputs"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"transcripts", "transcripts"}, + {"intent_slots", "intent_slots"}}; + def.max_batch_size = 64; + return def; +} + +IoBindingDefinition MakeAudioAsrIntentOperatorBinding() { + IoBindingDefinition def; + def.binding_id = "audio_asr_intent.operator.v1"; + def.biz_name = "speech_audio_asr_intent_slot"; + def.transport = "operator"; + def.input_converter_id = "audio.pcm.operator.v1"; + def.output_converter_id = "audio_result.plain.operator.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"audio_inputs", "audio_inputs"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"transcripts", "transcripts"}, + {"intent_slots", "intent_slots"}}; + def.max_batch_size = 64; + return def; +} + +REGISTER_BIZ_EXPOSURE(MakeAudioAsrIntentBizExposure()); +REGISTER_IO_BINDING(MakeAudioAsrIntentCAbiBinding()); +REGISTER_IO_BINDING(MakeAudioAsrIntentOperatorBinding()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/biz/audio_asr_intent_operator_bridge.cpp b/src/adapter/biz/audio_asr_intent_operator_bridge.cpp deleted file mode 100644 index 85e7af46..00000000 --- a/src/adapter/biz/audio_asr_intent_operator_bridge.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "adapter/biz_results.h" -#include "adapter/operator_biz_bridge.h" - -namespace llm_edgeflow { - -void RegisterAudioAsrIntentBridge() { - auto desc = MakeSingleSlotBizBridge( - ALG_BIZ_TYPE_AUDIO_ASR_INTENT, "AudioAsrIntent", - "CompanyAudioInputStruct", "builtin.audio_asr_intent", "audio_in", - "audio_out"); - - desc.convert_sample_input = - [](const std::unordered_map& slots, - ProcessLocalShadowStorage& storage, const void** out_internal_dto, - std::string* err) -> int { - auto it = slots.find("audio_in"); - if (it == slots.end() || !it->second) { - if (err) *err = "Missing required input slot audio_in"; - return -3; - } - const auto* in = static_cast(it->second); - auto* dto = storage.AllocateShadowDto(); - dto->request_id = in->request_id; - dto->pcm_length = in->pcm_length; - dto->sample_rate = in->sample_rate; - if (in->pcm_length > 0 && in->pcm_buffer) { - storage.float_vectors.emplace_back(in->pcm_buffer, - in->pcm_buffer + in->pcm_length); - dto->pcm_buffer = storage.float_vectors.back().data(); - } else { - dto->pcm_buffer = nullptr; - } - *out_internal_dto = dto; - return 0; - }; - - 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) { - if (err) *err = "Null internal DTO or external output struct pointer"; - return -4; - } - const auto* in_dto = static_cast(internal_dto); - auto* out = - static_cast(external_output_struct); - out->request_id = in_dto->request_id; - out->status_code = in_dto->status_code; - - int ret = CopyToOperatorString( - in_dto->transcribed_text.c_str(), out->transcribed_text, - spec.GetCapacity("transcribed_text"), "transcribed_text", err); - if (ret != 0) return ret; - - return CopyToOperatorString( - in_dto->intent_slot_json.c_str(), out->intent_slot_json, - spec.GetCapacity("intent_slot_json"), "intent_slot_json", err); - }; - - RegisterOperatorBizBridge(desc); -} - -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterAudioAsrIntentBridge); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/compliance_audit_adapter.cpp b/src/adapter/biz/compliance_audit_adapter.cpp deleted file mode 100644 index 7e336ebf..00000000 --- a/src/adapter/biz/compliance_audit_adapter.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include -#include -#include -#include - -#include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_registry.h" -#include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_input_constraints.h" -#include "adapter/biz_results.h" -#include "adapter/result_packing_adapter.h" -#include "adapter/result_validation.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -inline static constexpr char kDialogueAuditBizName[] = - "dialogue_compliance_audit_v1"; - -class ComplianceAuditAdapter - : public ResultPackingAdapter { - public: - CompanyAlgBizType BizType() const override { - return ALG_BIZ_TYPE_COMPLIANCE_AUDIT; - } - - const char* AdapterName() const override { return "ComplianceAudit"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - ALG_BIZ_TYPE_COMPLIANCE_AUDIT, - "ComplianceAudit", - COMPANY_ALG_ABI_VERSION, - "CompanyAuditInputStruct", - "CompanyAuditOutputStruct", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {{kDialogueAuditBizName, - "dialogue_audit", - "对话合规审核", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(kUserTexts), - RequiredBizInput(kChannelNames)}, - {BizOutput(kStructuredVerdicts), - BizPortDefinition{kMatchedPolicy.name, kMatchedPolicy.type_id, true, - "N:1", "aggregate", "request"}, - BizOutput(kRuleMatches)}}}}; - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { - int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); - if (valid_ret != 0 || !ctx) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Batch envelope validation failed or null AlgContext", - "inputs", AdapterName()); - } - - std::vector req_ids; - TextBatch user_texts; - TextBatch channel_names; - - req_ids.reserve(num_inputs); - user_texts.reserve(num_inputs); - channel_names.reserve(num_inputs); - - constexpr size_t kMaxTextLen = 64 * 1024; // 64KB 单文本上限 - - for (int i = 0; i < num_inputs; ++i) { - auto* in = static_cast(inputs[i]); - if (!AdapterValidationHelper::RequireNotNull("inputs[i]", in, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (!AdapterValidationHelper::RequireBoundedString( - "inputs[i].user_text", in->user_text, kMaxTextLen, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (in->channel_name && - !AdapterValidationHelper::RequireBoundedString( - "inputs[i].channel_name", in->channel_name, - biz_input::kMaxChannelNameBytes, i, AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - req_ids.push_back(in->request_id); - user_texts.emplace_back(static_cast(i), 0, in->user_text); - channel_names.emplace_back(static_cast(i), 0, - in->channel_name ? in->channel_name : ""); - } - - if (!AdapterValidationHelper::PublishContextValue( - *ctx, kRawRequestIds, std::move(req_ids), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kUserTexts, std::move(user_texts), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kChannelNames, std::move(channel_names), AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - return COMPANY_ALG_SUCCESS; - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const { - if (!ctx) { - return AdapterValidationHelper::ReturnBufferTooSmall( - out_status, "Null AlgContext passed to Pack", "ctx", AdapterName()); - } - - const auto* verdicts = AdapterValidationHelper::ReadRequiredContextValue( - *ctx, kStructuredVerdicts, AdapterName(), out_status); - if (!verdicts) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - - const auto* matched_policies = ctx->Read(kMatchedPolicy); - const auto* raw_req_ids = ctx->Read(kRawRequestIds); - - int count = static_cast(verdicts->size()); - int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( - outputs, num_outputs, count, AdapterName(), out_status); - if (valid_ret != 0) return valid_ret; - - if (!matched_policies || - matched_policies->size() < static_cast(count)) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, - "matched_policies missing or count mismatch in AlgContext", - "matched_policies", AdapterName()); - } - - std::vector verdicts_by_request; - if (!IndexResults(verdicts, raw_req_ids, &verdicts_by_request, "verdicts", - AdapterName(), out_status)) - return COMPANY_ALG_ERR_INVALID_INPUT; - std::vector matched_policies_by_request; - if (!IndexResults(matched_policies, raw_req_ids, - &matched_policies_by_request, "matched_policies", - AdapterName(), out_status, true)) - return COMPANY_ALG_ERR_INVALID_INPUT; - - for (int i = 0; i < count; ++i) { - auto* out_ptr = static_cast(outputs[i]); - out_ptr->request_id = (*raw_req_ids)[i]; - - const auto& verdict_item = verdicts_by_request[i]->data; - if (matched_policies_by_request[i]->data.rank != 1 || - !IsSuccessfulDocument(verdict_item) || - !verdict_item.structured_data.contains("risk_level") || - !verdict_item.structured_data.contains("risk_score") || - !verdict_item.structured_data["risk_level"].is_string() || - !verdict_item.structured_data["risk_score"].is_number()) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, - "structured_data missing or invalid risk_level/risk_score types", - "structured_verdicts", AdapterName(), i); - } - - std::string risk_level = - verdict_item.structured_data["risk_level"].get(); - float risk_score = - verdict_item.structured_data["risk_score"].get(); - if (!std::isfinite(risk_score) || risk_score < 0 || risk_score > 1 || - (risk_level != "SAFE" && risk_level != "LOW_RISK" && - risk_level != "MEDIUM_RISK" && risk_level != "HIGH_RISK")) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Invalid risk level or score", "structured_verdicts", - AdapterName(), i); - } - const std::string& verdict_json = verdict_item.json_payload; - - std::string policy_clause = matched_policies_by_request[i]->data.text; - - out_ptr->risk_score = risk_score; - out_ptr->status_code = 0; - - if (!CopyResultString(out_ptr->risk_level, risk_level.c_str(), - "outputs[i].risk_level", i, AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - - if (!CopyResultString(out_ptr->matched_policy_clause, - policy_clause.c_str(), - "outputs[i].matched_policy_clause", i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - - if (!CopyResultString(out_ptr->audit_verdict_json, verdict_json.c_str(), - "outputs[i].audit_verdict_json", i, AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - } - *num_outputs = count; - return COMPANY_ALG_SUCCESS; - } -}; - -REGISTER_BIZ_ADAPTER(ComplianceAuditAdapter); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/compliance_audit_bindings.cpp b/src/adapter/biz/compliance_audit_bindings.cpp new file mode 100644 index 00000000..8306a5a9 --- /dev/null +++ b/src/adapter/biz/compliance_audit_bindings.cpp @@ -0,0 +1,79 @@ +#include "adapter/converter_authoring.h" +#include "adapter/io_binding.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +BizDefinition MakeComplianceAuditBizDefinition() { + BizDefinition def; + def.biz_name = "dialogue_compliance_audit_v1"; + def.demo_biz = "dialogue_audit"; + def.display_name = "对话合规审核"; + def.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("user_texts", "TextBatch", true, "1:1"), + BizPortDefinition("channel_names", "TextBatch", true, "1:1")}; + def.egress = { + BizPortDefinition("structured_verdicts", "StructuredDocumentBatch", true, + "1:1"), + BizPortDefinition("matched_policy", "RankedTextBatch", true, "N:1")}; + return def; +} + +const bool g_reg_compliance_audit_biz = []() { + auto def = MakeComplianceAuditBizDefinition(); + if (!PipelineCatalog::FindBiz(def.biz_name)) { + PipelineCatalog::RegisterBizDefinition(def); + } + return true; +}(); + +BizExposureDefinition MakeComplianceAuditBizExposure() { + BizExposureDefinition def; + def.biz_name = "dialogue_compliance_audit_v1"; + def.max_batch_size = 64; + def.required_transports = {"cabi", "operator"}; + return def; +} + +IoBindingDefinition MakeComplianceAuditCAbiBinding() { + IoBindingDefinition def; + def.binding_id = "compliance_audit.cabi.v1"; + def.biz_name = "dialogue_compliance_audit_v1"; + def.transport = "cabi"; + def.input_converter_id = "audit.plain.cabi.v1"; + def.output_converter_id = "audit_result.plain.cabi.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"user_texts", "user_texts"}, + {"channel_names", "channel_names"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"structured_verdicts", "structured_verdicts"}, + {"matched_policies", "matched_policy"}}; + def.max_batch_size = 64; + return def; +} + +IoBindingDefinition MakeComplianceAuditOperatorBinding() { + IoBindingDefinition def; + def.binding_id = "compliance_audit.operator.v1"; + def.biz_name = "dialogue_compliance_audit_v1"; + def.transport = "operator"; + def.input_converter_id = "audit.plain.operator.v1"; + def.output_converter_id = "audit_result.plain.operator.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"user_texts", "user_texts"}, + {"channel_names", "channel_names"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"structured_verdicts", "structured_verdicts"}, + {"matched_policies", "matched_policy"}}; + def.max_batch_size = 64; + return def; +} + +REGISTER_BIZ_EXPOSURE(MakeComplianceAuditBizExposure()); +REGISTER_IO_BINDING(MakeComplianceAuditCAbiBinding()); +REGISTER_IO_BINDING(MakeComplianceAuditOperatorBinding()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/biz/compliance_audit_operator_bridge.cpp b/src/adapter/biz/compliance_audit_operator_bridge.cpp deleted file mode 100644 index 41d6a7ac..00000000 --- a/src/adapter/biz/compliance_audit_operator_bridge.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "adapter/biz_results.h" -#include "adapter/operator_biz_bridge.h" - -namespace llm_edgeflow { - -void RegisterComplianceAuditBridge() { - auto desc = MakeSingleSlotBizBridge( - ALG_BIZ_TYPE_COMPLIANCE_AUDIT, "ComplianceAudit", - "CompanyAuditInputStruct", "builtin.compliance_audit", "audit_in", - "audit_out"); - - desc.convert_sample_input = - [](const std::unordered_map& slots, - ProcessLocalShadowStorage& storage, const void** out_internal_dto, - std::string* err) -> int { - auto it = slots.find("audit_in"); - if (it == slots.end() || !it->second) { - if (err) *err = "Missing required input slot audit_in"; - return -3; - } - const auto* in = static_cast(it->second); - auto* dto = storage.AllocateShadowDto(); - dto->request_id = in->request_id; - dto->user_text = storage.StoreString(in->user_text); - dto->channel_name = storage.StoreOptionalString(in->channel_name); - *out_internal_dto = dto; - return 0; - }; - - 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) { - if (err) *err = "Null internal DTO or external output struct pointer"; - return -4; - } - const auto* in_dto = static_cast(internal_dto); - auto* out = - static_cast(external_output_struct); - out->request_id = in_dto->request_id; - out->risk_score = in_dto->risk_score; - out->status_code = in_dto->status_code; - - int ret = - CopyToOperatorString(in_dto->risk_level.c_str(), out->risk_level, - spec.GetCapacity("risk_level"), "risk_level", err); - if (ret != 0) return ret; - - ret = CopyToOperatorString(in_dto->matched_policy_clause.c_str(), - out->matched_policy_clause, - spec.GetCapacity("matched_policy_clause"), - "matched_policy_clause", err); - if (ret != 0) return ret; - - return CopyToOperatorString( - in_dto->audit_verdict_json.c_str(), out->audit_verdict_json, - spec.GetCapacity("audit_verdict_json"), "audit_verdict_json", err); - }; - - RegisterOperatorBizBridge(desc); -} - -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterComplianceAuditBridge); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/cross_rerank_adapter.cpp b/src/adapter/biz/cross_rerank_adapter.cpp deleted file mode 100644 index 2b169ce8..00000000 --- a/src/adapter/biz/cross_rerank_adapter.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#include -#include -#include - -#include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_registry.h" -#include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_results.h" -#include "adapter/result_packing_adapter.h" -#include "adapter/result_validation.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -inline static constexpr char kCrossRerankBizName[] = - "dense_cross_rerank_scoring"; - -class CrossRerankAdapter - : public ResultPackingAdapter< - CrossRerankAdapter, CompanyRerankBatchOutputStruct, RerankResult> { - public: - CompanyAlgBizType BizType() const override { - return ALG_BIZ_TYPE_CROSS_RERANK; - } - - const char* AdapterName() const override { return "CrossRerank"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - ALG_BIZ_TYPE_CROSS_RERANK, - "CrossRerank", - COMPANY_ALG_ABI_VERSION, - "CompanyRerankBatchInputStruct", - "CompanyRerankBatchOutputStruct", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {{kCrossRerankBizName, - "cross_rerank", - "Cross-Encoder 精排", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(kRerankQueries), - RequiredBizInput(kRerankCandidates)}, - {BizPortDefinition{kRankedResults.name, kRankedResults.type_id, true, - "N:1", "aggregate", "request"}}}}}; - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { - int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); - if (valid_ret != 0 || !ctx) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Batch envelope validation failed or null AlgContext", - "inputs", AdapterName()); - } - - std::vector raw_req_ids; - TextBatch queries; - RankedTextBatch candidates; - QueryCandidatesBatch pairs; - - raw_req_ids.reserve(num_inputs); - queries.reserve(num_inputs); - - constexpr size_t kMaxTextLen = 64 * 1024; // 64KB 单文本上限 - - for (int i = 0; i < num_inputs; ++i) { - auto* in_rerank = - static_cast(inputs[i]); - if (!AdapterValidationHelper::RequireNotNull("inputs[i]", in_rerank, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (!AdapterValidationHelper::RequireBoundedString( - "inputs[i].query_text", in_rerank->query_text, kMaxTextLen, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (!AdapterValidationHelper::RequireRange( - "inputs[i].candidate_count", in_rerank->candidate_count, 1, 8, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - raw_req_ids.push_back(in_rerank->request_id); - queries.emplace_back(static_cast(i), 0, in_rerank->query_text); - - for (int c = 0; c < in_rerank->candidate_count; ++c) { - std::string field_name = - "inputs[i].candidate_passages[" + std::to_string(c) + "]"; - if (!AdapterValidationHelper::RequireBoundedString( - field_name.c_str(), in_rerank->candidate_passages[c], - kMaxTextLen, i, AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - std::string passage = in_rerank->candidate_passages[c]; - candidates.emplace_back( - static_cast(i), static_cast(c), - RankedCandidate(passage, 0.0f, c + 1, static_cast(c))); - pairs.emplace_back( - static_cast(i), static_cast(c), - QueryCandidatePair(in_rerank->query_text, std::move(passage))); - } - } - - if (!AdapterValidationHelper::PublishContextValue( - *ctx, kRawRequestIds, std::move(raw_req_ids), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kRerankQueries, std::move(queries), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kRerankCandidates, std::move(candidates), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kRerankPairs, std::move(pairs), AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - return COMPANY_ALG_SUCCESS; - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const { - if (!ctx) { - return AdapterValidationHelper::ReturnBufferTooSmall( - out_status, "Null AlgContext passed to Pack", "ctx", AdapterName()); - } - - const auto* res = AdapterValidationHelper::ReadRequiredContextValue( - *ctx, kRankedResults, AdapterName(), out_status); - if (!res) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - - const auto* raw_req_ids = ctx->Read(kRawRequestIds); - - std::vector first; - if (!IndexResults(res, raw_req_ids, &first, "ranked_results", AdapterName(), - out_status, true)) - return COMPANY_ALG_ERR_INVALID_INPUT; - - // 按 req_id 分组 - std::unordered_map> req_map; - for (const auto& item : *res) { - req_map[item.req_id].push_back(item.data); - } - - for (auto& entry : req_map) { - auto& list = entry.second; - std::sort(list.begin(), list.end(), - [](const auto& a, const auto& b) { return a.rank < b.rank; }); - for (size_t k = 0; k < list.size(); ++k) { - if (list.size() > 8 || list[k].rank != static_cast(k + 1) || - list[k].original_sub_id >= 8) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Invalid ranked result", "ranked_results", - AdapterName()); - } - } - } - int count = static_cast(raw_req_ids->size()); - int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( - outputs, num_outputs, count, AdapterName(), out_status); - if (valid_ret != 0) return valid_ret; - - for (int i = 0; i < count; ++i) { - auto* out_ptr = static_cast(outputs[i]); - out_ptr->request_id = (*raw_req_ids)[i]; - - const auto& cand_list = req_map[static_cast(i)]; - int item_cnt = std::min(static_cast(cand_list.size()), 8); - out_ptr->count = item_cnt; - out_ptr->status_code = 0; - - for (int k = 0; k < item_cnt; ++k) { - out_ptr->scores[k] = cand_list[k].score; - out_ptr->sorted_indices[k] = - static_cast(cand_list[k].original_sub_id); - } - } - *num_outputs = count; - return COMPANY_ALG_SUCCESS; - } -}; - -REGISTER_BIZ_ADAPTER(CrossRerankAdapter); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/cross_rerank_bindings.cpp b/src/adapter/biz/cross_rerank_bindings.cpp new file mode 100644 index 00000000..1c4b9e22 --- /dev/null +++ b/src/adapter/biz/cross_rerank_bindings.cpp @@ -0,0 +1,78 @@ +#include "adapter/converter_authoring.h" +#include "adapter/io_binding.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +BizDefinition MakeCrossRerankBizDefinition() { + BizDefinition def; + def.biz_name = "dense_cross_rerank_scoring"; + def.demo_biz = "cross_rerank"; + def.display_name = "Cross-Encoder 精排"; + def.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("rerank_queries", "TextBatch", true, "1:1"), + BizPortDefinition("rerank_candidates", "RankedTextBatch", true, "N:1"), + BizPortDefinition("rerank_pairs", "QueryCandidatesBatch", true, "N:1")}; + def.egress = { + BizPortDefinition("ranked_results", "RankedTextBatch", true, "N:1")}; + return def; +} + +const bool g_reg_cross_rerank_biz = []() { + auto def = MakeCrossRerankBizDefinition(); + if (!PipelineCatalog::FindBiz(def.biz_name)) { + PipelineCatalog::RegisterBizDefinition(def); + } + return true; +}(); + +BizExposureDefinition MakeCrossRerankBizExposure() { + BizExposureDefinition def; + def.biz_name = "dense_cross_rerank_scoring"; + def.max_batch_size = 64; + def.required_transports = {"cabi", "operator"}; + return def; +} + +IoBindingDefinition MakeCrossRerankCAbiBinding() { + IoBindingDefinition def; + def.binding_id = "cross_rerank.cabi.v1"; + def.biz_name = "dense_cross_rerank_scoring"; + def.transport = "cabi"; + def.input_converter_id = "rerank.plain.cabi.v1"; + def.output_converter_id = "rerank_result.plain.cabi.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"rerank_queries", "rerank_queries"}, + {"rerank_candidates", "rerank_candidates"}, + {"rerank_pairs", "rerank_pairs"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"ranked_results", "ranked_results"}}; + def.max_batch_size = 64; + return def; +} + +IoBindingDefinition MakeCrossRerankOperatorBinding() { + IoBindingDefinition def; + def.binding_id = "cross_rerank.operator.v1"; + def.biz_name = "dense_cross_rerank_scoring"; + def.transport = "operator"; + def.input_converter_id = "rerank.plain.operator.v1"; + def.output_converter_id = "rerank_result.plain.operator.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"rerank_queries", "rerank_queries"}, + {"rerank_candidates", "rerank_candidates"}, + {"rerank_pairs", "rerank_pairs"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"ranked_results", "ranked_results"}}; + def.max_batch_size = 64; + return def; +} + +REGISTER_BIZ_EXPOSURE(MakeCrossRerankBizExposure()); +REGISTER_IO_BINDING(MakeCrossRerankCAbiBinding()); +REGISTER_IO_BINDING(MakeCrossRerankOperatorBinding()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/biz/cross_rerank_operator_bridge.cpp b/src/adapter/biz/cross_rerank_operator_bridge.cpp deleted file mode 100644 index f07a26a8..00000000 --- a/src/adapter/biz/cross_rerank_operator_bridge.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "adapter/biz_results.h" -#include "adapter/operator_biz_bridge.h" - -namespace llm_edgeflow { - -void RegisterCrossRerankBridge() { - auto desc = MakeSingleSlotBizBridge( - ALG_BIZ_TYPE_CROSS_RERANK, "CrossRerank", "CompanyRerankBatchInputStruct", - "builtin.cross_rerank", "rerank_in", "rerank_out"); - - desc.convert_sample_input = - [](const std::unordered_map& slots, - ProcessLocalShadowStorage& storage, const void** out_internal_dto, - std::string* err) -> int { - auto it = slots.find("rerank_in"); - if (it == slots.end() || !it->second) { - if (err) *err = "Missing required input slot rerank_in"; - return -3; - } - const auto* in = static_cast(it->second); - auto* dto = storage.AllocateShadowDto(); - dto->request_id = in->request_id; - dto->candidate_count = in->candidate_count; - dto->query_text = storage.StoreString(in->query_text); - - int count = in->candidate_count; - if (count < 0) { - count = 0; - } else if (count > COMPANY_OPERATOR_MAX_RERANK_CANDIDATES) { - count = COMPANY_OPERATOR_MAX_RERANK_CANDIDATES; - } - for (int i = 0; i < count; ++i) { - dto->candidate_passages[i] = - storage.StoreString(in->candidate_passages[i]); - } - for (int i = count; i < 8; ++i) { - dto->candidate_passages[i] = nullptr; - } - *out_internal_dto = dto; - return 0; - }; - - 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) { - if (err) *err = "Null internal DTO or external output struct pointer"; - return -4; - } - const auto* in_dto = static_cast(internal_dto); - auto* out = - static_cast(external_output_struct); - out->request_id = in_dto->request_id; - out->count = in_dto->count; - out->status_code = in_dto->status_code; - - for (int i = 0; - i < in_dto->count && i < COMPANY_OPERATOR_MAX_RERANK_CANDIDATES; ++i) { - out->scores[i] = in_dto->scores[i]; - out->sorted_indices[i] = in_dto->sorted_indices[i]; - } - return 0; - }; - - RegisterOperatorBizBridge(desc); -} - -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterCrossRerankBridge); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/doc_qa_adapter.cpp b/src/adapter/biz/doc_qa_adapter.cpp deleted file mode 100644 index bb211a1a..00000000 --- a/src/adapter/biz/doc_qa_adapter.cpp +++ /dev/null @@ -1,169 +0,0 @@ -#include -#include - -#include "adapter/adapter_batch.h" -#include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_registry.h" -#include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_results.h" -#include "adapter/result_packing_adapter.h" -#include "adapter/result_validation.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -inline static constexpr char kDocQaBizName[] = "smart_doc_qa_v1"; - -class DocQaAdapter - : public ResultPackingAdapter { - public: - CompanyAlgBizType BizType() const override { return ALG_BIZ_TYPE_DOC_QA; } - - const char* AdapterName() const override { return "DocQA"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - ALG_BIZ_TYPE_DOC_QA, - "DocQA", - COMPANY_ALG_ABI_VERSION, - "CompanyDocInputStruct", - "CompanyDocOutputStruct", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {{kDocQaBizName, - "doc_qa", - "智能文档问答", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(kRawDocs), - RequiredBizInput(kRawQueries)}, - {BizOutput(kLlmAnswers), BizOutput(kIntentMatches), - BizOutput(kDocChunkCounts)}}}}; - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { - int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); - if (valid_ret != 0 || !ctx) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Batch envelope validation failed or null AlgContext", - "inputs", AdapterName()); - } - - std::vector raw_req_ids; - TextBatch raw_docs; - TextBatch raw_queries; - - raw_req_ids.reserve(num_inputs); - raw_docs.reserve(num_inputs); - raw_queries.reserve(num_inputs); - - constexpr size_t kMaxQueryLen = 64 * 1024; // 64KB - constexpr size_t kMaxDocLen = 10 * 1024 * 1024; // 10MB 单文档上限 - - for (int i = 0; i < num_inputs; ++i) { - auto* in_doc = static_cast(inputs[i]); - if (!AdapterValidationHelper::RequireNotNull("inputs[i]", in_doc, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (!AdapterValidationHelper::RequireBoundedString( - "inputs[i].query_text", in_doc->query_text, kMaxQueryLen, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (in_doc->doc_text) { - if (!AdapterValidationHelper::RequireBoundedString( - "inputs[i].doc_text", in_doc->doc_text, kMaxDocLen, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - } - - raw_req_ids.push_back(in_doc->request_id); - raw_docs.emplace_back(static_cast(i), 0, - in_doc->doc_text ? in_doc->doc_text : ""); - raw_queries.emplace_back(static_cast(i), 0, - in_doc->query_text ? in_doc->query_text : ""); - } - - if (!AdapterValidationHelper::PublishContextValue( - *ctx, kRawRequestIds, std::move(raw_req_ids), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kRawDocs, std::move(raw_docs), AdapterName(), out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kRawQueries, std::move(raw_queries), AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - return COMPANY_ALG_SUCCESS; - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const { - const ResultBindingSpec primary_spec( - kLlmAnswers, "llm_answers", "answers", true, - "llm_answers not found in AlgContext", - COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - - const ResultBindingSpec> raw_req_ids_spec( - kRawRequestIds, "raw_request_ids", "raw_request_ids", true, - "raw_request_ids missing or count mismatch in AlgContext", - COMPANY_ALG_ERR_INVALID_INPUT); - - const ResultBindingSpec intent_spec( - kIntentMatches, "intent_matches", "intent_matches", true, - "intent_matches missing or count mismatch in AlgContext", - COMPANY_ALG_ERR_INVALID_INPUT); - - const ResultBindingSpec chunk_spec( - kDocChunkCounts, "doc_chunk_counts", "chunk_counts", true, - "doc_chunk_counts missing or count mismatch in AlgContext", - COMPANY_ALG_ERR_INVALID_INPUT); - - RequestResults results; - int ret = ReadMultiWayResults(ctx, outputs, num_outputs, AdapterName(), - out_status, &results, primary_spec, - raw_req_ids_spec, intent_spec, chunk_spec); - if (ret != COMPANY_ALG_SUCCESS) return ret; - - for (size_t i = 0; i < results.Size(); ++i) { - auto* out_ptr = static_cast(outputs[i]); - out_ptr->request_id = results.RequestId(i); - - const auto& match = results.Secondary<0>(i).data; - const std::string& intent = match.category; - float conf = match.score; - out_ptr->confidence = conf; - - out_ptr->chunk_count = results.Secondary<1>(i).data; - out_ptr->status_code = match.status_code; - - if (!CopyResultString(out_ptr->intent_name, intent.c_str(), - "outputs[i].intent_name", static_cast(i), - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - - if (!CopyResultString(out_ptr->answer_text, - results.Primary(i).data.c_str(), - "outputs[i].answer_text", static_cast(i), - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - } - *num_outputs = static_cast(results.Size()); - return COMPANY_ALG_SUCCESS; - } -}; - -REGISTER_BIZ_ADAPTER(DocQaAdapter); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/doc_qa_bindings.cpp b/src/adapter/biz/doc_qa_bindings.cpp new file mode 100644 index 00000000..423cd14c --- /dev/null +++ b/src/adapter/biz/doc_qa_bindings.cpp @@ -0,0 +1,81 @@ +#include "adapter/converter_authoring.h" +#include "adapter/io_binding.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +BizDefinition MakeDocQaBizDefinition() { + BizDefinition def; + def.biz_name = "smart_doc_qa_v1"; + def.demo_biz = "doc_qa"; + def.display_name = "智能文档问答"; + def.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("raw_docs", "TextBatch", true, "1:1"), + BizPortDefinition("raw_queries", "TextBatch", true, "1:1")}; + def.egress = { + BizPortDefinition("llm_answers", "TextBatch", true, "1:1"), + BizPortDefinition("intent_matches", "RuleMatchBatch", true, "1:1"), + BizPortDefinition("doc_chunk_counts", "Int32Batch", true, "1:1")}; + return def; +} + +const bool g_reg_doc_qa_biz = []() { + auto def = MakeDocQaBizDefinition(); + if (!PipelineCatalog::FindBiz(def.biz_name)) { + PipelineCatalog::RegisterBizDefinition(def); + } + return true; +}(); + +BizExposureDefinition MakeDocQaBizExposure() { + BizExposureDefinition def; + def.biz_name = "smart_doc_qa_v1"; + def.max_batch_size = 64; + def.required_transports = {"cabi", "operator"}; + return def; +} + +IoBindingDefinition MakeDocQaCAbiBinding() { + IoBindingDefinition def; + def.binding_id = "doc_qa.cabi.v1"; + def.biz_name = "smart_doc_qa_v1"; + def.transport = "cabi"; + def.input_converter_id = "doc_query.plain.cabi.v1"; + def.output_converter_id = "doc_answer.plain.cabi.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"raw_docs", "raw_docs"}, + {"raw_queries", "raw_queries"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}, + {"intent_matches", "intent_matches"}, + {"doc_chunk_counts", "doc_chunk_counts"}}; + def.max_batch_size = 64; + return def; +} + +IoBindingDefinition MakeDocQaOperatorBinding() { + IoBindingDefinition def; + def.binding_id = "doc_qa.operator.v1"; + def.biz_name = "smart_doc_qa_v1"; + def.transport = "operator"; + def.input_converter_id = "doc_query.plain.operator.v1"; + def.output_converter_id = "doc_answer.plain.operator.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"raw_docs", "raw_docs"}, + {"raw_queries", "raw_queries"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}, + {"intent_matches", "intent_matches"}, + {"doc_chunk_counts", "doc_chunk_counts"}}; + def.max_batch_size = 64; + return def; +} + +REGISTER_BIZ_EXPOSURE(MakeDocQaBizExposure()); +REGISTER_IO_BINDING(MakeDocQaCAbiBinding()); +REGISTER_IO_BINDING(MakeDocQaOperatorBinding()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/biz/doc_qa_operator_bridge.cpp b/src/adapter/biz/doc_qa_operator_bridge.cpp deleted file mode 100644 index 90acaa9d..00000000 --- a/src/adapter/biz/doc_qa_operator_bridge.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "adapter/biz_results.h" -#include "adapter/operator_biz_bridge.h" - -namespace llm_edgeflow { - -void RegisterDocQaBridge() { - auto desc = MakeSingleSlotBizBridge( - ALG_BIZ_TYPE_DOC_QA, "DocQA", "CompanyDocInputStruct", "builtin.doc_qa", - "doc_in", "doc_out"); - - desc.convert_sample_input = - [](const std::unordered_map& slots, - ProcessLocalShadowStorage& storage, const void** out_internal_dto, - std::string* err) -> int { - auto it = slots.find("doc_in"); - if (it == slots.end() || !it->second) { - if (err) *err = "Missing required input slot doc_in"; - return -3; - } - const auto* in = static_cast(it->second); - auto* dto = storage.AllocateShadowDto(); - dto->request_id = in->request_id; - dto->query_text = storage.StoreString(in->query_text); - dto->doc_text = storage.StoreOptionalString(in->doc_text); - *out_internal_dto = dto; - return 0; - }; - - 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) { - if (err) *err = "Null internal DTO or external output struct pointer"; - return -4; - } - const auto* in_dto = static_cast(internal_dto); - auto* out = static_cast(external_output_struct); - out->request_id = in_dto->request_id; - out->confidence = in_dto->confidence; - out->chunk_count = in_dto->chunk_count; - out->status_code = in_dto->status_code; - - int ret = CopyToOperatorString( - in_dto->intent_name.c_str(), out->intent_name, - spec.GetCapacity("intent_name"), "intent_name", err); - if (ret != 0) return ret; - - return CopyToOperatorString(in_dto->answer_text.c_str(), out->answer_text, - spec.GetCapacity("answer_text"), "answer_text", - err); - }; - - RegisterOperatorBizBridge(desc); -} - -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterDocQaBridge); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/entity_extract_adapter.cpp b/src/adapter/biz/entity_extract_adapter.cpp deleted file mode 100644 index 6fe883a8..00000000 --- a/src/adapter/biz/entity_extract_adapter.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include - -#include "adapter/adapter_batch.h" -#include "adapter/biz_adapter_registry.h" -#include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_results.h" -#include "adapter/result_packing_adapter.h" -#include "adapter/result_validation.h" -#include "adapter/text_carrier.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -inline static constexpr char kEntityExtractBizName[] = "entity_extract_v1"; - -class EntityExtractAdapter - : public ResultPackingAdapter { - public: - CompanyAlgBizType BizType() const override { - return ALG_BIZ_TYPE_ENTITY_EXTRACT; - } - - const char* AdapterName() const override { return "EntityExtract"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - ALG_BIZ_TYPE_ENTITY_EXTRACT, - "EntityExtract", - COMPANY_ALG_ABI_VERSION, - "CompanyEntityInputStruct", - "CompanyEntityOutputStruct", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {{kEntityExtractBizName, - "entity_extract", - "实体抽取", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(kInputSentences)}, - {BizOutput(kExtractedEntities)}}}}; - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { - return UnpackTextBatchSkeleton( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName(), ctx, - kInputSentences, - [](const OwnedTextRequest& req) { - return AdapterResult::Ok(req.text); - }, - out_status); - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const { - if (!ctx) { - return AdapterValidationHelper::ReturnBufferTooSmall( - out_status, "Null AlgContext passed to Pack", "ctx", AdapterName()); - } - - const auto* res = AdapterValidationHelper::ReadRequiredContextValue( - *ctx, kExtractedEntities, AdapterName(), out_status); - if (!res) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - - const auto* raw_req_ids = ctx->Read(kRawRequestIds); - - int count = static_cast(res->size()); - int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( - outputs, num_outputs, count, AdapterName(), out_status); - if (valid_ret != 0) return valid_ret; - - std::vector res_by_request; - if (!IndexResults(res, raw_req_ids, &res_by_request, "res", AdapterName(), - out_status)) - return COMPANY_ALG_ERR_INVALID_INPUT; - - for (int i = 0; i < count; ++i) { - auto* out_ptr = static_cast(outputs[i]); - // 保持行为:逐行先写 request_id,再检查结构化状态 - out_ptr->request_id = (*raw_req_ids)[i]; - if (!IsSuccessfulDocument(res_by_request[i]->data)) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Structured result failed or used fallback", "res", - AdapterName(), i); - } - out_ptr->status_code = 0; - - int write_ret = WriteTextCarrierOutput( - out_ptr, (*raw_req_ids)[i], 0, res_by_request[i]->data.json_payload, - i, AdapterName(), out_status); - if (write_ret != COMPANY_ALG_SUCCESS) { - return write_ret; - } - } - *num_outputs = count; - return COMPANY_ALG_SUCCESS; - } -}; - -REGISTER_BIZ_ADAPTER(EntityExtractAdapter); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/entity_extract_bindings.cpp b/src/adapter/biz/entity_extract_bindings.cpp new file mode 100644 index 00000000..542e0149 --- /dev/null +++ b/src/adapter/biz/entity_extract_bindings.cpp @@ -0,0 +1,72 @@ +#include "adapter/converter_authoring.h" +#include "adapter/io_binding.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +BizDefinition MakeEntityExtractBizDefinition() { + BizDefinition def; + def.biz_name = "entity_extract_v1"; + def.demo_biz = "entity_extract"; + def.display_name = "实体抽取"; + def.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.egress = {BizPortDefinition("extracted_entities", + "StructuredDocumentBatch", true, "1:1")}; + return def; +} + +const bool g_reg_entity_extract_biz = []() { + auto def = MakeEntityExtractBizDefinition(); + if (!PipelineCatalog::FindBiz(def.biz_name)) { + PipelineCatalog::RegisterBizDefinition(def); + } + return true; +}(); + +BizExposureDefinition MakeEntityExtractBizExposure() { + BizExposureDefinition def; + def.biz_name = "entity_extract_v1"; + def.max_batch_size = 64; + def.required_transports = {"cabi", "operator"}; + return def; +} + +IoBindingDefinition MakeEntityExtractCAbiBinding() { + IoBindingDefinition def; + def.binding_id = "entity_extract.cabi.v1"; + def.biz_name = "entity_extract_v1"; + def.transport = "cabi"; + def.input_converter_id = "text.plain.cabi.v1"; + def.output_converter_id = "document.structured.cabi.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"extracted_entities", "extracted_entities"}}; + def.max_batch_size = 64; + return def; +} + +IoBindingDefinition MakeEntityExtractOperatorBinding() { + IoBindingDefinition def; + def.binding_id = "entity_extract.operator.v1"; + def.biz_name = "entity_extract_v1"; + def.transport = "operator"; + def.input_converter_id = "text.plain.operator.v1"; + def.output_converter_id = "document.structured.operator.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"extracted_entities", "extracted_entities"}}; + def.max_batch_size = 64; + return def; +} + +REGISTER_BIZ_EXPOSURE(MakeEntityExtractBizExposure()); +REGISTER_IO_BINDING(MakeEntityExtractCAbiBinding()); +REGISTER_IO_BINDING(MakeEntityExtractOperatorBinding()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/biz/entity_extract_operator_bridge.cpp b/src/adapter/biz/entity_extract_operator_bridge.cpp deleted file mode 100644 index 5e0a154f..00000000 --- a/src/adapter/biz/entity_extract_operator_bridge.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "adapter/text_carrier.h" - -namespace llm_edgeflow { - -void RegisterEntityExtractBridge() { - RegisterOperatorBizBridge(MakeTextCarrierBridge( - ALG_BIZ_TYPE_ENTITY_EXTRACT, "EntityExtract", "builtin.entity_extract")); -} - -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterEntityExtractBridge); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/keyword_match_adapter.cpp b/src/adapter/biz/keyword_match_adapter.cpp deleted file mode 100644 index f2e454cc..00000000 --- a/src/adapter/biz/keyword_match_adapter.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include -#include - -#include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_registry.h" -#include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_results.h" -#include "adapter/result_packing_adapter.h" -#include "adapter/result_validation.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -inline static constexpr char kKeywordMatchBizName[] = "keyword_match_v1"; - -class KeywordMatchAdapter - : public ResultPackingAdapter { - public: - CompanyAlgBizType BizType() const override { - return ALG_BIZ_TYPE_KEYWORD_MATCH; - } - - const char* AdapterName() const override { return "KeywordMatch"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - ALG_BIZ_TYPE_KEYWORD_MATCH, - "KeywordMatch", - COMPANY_ALG_ABI_VERSION, - "CompanyKeywordInputStruct", - "CompanyKeywordOutputStruct", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {{kKeywordMatchBizName, - "keyword_match", - "关注词匹配", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(kInputSentences)}, - {BizOutput(kRuleMatches)}}}}; - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { - int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); - if (valid_ret != 0 || !ctx) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Batch envelope validation failed or null AlgContext", - "inputs", AdapterName()); - } - - std::vector req_ids; - TextBatch sentences; - req_ids.reserve(num_inputs); - sentences.reserve(num_inputs); - - constexpr size_t kMaxSentenceLen = 64 * 1024; // 64KB 单文本上限 - - for (int i = 0; i < num_inputs; ++i) { - auto* in = static_cast(inputs[i]); - if (!AdapterValidationHelper::RequireNotNull("inputs[i]", in, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (!AdapterValidationHelper::RequireBoundedString( - "inputs[i].sentence_text", in->sentence_text, kMaxSentenceLen, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - req_ids.push_back(in->request_id); - sentences.emplace_back(static_cast(i), 0, in->sentence_text); - } - - if (!AdapterValidationHelper::PublishContextValue( - *ctx, kRawRequestIds, std::move(req_ids), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kInputSentences, std::move(sentences), AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - return COMPANY_ALG_SUCCESS; - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const { - if (!ctx) { - return AdapterValidationHelper::ReturnBufferTooSmall( - out_status, "Null AlgContext passed to Pack", "ctx", AdapterName()); - } - - const auto* res = AdapterValidationHelper::ReadRequiredContextValue( - *ctx, kRuleMatches, AdapterName(), out_status); - if (!res) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - - const auto* raw_req_ids = ctx->Read(kRawRequestIds); - - int count = static_cast(res->size()); - int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( - outputs, num_outputs, count, AdapterName(), out_status); - if (valid_ret != 0) return valid_ret; - - std::vector res_by_request; - if (!IndexResults(res, raw_req_ids, &res_by_request, "res", AdapterName(), - out_status)) - return COMPANY_ALG_ERR_INVALID_INPUT; - - for (int i = 0; i < count; ++i) { - auto* out_ptr = static_cast(outputs[i]); - out_ptr->request_id = (*raw_req_ids)[i]; - out_ptr->is_hit = res_by_request[i]->data.is_hit; - out_ptr->status_code = res_by_request[i]->data.status_code; - - if (!CopyResultString(out_ptr->match_result_json, - res_by_request[i]->data.match_result_json.c_str(), - "outputs[i].match_result_json", i, AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - } - *num_outputs = count; - return COMPANY_ALG_SUCCESS; - } -}; - -REGISTER_BIZ_ADAPTER(KeywordMatchAdapter); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/keyword_match_bindings.cpp b/src/adapter/biz/keyword_match_bindings.cpp new file mode 100644 index 00000000..de7f4928 --- /dev/null +++ b/src/adapter/biz/keyword_match_bindings.cpp @@ -0,0 +1,72 @@ +#include "adapter/converter_authoring.h" +#include "adapter/io_binding.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +BizDefinition MakeKeywordMatchBizDefinition() { + BizDefinition def; + def.biz_name = "keyword_match_v1"; + def.demo_biz = "keyword_match"; + def.display_name = "关注词匹配"; + def.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.egress = { + BizPortDefinition("rule_matches", "RuleMatchBatch", true, "1:1")}; + return def; +} + +const bool g_reg_keyword_match_biz = []() { + auto def = MakeKeywordMatchBizDefinition(); + if (!PipelineCatalog::FindBiz(def.biz_name)) { + PipelineCatalog::RegisterBizDefinition(def); + } + return true; +}(); + +BizExposureDefinition MakeKeywordMatchBizExposure() { + BizExposureDefinition def; + def.biz_name = "keyword_match_v1"; + def.max_batch_size = 64; + def.required_transports = {"cabi", "operator"}; + return def; +} + +IoBindingDefinition MakeKeywordMatchCAbiBinding() { + IoBindingDefinition def; + def.binding_id = "keyword_match.cabi.v1"; + def.biz_name = "keyword_match_v1"; + def.transport = "cabi"; + def.input_converter_id = "keyword.plain.cabi.v1"; + def.output_converter_id = "keyword.result.cabi.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"rule_matches", "rule_matches"}}; + def.max_batch_size = 64; + return def; +} + +IoBindingDefinition MakeKeywordMatchOperatorBinding() { + IoBindingDefinition def; + def.binding_id = "keyword_match.operator.v1"; + def.biz_name = "keyword_match_v1"; + def.transport = "operator"; + def.input_converter_id = "keyword.plain.operator.v1"; + def.output_converter_id = "keyword.result.operator.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"rule_matches", "rule_matches"}}; + def.max_batch_size = 64; + return def; +} + +REGISTER_BIZ_EXPOSURE(MakeKeywordMatchBizExposure()); +REGISTER_IO_BINDING(MakeKeywordMatchCAbiBinding()); +REGISTER_IO_BINDING(MakeKeywordMatchOperatorBinding()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/biz/keyword_match_operator_bridge.cpp b/src/adapter/biz/keyword_match_operator_bridge.cpp deleted file mode 100644 index a6e8efd4..00000000 --- a/src/adapter/biz/keyword_match_operator_bridge.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "adapter/biz_results.h" -#include "adapter/operator_biz_bridge.h" - -namespace llm_edgeflow { - -void RegisterKeywordMatchBridge() { - auto desc = MakeSingleSlotBizBridge( - ALG_BIZ_TYPE_KEYWORD_MATCH, "KeywordMatch", "CompanyKeywordInputStruct", - "builtin.keyword_match", "keyword_in", "keyword_out"); - - desc.convert_sample_input = - [](const std::unordered_map& slots, - ProcessLocalShadowStorage& storage, const void** out_internal_dto, - std::string* err) -> int { - auto it = slots.find("keyword_in"); - if (it == slots.end() || !it->second) { - if (err) *err = "Missing required input slot keyword_in"; - return -3; - } - const auto* in = - static_cast(it->second); - auto* dto = storage.AllocateShadowDto(); - dto->request_id = in->request_id; - dto->sentence_text = storage.StoreString(in->sentence_text); - *out_internal_dto = dto; - return 0; - }; - - 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) { - if (err) *err = "Null internal DTO or external output struct pointer"; - return -4; - } - const auto* in_dto = static_cast(internal_dto); - auto* out = - static_cast(external_output_struct); - out->request_id = in_dto->request_id; - out->is_hit = in_dto->is_hit; - out->status_code = in_dto->status_code; - - return CopyToOperatorString( - in_dto->match_result_json.c_str(), out->match_result_json, - spec.GetCapacity("match_result_json"), "match_result_json", err); - }; - - RegisterOperatorBizBridge(desc); -} - -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterKeywordMatchBridge); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/ocr_doc_qa_adapter.cpp b/src/adapter/biz/ocr_doc_qa_adapter.cpp deleted file mode 100644 index 4c509a55..00000000 --- a/src/adapter/biz/ocr_doc_qa_adapter.cpp +++ /dev/null @@ -1,162 +0,0 @@ -#include -#include - -#include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_registry.h" -#include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_results.h" -#include "adapter/result_packing_adapter.h" -#include "adapter/result_validation.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -inline static constexpr char kOcrDocQaBizName[] = "multimodal_ocr_invoice_qa"; - -class OcrDocQaAdapter - : public ResultPackingAdapter { - public: - CompanyAlgBizType BizType() const override { return ALG_BIZ_TYPE_OCR_DOC_QA; } - - const char* AdapterName() const override { return "OcrDocQA"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - ALG_BIZ_TYPE_OCR_DOC_QA, - "OcrDocQA", - COMPANY_ALG_ABI_VERSION, - "CompanyOcrDocInputStruct", - "CompanyOcrDocOutputStruct", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {{kOcrDocQaBizName, - "ocr_doc_qa", - "OCR 票据问答", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(kImagePaths), - RequiredBizInput(kUserQueries)}, - {BizOutput(kExtractedInvoiceJson), BizOutput(kOcrDocs)}}}}; - return desc; - } - - int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { - int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); - if (valid_ret != 0 || !ctx) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Batch envelope validation failed or null AlgContext", - "inputs", AdapterName()); - } - - std::vector raw_req_ids; - ImageRefBatch raw_images; - TextBatch raw_queries; - - raw_req_ids.reserve(num_inputs); - raw_images.reserve(num_inputs); - raw_queries.reserve(num_inputs); - - constexpr size_t kMaxPathLen = 4096; - constexpr size_t kMaxQueryLen = 64 * 1024; - - for (int i = 0; i < num_inputs; ++i) { - auto* in_ocr = static_cast(inputs[i]); - if (!AdapterValidationHelper::RequireNotNull("inputs[i]", in_ocr, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - if (!AdapterValidationHelper::RequireBoundedString( - "inputs[i].image_path", in_ocr->image_path, kMaxPathLen, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - if (!AdapterValidationHelper::RequireBoundedString( - "inputs[i].query_prompt", in_ocr->query_prompt, kMaxQueryLen, i, - AdapterName(), out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - - raw_req_ids.push_back(in_ocr->request_id); - raw_images.emplace_back(static_cast(i), 0, in_ocr->image_path); - raw_queries.emplace_back( - static_cast(i), 0, - in_ocr->query_prompt ? in_ocr->query_prompt : ""); - } - - if (!AdapterValidationHelper::PublishContextValue( - *ctx, kRawRequestIds, std::move(raw_req_ids), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kImagePaths, std::move(raw_images), AdapterName(), - out_status) || - !AdapterValidationHelper::PublishContextValue( - *ctx, kUserQueries, std::move(raw_queries), AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_INVALID_INPUT; - } - return COMPANY_ALG_SUCCESS; - } - - template - int PackTyped(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const { - if (!ctx) { - return AdapterValidationHelper::ReturnBufferTooSmall( - out_status, "Null AlgContext passed to Pack", "ctx", AdapterName()); - } - - const auto* invoice_jsons = - AdapterValidationHelper::ReadRequiredContextValue( - *ctx, kExtractedInvoiceJson, AdapterName(), out_status); - if (!invoice_jsons) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - - const auto* ocr_docs = ctx->Read(kOcrDocs); - const auto* raw_req_ids = ctx->Read(kRawRequestIds); - - int count = static_cast(invoice_jsons->size()); - int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( - outputs, num_outputs, count, AdapterName(), out_status); - if (valid_ret != 0) return valid_ret; - - std::vector - invoice_jsons_by_request; - if (!IndexResults(invoice_jsons, raw_req_ids, &invoice_jsons_by_request, - "invoice_jsons", AdapterName(), out_status)) - return COMPANY_ALG_ERR_INVALID_INPUT; - std::vector ocr_docs_by_request; - if (!IndexResults(ocr_docs, raw_req_ids, &ocr_docs_by_request, "ocr_docs", - AdapterName(), out_status)) - return COMPANY_ALG_ERR_INVALID_INPUT; - - for (int i = 0; i < count; ++i) { - auto* out_ptr = static_cast(outputs[i]); - out_ptr->request_id = (*raw_req_ids)[i]; - out_ptr->detected_box_count = - static_cast(ocr_docs_by_request[i]->data.boxes.size()); - if (!IsSuccessfulDocument(invoice_jsons_by_request[i]->data)) { - return AdapterValidationHelper::ReturnInvalidInput( - out_status, "Structured result failed or used fallback", - "invoice_jsons", AdapterName(), i); - } - out_ptr->status_code = 0; - - if (!CopyResultString( - out_ptr->extracted_invoice_json, - invoice_jsons_by_request[i]->data.json_payload.c_str(), - "outputs[i].extracted_invoice_json", i, AdapterName(), - out_status)) { - return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; - } - } - *num_outputs = count; - return COMPANY_ALG_SUCCESS; - } -}; - -REGISTER_BIZ_ADAPTER(OcrDocQaAdapter); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/ocr_doc_qa_bindings.cpp b/src/adapter/biz/ocr_doc_qa_bindings.cpp new file mode 100644 index 00000000..71a8574b --- /dev/null +++ b/src/adapter/biz/ocr_doc_qa_bindings.cpp @@ -0,0 +1,78 @@ +#include "adapter/converter_authoring.h" +#include "adapter/io_binding.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +BizDefinition MakeOcrDocQaBizDefinition() { + BizDefinition def; + def.biz_name = "multimodal_ocr_invoice_qa"; + def.demo_biz = "ocr_doc_qa"; + def.display_name = "OCR 票据问答"; + def.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("image_paths", "ImageRefBatch", true, "1:1"), + BizPortDefinition("user_queries", "TextBatch", true, "1:1")}; + def.egress = {BizPortDefinition("extracted_invoice_json", + "StructuredDocumentBatch", true, "1:1"), + BizPortDefinition("ocr_docs", "OcrDocumentBatch", true, "1:1")}; + return def; +} + +const bool g_reg_ocr_doc_qa_biz = []() { + auto def = MakeOcrDocQaBizDefinition(); + if (!PipelineCatalog::FindBiz(def.biz_name)) { + PipelineCatalog::RegisterBizDefinition(def); + } + return true; +}(); + +BizExposureDefinition MakeOcrDocQaBizExposure() { + BizExposureDefinition def; + def.biz_name = "multimodal_ocr_invoice_qa"; + def.max_batch_size = 64; + def.required_transports = {"cabi", "operator"}; + return def; +} + +IoBindingDefinition MakeOcrDocQaCAbiBinding() { + IoBindingDefinition def; + def.binding_id = "ocr_doc_qa.cabi.v1"; + def.biz_name = "multimodal_ocr_invoice_qa"; + def.transport = "cabi"; + def.input_converter_id = "image_query.plain.cabi.v1"; + def.output_converter_id = "invoice_result.plain.cabi.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"image_paths", "image_paths"}, + {"user_queries", "user_queries"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"extracted_invoice_json", "extracted_invoice_json"}, + {"ocr_docs", "ocr_docs"}}; + def.max_batch_size = 64; + return def; +} + +IoBindingDefinition MakeOcrDocQaOperatorBinding() { + IoBindingDefinition def; + def.binding_id = "ocr_doc_qa.operator.v1"; + def.biz_name = "multimodal_ocr_invoice_qa"; + def.transport = "operator"; + def.input_converter_id = "image_query.plain.operator.v1"; + def.output_converter_id = "invoice_result.plain.operator.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"image_paths", "image_paths"}, + {"user_queries", "user_queries"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"extracted_invoice_json", "extracted_invoice_json"}, + {"ocr_docs", "ocr_docs"}}; + def.max_batch_size = 64; + return def; +} + +REGISTER_BIZ_EXPOSURE(MakeOcrDocQaBizExposure()); +REGISTER_IO_BINDING(MakeOcrDocQaCAbiBinding()); +REGISTER_IO_BINDING(MakeOcrDocQaOperatorBinding()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/biz/ocr_doc_qa_operator_bridge.cpp b/src/adapter/biz/ocr_doc_qa_operator_bridge.cpp deleted file mode 100644 index 65d8fafe..00000000 --- a/src/adapter/biz/ocr_doc_qa_operator_bridge.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "adapter/biz_results.h" -#include "adapter/operator_biz_bridge.h" - -namespace llm_edgeflow { - -void RegisterOcrDocQaBridge() { - OperatorBizBridgeDescriptor desc; - desc.biz_type = ALG_BIZ_TYPE_OCR_DOC_QA; - desc.adapter_name = "OcrDocQA"; - desc.internal_input_type_name = "CompanyOcrDocInputStruct"; - desc.internal_output_type_name = "OcrDocResult"; - desc.registration_identity = "builtin.ocr_doc_qa"; - - // Slot 1: frame -> CompanyFrame - OperatorBizSlot frame_slot; - frame_slot.logical_name = "frame"; - frame_slot.type_suffix = "frame"; - frame_slot.direction = IoDirection::kInput; - frame_slot.required = true; - desc.input_slots.push_back(frame_slot); - - // Slot 2: string -> CompanyString - OperatorBizSlot string_slot; - string_slot.logical_name = "string"; - string_slot.type_suffix = "string"; - string_slot.direction = IoDirection::kInput; - string_slot.required = true; - desc.input_slots.push_back(string_slot); - - // Output slot: od_out -> CompanyOdOutput - OperatorBizSlot out_slot; - out_slot.logical_name = "od_out"; - 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 = - [](const std::unordered_map& slots, - ProcessLocalShadowStorage& storage, const void** out_internal_dto, - std::string* err) -> int { - auto it_frame = slots.find("frame"); - if (it_frame == slots.end() || !it_frame->second) { - if (err) *err = "Missing required input slot frame"; - return -3; - } - auto it_str = slots.find("string"); - if (it_str == slots.end() || !it_str->second) { - if (err) *err = "Missing required input slot string"; - return -3; - } - - const auto* frame = static_cast(it_frame->second); - const auto* query = static_cast(it_str->second); - - auto* dto = storage.AllocateShadowDto(); - dto->request_id = frame->request_id; - dto->image_path = storage.StoreString(frame->image_uri); - dto->query_prompt = storage.StoreString(query); - *out_internal_dto = dto; - return 0; - }; - - 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) { - if (err) *err = "Null internal DTO or external output struct pointer"; - return -4; - } - const auto* in_dto = static_cast(internal_dto); - auto* out = static_cast(external_output_struct); - out->request_id = in_dto->request_id; - out->detected_box_count = in_dto->detected_box_count; - out->status_code = in_dto->status_code; - - return CopyToOperatorString( - in_dto->extracted_invoice_json.c_str(), out->result_json, - spec.GetCapacity("result_json"), "result_json", err); - }; - - desc.create_shadow_output_dto = [](ProcessLocalShadowStorage& s) -> void* { - return s.AllocateShadowDto(); - }; - - RegisterOperatorBizBridge(desc); -} - -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterOcrDocQaBridge); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/translate_adapter.cpp b/src/adapter/biz/translate_adapter.cpp deleted file mode 100644 index 70c69b4b..00000000 --- a/src/adapter/biz/translate_adapter.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include - -#include "adapter/adapter_authoring.h" -#include "adapter/biz_adapter_registry.h" -#include "adapter/biz_blackboard_keys.h" - -namespace llm_edgeflow { - -namespace { - -AdapterResult DecodeTranslateRequest( - const OwnedTextRequest& request) { - const auto req_json = nlohmann::json::parse(request.text, nullptr, false); - if (!req_json.is_object() || !req_json.contains("query") || - !req_json["query"].is_string()) { - return AdapterResult::InvalidInput( - "Expected a JSON object with string field query", "json", -1, - "Translate"); - } - // Pass only the original text to generation. std::string preserves - // decoded newlines, quotes and embedded NUL. Ignore all other fields. - return AdapterResult::Ok(req_json["query"].get()); -} - -AdapterResult EncodeTranslateResponse(const std::string& answer) { - // The model returns plain translated text. C++ alone owns the external - // response schema and escaping; do not parse, trim or repair its text. - // Exceptions from json dump (e.g. invalid UTF-8) propagate to - // SharedAlgorithmRuntime to restore COMPANY_ALG_ERR_EXCEPTION (-99) contract - // per RFC-0053 §3.2. - const nlohmann::json response = {{"translated", answer}}; - return AdapterResult::Ok(response.dump()); -} - -struct TranslateSpecProvider { - static const OneToOneTextAdapterSpec& GetSpec() { - static const OneToOneTextAdapterSpec spec = [] { - OneToOneTextAdapterSpec s; - s.biz_type = ALG_BIZ_TYPE_TRANSLATE; - s.adapter_name = "Translate"; - s.sdk_abi_version = COMPANY_ALG_ABI_VERSION; - s.c_input_type_name = "CompanyEntityInputStruct"; - s.c_output_type_name = "CompanyEntityOutputStruct"; - s.max_batch_size = 64; - s.ownership_policy = OwnershipPolicy::kCopyIn; - s.thread_model = ThreadModel::kStatelessThreadSafe; - s.cardinality = OutputCardinality::kOneToOne; - s.biz_name = "translate_v1"; - s.demo_biz = "translate"; - s.display_name = "JSON 字符串翻译"; - s.input_key = kInputSentences; - s.output_key = kLlmAnswers; - s.decode_fn = &DecodeTranslateRequest; - s.encode_fn = &EncodeTranslateResponse; - s.carrier_adapter_name = "EntityExtract"; - s.null_ctx_field = "json"; - return s; - }(); - return spec; - } -}; - -} // namespace - -using TranslateAdapter = OneToOneTextAdapter; - -REGISTER_BIZ_ADAPTER(TranslateAdapter); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz/translate_bindings.cpp b/src/adapter/biz/translate_bindings.cpp new file mode 100644 index 00000000..2ffcf062 --- /dev/null +++ b/src/adapter/biz/translate_bindings.cpp @@ -0,0 +1,71 @@ +#include "adapter/converter_authoring.h" +#include "adapter/io_binding.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +BizDefinition MakeTranslateBizDefinition() { + BizDefinition def; + def.biz_name = "translate_v1"; + def.demo_biz = "translate"; + def.display_name = "JSON 字符串翻译"; + def.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.egress = {BizPortDefinition("llm_answers", "TextBatch", true, "1:1")}; + return def; +} + +const bool g_reg_translate_biz = []() { + auto def = MakeTranslateBizDefinition(); + if (!PipelineCatalog::FindBiz(def.biz_name)) { + PipelineCatalog::RegisterBizDefinition(def); + } + return true; +}(); + +BizExposureDefinition MakeTranslateBizExposure() { + BizExposureDefinition def; + def.biz_name = "translate_v1"; + def.max_batch_size = 64; + def.required_transports = {"cabi", "operator"}; + return def; +} + +IoBindingDefinition MakeTranslateCAbiBinding() { + IoBindingDefinition def; + def.binding_id = "translate.cabi.v1"; + def.biz_name = "translate_v1"; + def.transport = "cabi"; + def.input_converter_id = "translate.json.cabi.v1"; + def.output_converter_id = "translate.json.cabi.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}}; + def.max_batch_size = 64; + return def; +} + +IoBindingDefinition MakeTranslateOperatorBinding() { + IoBindingDefinition def; + def.binding_id = "translate.operator.v1"; + def.biz_name = "translate_v1"; + def.transport = "operator"; + def.input_converter_id = "translate.json.operator.v1"; + def.output_converter_id = "translate.json.operator.v1"; + def.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}; + def.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}}; + def.max_batch_size = 64; + return def; +} + +REGISTER_BIZ_EXPOSURE(MakeTranslateBizExposure()); +REGISTER_IO_BINDING(MakeTranslateCAbiBinding()); +REGISTER_IO_BINDING(MakeTranslateOperatorBinding()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/biz/translate_operator_bridge.cpp b/src/adapter/biz/translate_operator_bridge.cpp deleted file mode 100644 index 608506a7..00000000 --- a/src/adapter/biz/translate_operator_bridge.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "adapter/text_carrier.h" - -namespace llm_edgeflow { - -void RegisterTranslateBridge() { - RegisterOperatorBizBridge(MakeTextCarrierBridge( - ALG_BIZ_TYPE_TRANSLATE, "Translate", "builtin.translate")); -} - -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterTranslateBridge); - -} // namespace llm_edgeflow diff --git a/src/adapter/biz_adapter_registry.cpp b/src/adapter/biz_adapter_registry.cpp deleted file mode 100644 index 2cfb3944..00000000 --- a/src/adapter/biz_adapter_registry.cpp +++ /dev/null @@ -1,135 +0,0 @@ -#include "adapter/biz_adapter_registry.h" - -#include "core/pipeline_catalog.h" - -namespace llm_edgeflow { - -bool BizAdapterRegistry::RegisterAdapter(std::shared_ptr adapter) { - if (!adapter) { - std::lock_guard lock(mutex_); - has_conflict_ = true; - registration_errors_.push_back("Null adapter pointer passed to registry"); - return false; - } - - std::lock_guard lock(mutex_); - CompanyAlgBizType biz_type = adapter->BizType(); - - if (biz_type == ALG_BIZ_TYPE_UNKNOWN) { - has_conflict_ = true; - std::string err = "Cannot register adapter '" + - std::string(adapter->AdapterName()) + - "' with ALG_BIZ_TYPE_UNKNOWN"; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - - // 冲突与契约检查 0: 必须满足框架当前可执行的 Descriptor 策略 (RECHECK-003, - // ADP-008) - const auto& desc = adapter->GetDescriptor(); - if (desc.ownership_policy != OwnershipPolicy::kCopyIn) { - has_conflict_ = true; - std::string err = "Unsupported OwnershipPolicy in adapter '" + - std::string(adapter->AdapterName()) + - "': only kCopyIn is currently supported"; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - if (desc.thread_model != ThreadModel::kStatelessThreadSafe) { - has_conflict_ = true; - std::string err = "Unsupported ThreadModel in adapter '" + - std::string(adapter->AdapterName()) + - "': only kStatelessThreadSafe is currently supported"; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - if (desc.cardinality != OutputCardinality::kOneToOne) { - has_conflict_ = true; - std::string err = "Unsupported OutputCardinality in adapter '" + - std::string(adapter->AdapterName()) + - "': only kOneToOne is currently supported"; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - if (desc.biz_type != biz_type || - desc.adapter_name != adapter->AdapterName()) { - has_conflict_ = true; - std::string err = - "Descriptor inconsistency for adapter '" + - std::string(adapter->AdapterName()) + - "': BizType/AdapterName mismatch between methods and descriptor"; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - - // 冲突检查 1: 业务 ID 重复冲突 - auto it = adapters_.find(biz_type); - if (it != adapters_.end()) { - has_conflict_ = true; - std::string err = "Conflict: BizType [" + std::to_string(biz_type) + - "] already registered by '" + it->second->AdapterName() + - "'. Cannot register '" + adapter->AdapterName() + "'."; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - - // 冲突检查 2: 业务名称重复冲突 - for (const auto& kv : adapters_) { - if (kv.second->AdapterName() == std::string(adapter->AdapterName())) { - has_conflict_ = true; - std::string err = "Conflict: AdapterName '" + - std::string(adapter->AdapterName()) + - "' already registered under BizType [" + - std::to_string(kv.first) + "]."; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - } - - // 冲突检查 3: 业务 Pipeline 契约必须声明且在 Catalog 中无冲突 - if (desc.biz_definitions.empty()) { - has_conflict_ = true; - std::string err = - "Adapter '" + std::string(adapter->AdapterName()) + - "' must declare at least one BizDefinition in biz_definitions"; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - - for (const auto& biz_definition : desc.biz_definitions) { - if (biz_definition.biz_name.empty()) { - has_conflict_ = true; - std::string err = "Adapter '" + std::string(adapter->AdapterName()) + - "' has a biz contract with empty biz_name"; - registration_errors_.push_back(err); - return false; - } - } - if (!PipelineCatalog::RegisterBizDefinitions(desc.biz_definitions)) { - has_conflict_ = true; - std::string err = "Conflict: one or more pipeline biz names in adapter '" + - std::string(adapter->AdapterName()) + - "' are invalid, duplicated, or already registered"; - registration_errors_.push_back(err); - ALG_LOG_ERROR("[BizAdapterRegistry] %s\n", err.c_str()); - return false; - } - - adapters_[biz_type] = adapter; - ALG_LOG_VERBOSE( - "[BizAdapterRegistry] Registered adapter for BizType [%d]: %s (SDK ABI: " - "%s)\n", - static_cast(biz_type), adapter->AdapterName(), - adapter->GetDescriptor().sdk_abi_version.c_str()); - return true; -} - -} // namespace llm_edgeflow diff --git a/src/adapter/c_api_adapter.cpp b/src/adapter/c_api_adapter.cpp index f3d05cd1..27c13119 100644 --- a/src/adapter/c_api_adapter.cpp +++ b/src/adapter/c_api_adapter.cpp @@ -58,8 +58,7 @@ int Alg_Create(void** hndl, std::string err_msg; int ret = llm_edgeflow::SharedAlgorithmRuntime::CreateFromConfigFile( - cfg_path, param_create->device_id, model_root, param_create->biz_type, - &runtime, &err_msg); + cfg_path, param_create->device_id, model_root, &runtime, &err_msg); if (ret != 0) { ALG_LOG_ERROR("[Company C Adapter] Alg_Create failed: %s\n", err_msg.c_str()); diff --git a/src/adapter/deployment_io_config.cpp b/src/adapter/deployment_io_config.cpp new file mode 100644 index 00000000..b6a949bb --- /dev/null +++ b/src/adapter/deployment_io_config.cpp @@ -0,0 +1,201 @@ +#include "adapter/deployment_io_config.h" + +#include +#include + +#include "contracts/path_utils.h" + +namespace llm_edgeflow { + +namespace fs = std::filesystem; + +bool DeploymentIoConfig::ReadFromFile(const std::string& config_path, + const std::string& transport, + DeploymentIoConfig* out_config, + std::string* out_error) { + if (config_path.empty()) { + if (out_error) *out_error = "Empty config_path"; + return false; + } + + std::ifstream ifs(config_path); + if (!ifs.is_open()) { + if (out_error) *out_error = "Failed to open config file: " + config_path; + return false; + } + + nlohmann::json root; + try { + ifs >> root; + } catch (const std::exception& e) { + if (out_error) { + *out_error = "JSON parse exception in " + config_path + ": " + e.what(); + } + return false; + } + + fs::path cfg_dir = fs::path(config_path).parent_path(); + if (cfg_dir.empty()) { + cfg_dir = "."; + } + cfg_dir = fs::absolute(cfg_dir); + + return Parse(root, cfg_dir.string(), transport, out_config, out_error); +} + +bool DeploymentIoConfig::Parse(const nlohmann::json& root, + const std::string& config_dir, + const std::string& transport, + DeploymentIoConfig* out_config, + std::string* out_error) { + if (!out_config) { + if (out_error) *out_error = "Null out_config pointer"; + return false; + } + + if (!root.is_object()) { + if (out_error) *out_error = "Root configuration must be a JSON object"; + return false; + } + + // 1. 顶层字段白名单检查: 仅允许 schema_version 和 data + for (auto it = root.begin(); it != root.end(); ++it) { + if (it.key() != "schema_version" && it.key() != "data") { + if (out_error) { + *out_error = "Unknown field at /: " + it.key() + + " (only schema_version and data allowed)"; + } + return false; + } + } + + if (!root.contains("schema_version")) { + if (out_error) *out_error = "Missing schema_version in config"; + return false; + } + if (!root["schema_version"].is_number_integer()) { + if (out_error) *out_error = "schema_version must be an integer"; + return false; + } + int ver = root["schema_version"].get(); + if (ver != 1) { + if (out_error) { + *out_error = + "Unsupported schema_version " + std::to_string(ver) + ", expected 1"; + } + return false; + } + + if (!root.contains("data") || !root["data"].is_object()) { + if (out_error) *out_error = "Missing or invalid 'data' object in config"; + return false; + } + + const auto& data = root["data"]; + + // 2. data 内部字段检查 + for (auto it = data.begin(); it != data.end(); ++it) { + if (it.key() != "pipe_path" && it.key() != "io_binding" && + it.key() != "model_paths" && it.key() != "outputs") { + if (out_error) { + *out_error = "Unknown field in conf data: '" + it.key() + "'"; + } + return false; + } + } + + if (!data.contains("pipe_path") || !data["pipe_path"].is_string() || + data["pipe_path"].get().empty()) { + if (out_error) *out_error = "Missing or empty 'data.pipe_path'"; + return false; + } + + if (!data.contains("io_binding") || !data["io_binding"].is_string() || + data["io_binding"].get().empty()) { + if (out_error) *out_error = "Missing or empty 'data.io_binding'"; + return false; + } + + out_config->schema_version = ver; + out_config->pipe_path = data["pipe_path"].get(); + out_config->io_binding = data["io_binding"].get(); + out_config->raw_json = root; + + // 3. outputs 约束 + if (data.contains("outputs")) { + if (transport == "cabi") { + if (out_error) { + *out_error = "C ABI deployment config does not accept 'data.outputs'"; + } + return false; + } + if (!data["outputs"].is_object()) { + if (out_error) *out_error = "data.outputs must be an object"; + return false; + } + out_config->outputs = data["outputs"]; + } else { + out_config->outputs = nlohmann::json::object(); + } + + // 4. model_paths + out_config->model_paths.clear(); + if (data.contains("model_paths")) { + if (!data["model_paths"].is_object()) { + if (out_error) *out_error = "data.model_paths must be an object"; + return false; + } + for (auto it = data["model_paths"].begin(); it != data["model_paths"].end(); + ++it) { + if (!it.value().is_string()) { + if (out_error) { + *out_error = + "data.model_paths[" + it.key() + "] value must be a string"; + } + return false; + } + out_config->model_paths[it.key()] = it.value().get(); + } + } + + // 5. 解析 pipe_path 相对 + // config_dir,严格限制在配置根目录下,拒绝任何逃逸与搜索回退 + fs::path base_dir = fs::absolute(fs::path(config_dir)); + fs::path raw_pipe = fs::path(out_config->pipe_path); + fs::path full_pipe = + raw_pipe.is_absolute() ? raw_pipe : (base_dir / raw_pipe); + + std::error_code ec; + fs::path canonical_base = fs::weakly_canonical(base_dir, ec); + fs::path canonical_pipe = fs::weakly_canonical(full_pipe, ec); + + if (!IsPathWithinRoot(canonical_base, canonical_pipe)) { + if (out_error) { + *out_error = + "data.pipe_path escapes config directory: " + out_config->pipe_path; + } + return false; + } + + if (!fs::exists(canonical_pipe)) { + if (out_error) { + *out_error = "Pipeline file does not exist: " + canonical_pipe.string(); + } + return false; + } + + // 校验符号链接目标,防止符号链接逃出配置目录 + fs::path real_pipe = fs::canonical(canonical_pipe, ec); + if (ec || !IsPathWithinRoot(canonical_base, real_pipe)) { + if (out_error) { + *out_error = + "data.pipe_path escapes config directory: " + out_config->pipe_path; + } + return false; + } + + out_config->resolved_pipe_path = real_pipe.string(); + return true; +} + +} // namespace llm_edgeflow diff --git a/src/adapter/deployment_io_config.h b/src/adapter/deployment_io_config.h new file mode 100644 index 00000000..59e8c01a --- /dev/null +++ b/src/adapter/deployment_io_config.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +namespace llm_edgeflow { + +/** + * @brief Schema 1 统一接入配置文件解析结构 + */ +struct DeploymentIoConfig { + int schema_version = 1; + std::string pipe_path; + std::string io_binding; + std::unordered_map model_paths; + nlohmann::json outputs = nlohmann::json::object(); + std::string resolved_pipe_path; + nlohmann::json raw_json; + + static bool Parse(const nlohmann::json& root, const std::string& config_dir, + const std::string& transport, + DeploymentIoConfig* out_config, std::string* out_error); + + static bool ReadFromFile(const std::string& config_path, + const std::string& transport, + DeploymentIoConfig* out_config, + std::string* out_error); +}; + +} // namespace llm_edgeflow diff --git a/src/adapter/input/README.md b/src/adapter/input/README.md new file mode 100644 index 00000000..87846614 --- /dev/null +++ b/src/adapter/input/README.md @@ -0,0 +1,9 @@ +# Input Converters (输入转换器) + +本目录包含所有外部输入协议到内部 Pipeline Blackboard 端口的独立输入转换器。 + +## 规范与契约 +- 每个输入转换器通过 `REGISTER_INPUT_CONVERTER` 注册 `InputConverterDefinition`。 +- 负责外部载体批次envelope校验、类型转换、深拷贝(copy-in)以及按端口映射发布到 `AlgContext`。 +- 保证无请求间共享状态与局部临时引用的生命周期隔离。 +- 输入校验失败时立即终止,不分配后续输出池资源,不触发 Pipeline 执行。 diff --git a/src/adapter/input/audio_input.cpp b/src/adapter/input/audio_input.cpp new file mode 100644 index 00000000..ca4cf83f --- /dev/null +++ b/src/adapter/input/audio_input.cpp @@ -0,0 +1,223 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/biz_input_constraints.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +int DecodeCAbiAudioInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, AlgContext* context, + AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + int valid_ret = AdapterValidationHelper::ValidateBatchInputs( + source.items, static_cast(source.count), 64, + options.converter_id.c_str()); + if (valid_ret != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch envelope validation failed", "inputs", + options.converter_id.c_str()); + } + + std::vector raw_req_ids; + AudioPcmBatch raw_audios; + + raw_req_ids.reserve(source.count); + raw_audios.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in_audio = source.GetCAbi(i); + if (!AdapterValidationHelper::RequireNotNull( + "inputs[i]", in_audio, static_cast(i), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (!AdapterValidationHelper::RequireRange( + "inputs[i].sample_rate", in_audio->sample_rate, + biz_input::kMinSampleRate, biz_input::kMaxSampleRate, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + if (!AdapterValidationHelper::RequireRange( + "inputs[i].pcm_length", in_audio->pcm_length, 0, + biz_input::kMaxAudioPcmSamples, static_cast(i), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (in_audio->pcm_length > 0) { + if (!AdapterValidationHelper::RequireNotNull( + "inputs[i].pcm_buffer", in_audio->pcm_buffer, static_cast(i), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + if (!AdapterValidationHelper::CheckedMultiply( + "inputs[i].pcm_buffer", in_audio->pcm_length, sizeof(float), + biz_input::kMaxAudioPcmBytes, static_cast(i), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + } + + raw_req_ids.push_back(in_audio->request_id); + + AudioPcmPayload pcm_dto; + if (in_audio->pcm_buffer && in_audio->pcm_length > 0) { + pcm_dto.pcm_data.assign(in_audio->pcm_buffer, + in_audio->pcm_buffer + in_audio->pcm_length); + } + pcm_dto.sample_rate = in_audio->sample_rate; + raw_audios.emplace_back(static_cast(i), 0, std::move(pcm_dto)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(raw_req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("audio_inputs"), + std::move(raw_audios), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +int DecodeOperatorAudioInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + if (source.count == 0 || source.count > 64) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch size out of range [1, 64]", "slots", + options.converter_id.c_str()); + } + + std::vector raw_req_ids; + AudioPcmBatch raw_audios; + + raw_req_ids.reserve(source.count); + raw_audios.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetSlot("audio_in", i); + if (!in) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing audio_in input slot or slot item is null", + "audio_in", options.converter_id.c_str(), static_cast(i)); + } + + if (in->sample_rate < biz_input::kMinSampleRate || + in->sample_rate > biz_input::kMaxSampleRate) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "sample_rate out of range", "audio_in.sample_rate", + options.converter_id.c_str(), static_cast(i)); + } + + if (in->pcm_length < 0 || in->pcm_length > biz_input::kMaxAudioPcmSamples || + static_cast(in->pcm_length) > + biz_input::kMaxAudioPcmBytes / sizeof(float)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "pcm_length invalid or exceeds limit", "audio_in.pcm_length", + options.converter_id.c_str(), static_cast(i)); + } + + if (in->pcm_length > 0 && !in->pcm_buffer) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "pcm_buffer pointer is null", "audio_in.pcm_buffer", + options.converter_id.c_str(), static_cast(i)); + } + + raw_req_ids.push_back(in->request_id); + + AudioPcmPayload pcm_dto; + if (in->pcm_buffer && in->pcm_length > 0) { + pcm_dto.pcm_data.assign(in->pcm_buffer, in->pcm_buffer + in->pcm_length); + } + pcm_dto.sample_rate = in->sample_rate; + raw_audios.emplace_back(static_cast(i), 0, std::move(pcm_dto)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(raw_req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("audio_inputs"), + std::move(raw_audios), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +InputConverterDefinition MakeCAbiAudioInputConverter() { + InputConverterDefinition def; + def.converter_id = "audio.pcm.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "audio.pcm.request"; + def.schema_version = 1; + def.external_type = "CompanyAudioInputStruct"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"inputs", + "CompanyAudioInputStruct", + PortDirection::kInput, + true, + "CompanyAudioInputStruct", + "", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("audio_inputs", "AudioPcmBatch", true, "1:1")}; + def.decode_fn = &DecodeCAbiAudioInput; + return def; +} + +InputConverterDefinition MakeOperatorAudioInputConverter() { + InputConverterDefinition def; + def.converter_id = "audio.pcm.operator.v1"; + def.transport = "operator"; + def.schema_id = "audio.pcm.request"; + def.schema_version = 1; + def.external_type = "CompanyOperatorAudioInput"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"audio_in", + "CompanyOperatorAudioInput", + PortDirection::kInput, + true, + "CompanyOperatorAudioInput", + "audio_in", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("audio_inputs", "AudioPcmBatch", true, "1:1")}; + def.decode_fn = &DecodeOperatorAudioInput; + return def; +} + +REGISTER_INPUT_CONVERTER(MakeCAbiAudioInputConverter()); +REGISTER_INPUT_CONVERTER(MakeOperatorAudioInputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/input/audit_input.cpp b/src/adapter/input/audit_input.cpp new file mode 100644 index 00000000..5f48b1fe --- /dev/null +++ b/src/adapter/input/audit_input.cpp @@ -0,0 +1,226 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/biz_input_constraints.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +constexpr size_t kMaxTextLen = 64 * 1024; // 64KB + +int DecodeCAbiAuditInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, AlgContext* context, + AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + int valid_ret = AdapterValidationHelper::ValidateBatchInputs( + source.items, static_cast(source.count), 64, + options.converter_id.c_str()); + if (valid_ret != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch envelope validation failed", "inputs", + options.converter_id.c_str()); + } + + std::vector req_ids; + TextBatch user_texts; + TextBatch channel_names; + + req_ids.reserve(source.count); + user_texts.reserve(source.count); + channel_names.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetCAbi(i); + if (!AdapterValidationHelper::RequireNotNull( + "inputs[i]", in, static_cast(i), options.converter_id.c_str(), + status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (!AdapterValidationHelper::RequireBoundedString( + "inputs[i].user_text", in->user_text, kMaxTextLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (in->channel_name && + !AdapterValidationHelper::RequireBoundedString( + "inputs[i].channel_name", in->channel_name, + biz_input::kMaxChannelNameBytes, static_cast(i), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + req_ids.push_back(in->request_id); + user_texts.emplace_back(static_cast(i), 0, in->user_text); + channel_names.emplace_back(static_cast(i), 0, + in->channel_name ? in->channel_name : ""); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("user_texts"), std::move(user_texts), + options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("channel_names"), + std::move(channel_names), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +int DecodeOperatorAuditInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + if (source.count == 0 || source.count > 64) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch size out of range [1, 64]", "slots", + options.converter_id.c_str()); + } + + std::vector req_ids; + TextBatch user_texts; + TextBatch channel_names; + + req_ids.reserve(source.count); + user_texts.reserve(source.count); + channel_names.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetSlot("audit_in", i); + if (!in) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing audit_in input slot or slot item is null", + "audit_in", options.converter_id.c_str(), static_cast(i)); + } + + if (!in->user_text || in->user_text->length < 0 || + (in->user_text->length > 0 && !in->user_text->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid user_text CompanyString", "audit_in.user_text", + options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(in->user_text->length) > kMaxTextLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "user_text length exceeds limit", "audit_in.user_text", + options.converter_id.c_str(), static_cast(i)); + } + + std::string channel_str; + if (in->channel_name) { + if (in->channel_name->length < 0 || + (in->channel_name->length > 0 && !in->channel_name->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid channel_name CompanyString", + "audit_in.channel_name", options.converter_id.c_str(), + static_cast(i)); + } + if (static_cast(in->channel_name->length) > + biz_input::kMaxChannelNameBytes) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "channel_name length exceeds limit", + "audit_in.channel_name", options.converter_id.c_str(), + static_cast(i)); + } + channel_str.assign(in->channel_name->data, in->channel_name->length); + } + + std::string user_str(in->user_text->data, in->user_text->length); + req_ids.push_back(in->request_id); + user_texts.emplace_back(static_cast(i), 0, std::move(user_str)); + channel_names.emplace_back(static_cast(i), 0, + std::move(channel_str)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("user_texts"), std::move(user_texts), + options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("channel_names"), + std::move(channel_names), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +InputConverterDefinition MakeCAbiAuditInputConverter() { + InputConverterDefinition def; + def.converter_id = "audit.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "audit.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyAuditInputStruct"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"inputs", + "CompanyAuditInputStruct", + PortDirection::kInput, + true, + "CompanyAuditInputStruct", + "", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("user_texts", "TextBatch", true, "1:1"), + NodePortDefinition("channel_names", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeCAbiAuditInput; + return def; +} + +InputConverterDefinition MakeOperatorAuditInputConverter() { + InputConverterDefinition def; + def.converter_id = "audit.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "audit.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyOperatorAuditInput"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"audit_in", + "CompanyOperatorAuditInput", + PortDirection::kInput, + true, + "CompanyOperatorAuditInput", + "audit_in", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("user_texts", "TextBatch", true, "1:1"), + NodePortDefinition("channel_names", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeOperatorAuditInput; + return def; +} + +REGISTER_INPUT_CONVERTER(MakeCAbiAuditInputConverter()); +REGISTER_INPUT_CONVERTER(MakeOperatorAuditInputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/input/doc_query_input.cpp b/src/adapter/input/doc_query_input.cpp new file mode 100644 index 00000000..72090e77 --- /dev/null +++ b/src/adapter/input/doc_query_input.cpp @@ -0,0 +1,224 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +constexpr size_t kMaxQueryLen = 64 * 1024; // 64KB +constexpr size_t kMaxDocLen = 10 * 1024 * 1024; // 10MB + +int DecodeCAbiDocQueryInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + int valid_ret = AdapterValidationHelper::ValidateBatchInputs( + source.items, static_cast(source.count), 64, + options.converter_id.c_str()); + if (valid_ret != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch envelope validation failed", "inputs", + options.converter_id.c_str()); + } + + std::vector raw_req_ids; + TextBatch raw_docs; + TextBatch raw_queries; + + raw_req_ids.reserve(source.count); + raw_docs.reserve(source.count); + raw_queries.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in_doc = source.GetCAbi(i); + if (!AdapterValidationHelper::RequireNotNull( + "inputs[i]", in_doc, static_cast(i), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (!AdapterValidationHelper::RequireBoundedString( + "inputs[i].query_text", in_doc->query_text, kMaxQueryLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (in_doc->doc_text) { + if (!AdapterValidationHelper::RequireBoundedString( + "inputs[i].doc_text", in_doc->doc_text, kMaxDocLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + } + + raw_req_ids.push_back(in_doc->request_id); + raw_docs.emplace_back(static_cast(i), 0, + in_doc->doc_text ? in_doc->doc_text : ""); + raw_queries.emplace_back(static_cast(i), 0, + in_doc->query_text ? in_doc->query_text : ""); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(raw_req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_docs"), std::move(raw_docs), + options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_queries"), + std::move(raw_queries), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +int DecodeOperatorDocQueryInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + if (source.count == 0 || source.count > 64) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch size out of range [1, 64]", "slots", + options.converter_id.c_str()); + } + + std::vector raw_req_ids; + TextBatch raw_docs; + TextBatch raw_queries; + + raw_req_ids.reserve(source.count); + raw_docs.reserve(source.count); + raw_queries.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetSlot("doc_in", i); + if (!in) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing doc_in input slot or slot item is null", "doc_in", + options.converter_id.c_str(), static_cast(i)); + } + + if (!in->query_text || in->query_text->length < 0 || + (in->query_text->length > 0 && !in->query_text->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid query_text CompanyString", "doc_in.query_text", + options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(in->query_text->length) > kMaxQueryLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "query_text length exceeds limit", "doc_in.query_text", + options.converter_id.c_str(), static_cast(i)); + } + + std::string doc_str; + if (in->doc_text) { + if (in->doc_text->length < 0 || + (in->doc_text->length > 0 && !in->doc_text->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid doc_text CompanyString", "doc_in.doc_text", + options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(in->doc_text->length) > kMaxDocLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "doc_text length exceeds limit", "doc_in.doc_text", + options.converter_id.c_str(), static_cast(i)); + } + doc_str.assign(in->doc_text->data, in->doc_text->length); + } + + std::string query_str(in->query_text->data, in->query_text->length); + + raw_req_ids.push_back(in->request_id); + raw_docs.emplace_back(static_cast(i), 0, std::move(doc_str)); + raw_queries.emplace_back(static_cast(i), 0, std::move(query_str)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(raw_req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_docs"), std::move(raw_docs), + options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_queries"), + std::move(raw_queries), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +InputConverterDefinition MakeCAbiDocQueryInputConverter() { + InputConverterDefinition def; + def.converter_id = "doc_query.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "doc_query.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyDocInputStruct"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"inputs", + "CompanyDocInputStruct", + PortDirection::kInput, + true, + "CompanyDocInputStruct", + "", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("raw_docs", "TextBatch", true, "1:1"), + NodePortDefinition("raw_queries", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeCAbiDocQueryInput; + return def; +} + +InputConverterDefinition MakeOperatorDocQueryInputConverter() { + InputConverterDefinition def; + def.converter_id = "doc_query.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "doc_query.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyOperatorDocInput"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"doc_in", + "CompanyOperatorDocInput", + PortDirection::kInput, + true, + "CompanyOperatorDocInput", + "doc_in", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("raw_docs", "TextBatch", true, "1:1"), + NodePortDefinition("raw_queries", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeOperatorDocQueryInput; + return def; +} + +REGISTER_INPUT_CONVERTER(MakeCAbiDocQueryInputConverter()); +REGISTER_INPUT_CONVERTER(MakeOperatorDocQueryInputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/input/image_query_input.cpp b/src/adapter/input/image_query_input.cpp new file mode 100644 index 00000000..a898ef0c --- /dev/null +++ b/src/adapter/input/image_query_input.cpp @@ -0,0 +1,230 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +constexpr size_t kMaxPathLen = 4096; +constexpr size_t kMaxQueryLen = 64 * 1024; + +int DecodeCAbiImageQueryInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + int valid_ret = AdapterValidationHelper::ValidateBatchInputs( + source.items, static_cast(source.count), 64, + options.converter_id.c_str()); + if (valid_ret != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch envelope validation failed", "inputs", + options.converter_id.c_str()); + } + + std::vector raw_req_ids; + ImageRefBatch raw_images; + TextBatch raw_queries; + + raw_req_ids.reserve(source.count); + raw_images.reserve(source.count); + raw_queries.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in_ocr = source.GetCAbi(i); + if (!AdapterValidationHelper::RequireNotNull( + "inputs[i]", in_ocr, static_cast(i), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (!AdapterValidationHelper::RequireBoundedString( + "inputs[i].image_path", in_ocr->image_path, kMaxPathLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + if (!AdapterValidationHelper::RequireBoundedString( + "inputs[i].query_prompt", in_ocr->query_prompt, kMaxQueryLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + raw_req_ids.push_back(in_ocr->request_id); + raw_images.emplace_back(static_cast(i), 0, in_ocr->image_path); + raw_queries.emplace_back(static_cast(i), 0, + in_ocr->query_prompt ? in_ocr->query_prompt : ""); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(raw_req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("image_paths"), std::move(raw_images), + options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("user_queries"), + std::move(raw_queries), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +int DecodeOperatorImageQueryInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + if (source.count == 0 || source.count > 64) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch size out of range [1, 64]", "slots", + options.converter_id.c_str()); + } + + std::vector raw_req_ids; + ImageRefBatch raw_images; + TextBatch raw_queries; + + raw_req_ids.reserve(source.count); + raw_images.reserve(source.count); + raw_queries.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* frame = source.GetSlot("frame", i); + if (!frame) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing frame input slot or slot item is null", "frame", + options.converter_id.c_str(), static_cast(i)); + } + const auto* query = source.GetSlot("string", i); + if (!query) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing string input slot or slot item is null", "string", + options.converter_id.c_str(), static_cast(i)); + } + + if (!frame->image_uri || frame->image_uri->length < 0 || + (frame->image_uri->length > 0 && !frame->image_uri->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid frame.image_uri CompanyString", "frame.image_uri", + options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(frame->image_uri->length) > kMaxPathLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "image_uri length exceeds limit", "frame.image_uri", + options.converter_id.c_str(), static_cast(i)); + } + + if (query->length < 0 || (query->length > 0 && !query->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid query CompanyString", "string", + options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(query->length) > kMaxQueryLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "query length exceeds limit", "string", + options.converter_id.c_str(), static_cast(i)); + } + + std::string image_path(frame->image_uri->data, frame->image_uri->length); + std::string query_prompt(query->data, query->length); + + raw_req_ids.push_back(frame->request_id); + raw_images.emplace_back(static_cast(i), 0, std::move(image_path)); + raw_queries.emplace_back(static_cast(i), 0, + std::move(query_prompt)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(raw_req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("image_paths"), std::move(raw_images), + options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("user_queries"), + std::move(raw_queries), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +InputConverterDefinition MakeCAbiImageQueryInputConverter() { + InputConverterDefinition def; + def.converter_id = "image_query.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "image_query.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyOcrDocInputStruct"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"inputs", + "CompanyOcrDocInputStruct", + PortDirection::kInput, + true, + "CompanyOcrDocInputStruct", + "", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("image_paths", "ImageRefBatch", true, "1:1"), + NodePortDefinition("user_queries", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeCAbiImageQueryInput; + return def; +} + +InputConverterDefinition MakeOperatorImageQueryInputConverter() { + InputConverterDefinition def; + def.converter_id = "image_query.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "image_query.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyFrame,CompanyString"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"frame", + "CompanyFrame", + PortDirection::kInput, + true, + "CompanyFrame", + "frame", + {}}, + {"string", + "CompanyString", + PortDirection::kInput, + true, + "CompanyString", + "string", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("image_paths", "ImageRefBatch", true, "1:1"), + NodePortDefinition("user_queries", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeOperatorImageQueryInput; + return def; +} + +REGISTER_INPUT_CONVERTER(MakeCAbiImageQueryInputConverter()); +REGISTER_INPUT_CONVERTER(MakeOperatorImageQueryInputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/input/rerank_input.cpp b/src/adapter/input/rerank_input.cpp new file mode 100644 index 00000000..f0ff2278 --- /dev/null +++ b/src/adapter/input/rerank_input.cpp @@ -0,0 +1,257 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "contracts/inference_payloads.h" +#include "core/common_contracts.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +constexpr size_t kMaxTextLen = 64 * 1024; // 64KB + +int DecodeCAbiRerankInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + int valid_ret = AdapterValidationHelper::ValidateBatchInputs( + source.items, static_cast(source.count), 64, + options.converter_id.c_str()); + if (valid_ret != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch envelope validation failed", "inputs", + options.converter_id.c_str()); + } + + std::vector raw_req_ids; + TextBatch queries; + RankedTextBatch candidates; + QueryCandidatesBatch pairs; + + raw_req_ids.reserve(source.count); + queries.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in_rerank = source.GetCAbi(i); + if (!AdapterValidationHelper::RequireNotNull( + "inputs[i]", in_rerank, static_cast(i), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (!AdapterValidationHelper::RequireBoundedString( + "inputs[i].query_text", in_rerank->query_text, kMaxTextLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + if (!AdapterValidationHelper::RequireRange( + "inputs[i].candidate_count", in_rerank->candidate_count, 1, 8, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + raw_req_ids.push_back(in_rerank->request_id); + queries.emplace_back(static_cast(i), 0, in_rerank->query_text); + + for (int c = 0; c < in_rerank->candidate_count; ++c) { + std::string field_name = + "inputs[i].candidate_passages[" + std::to_string(c) + "]"; + if (!AdapterValidationHelper::RequireBoundedString( + field_name.c_str(), in_rerank->candidate_passages[c], kMaxTextLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::string passage = in_rerank->candidate_passages[c]; + candidates.emplace_back( + static_cast(i), static_cast(c), + RankedCandidate(passage, 0.0f, c + 1, static_cast(c))); + pairs.emplace_back( + static_cast(i), static_cast(c), + QueryCandidatePair(in_rerank->query_text, std::move(passage))); + } + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(raw_req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("rerank_queries"), std::move(queries), + options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("rerank_candidates"), + std::move(candidates), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("rerank_pairs"), std::move(pairs), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +int DecodeOperatorRerankInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + if (source.count == 0 || source.count > 64) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch size out of range [1, 64]", "slots", + options.converter_id.c_str()); + } + + std::vector raw_req_ids; + TextBatch queries; + RankedTextBatch candidates; + QueryCandidatesBatch pairs; + + raw_req_ids.reserve(source.count); + queries.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetSlot("rerank_in", i); + if (!in) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing rerank_in input slot or slot item is null", + "rerank_in", options.converter_id.c_str(), static_cast(i)); + } + + if (!in->query_text || in->query_text->length < 0 || + (in->query_text->length > 0 && !in->query_text->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid query_text CompanyString", "rerank_in.query_text", + options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(in->query_text->length) > kMaxTextLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "query_text length exceeds limit", "rerank_in.query_text", + options.converter_id.c_str(), static_cast(i)); + } + + if (in->candidate_count < 1 || in->candidate_count > 8) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "candidate_count out of valid range [1, 8]", + "rerank_in.candidate_count", options.converter_id.c_str(), + static_cast(i)); + } + + std::string query_str(in->query_text->data, in->query_text->length); + raw_req_ids.push_back(in->request_id); + queries.emplace_back(static_cast(i), 0, query_str); + + for (int c = 0; c < in->candidate_count; ++c) { + const auto* pass = in->candidate_passages[c]; + if (!pass || pass->length < 0 || (pass->length > 0 && !pass->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid candidate passage CompanyString", + "rerank_in.candidate_passages", options.converter_id.c_str(), + static_cast(i)); + } + if (static_cast(pass->length) > kMaxTextLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "candidate passage length exceeds limit", + "rerank_in.candidate_passages", options.converter_id.c_str(), + static_cast(i)); + } + + std::string passage(pass->data, pass->length); + candidates.emplace_back( + static_cast(i), static_cast(c), + RankedCandidate(passage, 0.0f, c + 1, static_cast(c))); + pairs.emplace_back(static_cast(i), static_cast(c), + QueryCandidatePair(query_str, std::move(passage))); + } + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(raw_req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("rerank_queries"), std::move(queries), + options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("rerank_candidates"), + std::move(candidates), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("rerank_pairs"), std::move(pairs), + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +InputConverterDefinition MakeCAbiRerankInputConverter() { + InputConverterDefinition def; + def.converter_id = "rerank.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "rerank.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyRerankBatchInputStruct"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"inputs", + "CompanyRerankBatchInputStruct", + PortDirection::kInput, + true, + "CompanyRerankBatchInputStruct", + "", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("rerank_queries", "TextBatch", true, "1:1"), + NodePortDefinition("rerank_candidates", "RankedTextBatch", true, "N:1"), + NodePortDefinition("rerank_pairs", "QueryCandidatesBatch", true, "N:1")}; + def.decode_fn = &DecodeCAbiRerankInput; + return def; +} + +InputConverterDefinition MakeOperatorRerankInputConverter() { + InputConverterDefinition def; + def.converter_id = "rerank.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "rerank.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyOperatorRerankInput"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"rerank_in", + "CompanyOperatorRerankInput", + PortDirection::kInput, + true, + "CompanyOperatorRerankInput", + "rerank_in", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("rerank_queries", "TextBatch", true, "1:1"), + NodePortDefinition("rerank_candidates", "RankedTextBatch", true, "N:1"), + NodePortDefinition("rerank_pairs", "QueryCandidatesBatch", true, "N:1")}; + def.decode_fn = &DecodeOperatorRerankInput; + return def; +} + +REGISTER_INPUT_CONVERTER(MakeCAbiRerankInputConverter()); +REGISTER_INPUT_CONVERTER(MakeOperatorRerankInputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/input/text_input.cpp b/src/adapter/input/text_input.cpp new file mode 100644 index 00000000..7d5dfdea --- /dev/null +++ b/src/adapter/input/text_input.cpp @@ -0,0 +1,303 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +constexpr size_t kMaxSentenceLen = 64 * 1024; // 64 KiB + +template +int DecodeCAbiTextHelper(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, AlgContext* context, + AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + int valid_ret = AdapterValidationHelper::ValidateBatchInputs( + source.items, static_cast(source.count), 64, + options.converter_id.c_str()); + if (valid_ret != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch envelope validation failed", "inputs", + options.converter_id.c_str()); + } + + std::vector req_ids; + TextBatch sentences; + req_ids.reserve(source.count); + sentences.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetCAbi(i); + if (!AdapterValidationHelper::RequireNotNull( + "inputs[i]", in, static_cast(i), options.converter_id.c_str(), + status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + if (!AdapterValidationHelper::RequireBoundedString( + "inputs[i].sentence_text", in->sentence_text, kMaxSentenceLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + req_ids.push_back(in->request_id); + sentences.emplace_back(static_cast(i), 0, in->sentence_text); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("input_sentences"), + std::move(sentences), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +int DecodeCAbiTextInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, AlgContext* context, + AdapterStatus* status) { + return DecodeCAbiTextHelper( + source, options, bindings, context, status); +} + +int DecodeCAbiKeywordInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + return DecodeCAbiTextHelper( + source, options, bindings, context, status); +} + +int DecodeOperatorEntityInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + if (source.count == 0 || source.count > 64) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch size out of range [1, 64]", "slots", + options.converter_id.c_str()); + } + + std::vector req_ids; + TextBatch sentences; + req_ids.reserve(source.count); + sentences.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetSlot("entity_in", i); + if (!in) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing entity_in input slot or slot item is null", + "entity_in", options.converter_id.c_str(), static_cast(i)); + } + if (!in->sentence_text || in->sentence_text->length < 0 || + (in->sentence_text->length > 0 && !in->sentence_text->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "sentence_text string pointer is null or invalid", + "sentence_text", options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(in->sentence_text->length) > kMaxSentenceLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "sentence_text length exceeds 64 KiB limit", "sentence_text", + options.converter_id.c_str(), static_cast(i)); + } + std::string text(in->sentence_text->data, in->sentence_text->length); + req_ids.push_back(in->request_id); + sentences.emplace_back(static_cast(i), 0, std::move(text)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("input_sentences"), + std::move(sentences), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +int DecodeOperatorKeywordInput(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + if (source.count == 0 || source.count > 64) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch size out of range [1, 64]", "slots", + options.converter_id.c_str()); + } + + std::vector req_ids; + TextBatch sentences; + req_ids.reserve(source.count); + sentences.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = + source.GetSlot("keyword_in", i); + if (!in) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing keyword_in input slot or slot item is null", + "keyword_in", options.converter_id.c_str(), static_cast(i)); + } + if (!in->sentence_text || in->sentence_text->length < 0 || + (in->sentence_text->length > 0 && !in->sentence_text->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "sentence_text string pointer is null or invalid", + "sentence_text", options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(in->sentence_text->length) > kMaxSentenceLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "sentence_text length exceeds 64 KiB limit", "sentence_text", + options.converter_id.c_str(), static_cast(i)); + } + std::string text(in->sentence_text->data, in->sentence_text->length); + req_ids.push_back(in->request_id); + sentences.emplace_back(static_cast(i), 0, std::move(text)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("input_sentences"), + std::move(sentences), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +InputConverterDefinition MakeCAbiTextInputConverter() { + InputConverterDefinition def; + def.converter_id = "text.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "text.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyEntityInputStruct"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"sentence_text", + "CompanyEntityInputStruct", + PortDirection::kInput, + true, + "CompanyEntityInputStruct", + "", + {}, + ""}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeCAbiTextInput; + return def; +} + +InputConverterDefinition MakeOperatorEntityInputConverter() { + InputConverterDefinition def; + def.converter_id = "text.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "text.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyOperatorEntityInput"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"entity_in", + "CompanyOperatorEntityInput", + PortDirection::kInput, + true, + "entity_in", + "entity_in", + {}, + ""}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeOperatorEntityInput; + return def; +} + +InputConverterDefinition MakeOperatorKeywordInputConverter() { + InputConverterDefinition def; + def.converter_id = "keyword.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "text.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyOperatorKeywordInput"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"keyword_in", + "CompanyOperatorKeywordInput", + PortDirection::kInput, + true, + "keyword_in", + "keyword_in", + {}, + ""}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeOperatorKeywordInput; + return def; +} + +InputConverterDefinition MakeCAbiKeywordInputConverter() { + InputConverterDefinition def; + def.converter_id = "keyword.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "text.plain.request"; + def.schema_version = 1; + def.external_type = "CompanyKeywordInputStruct"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"sentence_text", + "CompanyKeywordInputStruct", + PortDirection::kInput, + true, + "CompanyKeywordInputStruct", + "", + {}, + ""}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeCAbiKeywordInput; + return def; +} + +REGISTER_INPUT_CONVERTER(MakeCAbiTextInputConverter()); +REGISTER_INPUT_CONVERTER(MakeCAbiKeywordInputConverter()); +REGISTER_INPUT_CONVERTER(MakeOperatorEntityInputConverter()); +REGISTER_INPUT_CONVERTER(MakeOperatorKeywordInputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/input/translate_json_input.cpp b/src/adapter/input/translate_json_input.cpp new file mode 100644 index 00000000..19a48447 --- /dev/null +++ b/src/adapter/input/translate_json_input.cpp @@ -0,0 +1,208 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" +#include "nlohmann/json.hpp" + +namespace llm_edgeflow { +namespace { + +constexpr size_t kMaxSentenceLen = 64 * 1024; // 64 KiB + +int ParseTranslateQuery(const std::string& raw_text, std::string* out_query) { + const auto req_json = nlohmann::json::parse(raw_text, nullptr, false); + if (!req_json.is_object() || !req_json.contains("query") || + !req_json["query"].is_string()) { + return -1; + } + if (out_query) { + *out_query = req_json["query"].get(); + } + return 0; +} + +int DecodeCAbiTranslateJson(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + int valid_ret = AdapterValidationHelper::ValidateBatchInputs( + source.items, static_cast(source.count), 64, + options.converter_id.c_str()); + if (valid_ret != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch envelope validation failed", "inputs", + options.converter_id.c_str()); + } + + std::vector req_ids; + TextBatch sentences; + req_ids.reserve(source.count); + sentences.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetCAbi(i); + if (!AdapterValidationHelper::RequireNotNull( + "inputs[i]", in, static_cast(i), options.converter_id.c_str(), + status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + if (!AdapterValidationHelper::RequireBoundedString( + "inputs[i].sentence_text", in->sentence_text, kMaxSentenceLen, + static_cast(i), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + } + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetCAbi(i); + std::string query; + if (ParseTranslateQuery(in->sentence_text, &query) != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Expected a JSON object with string field query", "json", + options.converter_id.c_str(), static_cast(i)); + } + + req_ids.push_back(in->request_id); + sentences.emplace_back(static_cast(i), 0, std::move(query)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("input_sentences"), + std::move(sentences), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +int DecodeOperatorTranslateJson(const ExternalInputBatchView& source, + const InputDecodeOptions& options, + const InputPortBindings& bindings, + AlgContext* context, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Null AlgContext passed to Decode", "context", + options.converter_id.c_str()); + } + if (source.count == 0 || source.count > 64) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Batch size out of range [1, 64]", "slots", + options.converter_id.c_str()); + } + + std::vector req_ids; + TextBatch sentences; + req_ids.reserve(source.count); + sentences.reserve(source.count); + + for (size_t i = 0; i < source.count; ++i) { + const auto* in = source.GetSlot("entity_in", i); + if (!in) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing entity_in input slot or slot item is null", + "entity_in", options.converter_id.c_str(), static_cast(i)); + } + if (!in->sentence_text || in->sentence_text->length < 0 || + (in->sentence_text->length > 0 && !in->sentence_text->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "sentence_text string pointer is null or invalid", + "sentence_text", options.converter_id.c_str(), static_cast(i)); + } + if (static_cast(in->sentence_text->length) > kMaxSentenceLen) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "sentence_text length exceeds 64 KiB limit", "sentence_text", + options.converter_id.c_str(), static_cast(i)); + } + + std::string raw(in->sentence_text->data, in->sentence_text->length); + std::string query; + if (ParseTranslateQuery(raw, &query) != 0) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Expected a JSON object with string field query", "json", + options.converter_id.c_str(), static_cast(i)); + } + + req_ids.push_back(in->request_id); + sentences.emplace_back(static_cast(i), 0, std::move(query)); + } + + if (!AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("raw_request_ids"), + std::move(req_ids), options.converter_id.c_str(), status) || + !AdapterValidationHelper::PublishContextValue( + *context, bindings.GetActualKey("input_sentences"), + std::move(sentences), options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + return COMPANY_ALG_SUCCESS; +} + +InputConverterDefinition MakeCAbiTranslateJsonInputConverter() { + InputConverterDefinition def; + def.converter_id = "translate.json.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "translate.json.request"; + def.schema_version = 1; + def.external_type = "CompanyEntityInputStruct"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"sentence_text", + "CompanyEntityInputStruct", + PortDirection::kInput, + true, + "CompanyEntityInputStruct", + "", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeCAbiTranslateJson; + return def; +} + +InputConverterDefinition MakeOperatorTranslateJsonInputConverter() { + InputConverterDefinition def; + def.converter_id = "translate.json.operator.v1"; + def.transport = "operator"; + def.schema_id = "translate.json.request"; + def.schema_version = 1; + def.external_type = "CompanyOperatorEntityInput"; + def.max_batch_size = 64; + def.ownership_policy = "copy_in"; + def.thread_model = "stateless"; + def.external_slots = {{"entity_in", + "CompanyOperatorEntityInput", + PortDirection::kInput, + true, + "entity_in", + "entity_in", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("input_sentences", "TextBatch", true, "1:1")}; + def.decode_fn = &DecodeOperatorTranslateJson; + return def; +} + +REGISTER_INPUT_CONVERTER(MakeCAbiTranslateJsonInputConverter()); +REGISTER_INPUT_CONVERTER(MakeOperatorTranslateJsonInputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/io_binding_registry.cpp b/src/adapter/io_binding_registry.cpp new file mode 100644 index 00000000..9d689470 --- /dev/null +++ b/src/adapter/io_binding_registry.cpp @@ -0,0 +1,383 @@ +#include "adapter/io_binding_registry.h" + +#include + +#include "adapter/io_converter_registry.h" +#include "adapter/operator/operator_value_type_registry.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { + +IoBindingRegistry& IoBindingRegistry::Instance() { + static IoBindingRegistry instance; + return instance; +} + +bool IoBindingRegistry::RegisterBinding(const IoBindingDefinition& def) { + std::lock_guard lock(mutex_); + if (def.binding_id.empty()) { + conflict_errors_.push_back("Empty binding_id in IoBindingDefinition"); + return false; + } + if (def.biz_name.empty()) { + conflict_errors_.push_back("Empty biz_name in IoBindingDefinition: " + + def.binding_id); + return false; + } + if (def.transport != "cabi" && def.transport != "operator") { + conflict_errors_.push_back("Invalid transport '" + def.transport + + "' in IoBindingDefinition: " + def.binding_id); + return false; + } + if (def.input_converter_id.empty()) { + conflict_errors_.push_back( + "Empty input_converter_id in IoBindingDefinition: " + def.binding_id); + return false; + } + if (def.output_converter_id.empty()) { + conflict_errors_.push_back( + "Empty output_converter_id in IoBindingDefinition: " + def.binding_id); + return false; + } + + auto it = bindings_.find(def.binding_id); + if (it != bindings_.end()) { + conflict_errors_.push_back("Duplicate IoBinding registration: " + + def.binding_id); + return false; + } + + bindings_[def.binding_id] = def; + return true; +} + +bool IoBindingRegistry::RegisterExposure(const BizExposureDefinition& def) { + std::lock_guard lock(mutex_); + if (def.biz_name.empty()) { + conflict_errors_.push_back("Empty biz_name in BizExposureDefinition"); + return false; + } + if (def.required_transports.empty()) { + conflict_errors_.push_back( + "Empty required_transports in BizExposureDefinition for: " + + def.biz_name); + return false; + } + + auto it = exposures_.find(def.biz_name); + if (it != exposures_.end()) { + conflict_errors_.push_back("Duplicate BizExposure registration: " + + def.biz_name); + return false; + } + + exposures_[def.biz_name] = def; + return true; +} + +const IoBindingDefinition* IoBindingRegistry::FindBinding( + const std::string& binding_id) const { + std::lock_guard lock(mutex_); + auto it = bindings_.find(binding_id); + if (it != bindings_.end()) { + return &it->second; + } + return nullptr; +} + +const BizExposureDefinition* IoBindingRegistry::FindExposure( + const std::string& biz_name) const { + std::lock_guard lock(mutex_); + auto it = exposures_.find(biz_name); + if (it != exposures_.end()) { + return &it->second; + } + return nullptr; +} + +std::vector IoBindingRegistry::AllBindings() const { + std::lock_guard lock(mutex_); + std::vector result; + result.reserve(bindings_.size()); + for (const auto& [_, def] : bindings_) { + result.push_back(def); + } + return result; +} + +std::vector IoBindingRegistry::AllExposures() const { + std::lock_guard lock(mutex_); + std::vector result; + result.reserve(exposures_.size()); + for (const auto& [_, def] : exposures_) { + result.push_back(def); + } + return result; +} + +bool IoBindingRegistry::HasConflict() const { + std::lock_guard lock(mutex_); + return !conflict_errors_.empty(); +} + +std::vector IoBindingRegistry::GetConflictErrors() const { + std::lock_guard lock(mutex_); + return conflict_errors_; +} + +bool IoBindingRegistry::Audit(std::vector* out_errors) const { + std::lock_guard lock(mutex_); + std::vector errors = conflict_errors_; + + const auto& conv_reg = IoConverterRegistry::Instance(); + if (conv_reg.HasConflict()) { + auto conv_errs = conv_reg.GetConflictErrors(); + errors.insert(errors.end(), conv_errs.begin(), conv_errs.end()); + } + + const auto catalog_snapshot = PipelineCatalog::Snapshot(); + + for (const auto& [binding_id, binding] : bindings_) { + // 1. 检查 biz_name 是否在 PipelineCatalog 中已注册 + const auto* biz_def = catalog_snapshot.FindBiz(binding.biz_name); + if (!biz_def) { + errors.push_back( + "Binding '" + binding_id + + "' references unregistered biz_name: " + binding.biz_name); + } + + // 2. 检查 input converter + const auto* in_conv = + conv_reg.FindInputConverter(binding.input_converter_id); + if (!in_conv) { + errors.push_back("Binding '" + binding_id + + "' references unregistered input_converter: " + + binding.input_converter_id); + } else { + if (in_conv->transport != binding.transport) { + errors.push_back("Binding '" + binding_id + "' transport '" + + binding.transport + + "' does not match input converter transport '" + + in_conv->transport + "'"); + } + if (in_conv->max_batch_size == 0) { + errors.push_back( + "Binding '" + binding_id + + "' references input converter with max_batch_size 0: " + + binding.input_converter_id); + } + for (const auto& [logical_name, target_key] : binding.input_ports) { + auto port_it = std::find_if( + in_conv->logical_ports.begin(), in_conv->logical_ports.end(), + [&](const auto& p) { return p.logical_name == logical_name; }); + if (port_it == in_conv->logical_ports.end()) { + errors.push_back( + "Binding '" + binding_id + + "' maps unadvertised input logical port: " + logical_name); + } else if (biz_def) { + auto ingress_it = std::find_if( + biz_def->ingress.begin(), biz_def->ingress.end(), + [&](const auto& p) { return p.blackboard_key == target_key; }); + if (ingress_it == biz_def->ingress.end()) { + errors.push_back( + "Binding '" + binding_id + "' input port '" + logical_name + + "' maps to non-existent biz ingress key: " + target_key); + } else if (port_it->type_id != ingress_it->type_id) { + errors.push_back("Binding '" + binding_id + "' input port '" + + logical_name + "' type '" + port_it->type_id + + "' does not match biz ingress key '" + target_key + + "' type '" + ingress_it->type_id + "'"); + } + } + } + + if (biz_def) { + for (const auto& ingress_port : biz_def->ingress) { + if (!ingress_port.required) continue; + bool covered = + std::any_of(binding.input_ports.begin(), + binding.input_ports.end(), [&](const auto& kv) { + return kv.second == ingress_port.blackboard_key; + }); + if (!covered) { + errors.push_back("Binding '" + binding_id + + "' missing required biz ingress port: " + + ingress_port.blackboard_key); + } + } + } + + for (const auto& port : in_conv->logical_ports) { + if (!port.required) continue; + if (binding.input_ports.find(port.logical_name) == + binding.input_ports.end()) { + errors.push_back("Binding '" + binding_id + + "' missing required input converter logical port " + "mapping: " + + port.logical_name); + } + } + } + + // 3. 检查 output converter + const auto* out_conv = + conv_reg.FindOutputConverter(binding.output_converter_id); + if (!out_conv) { + errors.push_back("Binding '" + binding_id + + "' references unregistered output_converter: " + + binding.output_converter_id); + } else { + if (out_conv->transport != binding.transport) { + errors.push_back("Binding '" + binding_id + "' transport '" + + binding.transport + + "' does not match output converter transport '" + + out_conv->transport + "'"); + } + if (out_conv->max_batch_size == 0) { + errors.push_back( + "Binding '" + binding_id + + "' references output converter with max_batch_size 0: " + + binding.output_converter_id); + } + for (const auto& [logical_name, target_key] : binding.output_ports) { + auto port_it = std::find_if( + out_conv->logical_ports.begin(), out_conv->logical_ports.end(), + [&](const auto& p) { return p.logical_name == logical_name; }); + if (port_it == out_conv->logical_ports.end()) { + errors.push_back( + "Binding '" + binding_id + + "' maps unadvertised output logical port: " + logical_name); + } else if (biz_def) { + auto egress_it = std::find_if( + biz_def->egress.begin(), biz_def->egress.end(), + [&](const auto& p) { return p.blackboard_key == target_key; }); + if (egress_it != biz_def->egress.end()) { + if (port_it->type_id != egress_it->type_id) { + errors.push_back("Binding '" + binding_id + "' output port '" + + logical_name + "' type '" + port_it->type_id + + "' does not match biz egress key '" + + target_key + "' type '" + egress_it->type_id + + "'"); + } + } else { + auto ingress_it = std::find_if( + biz_def->ingress.begin(), biz_def->ingress.end(), + [&](const auto& p) { return p.blackboard_key == target_key; }); + if (ingress_it != biz_def->ingress.end()) { + if (port_it->type_id != ingress_it->type_id) { + errors.push_back("Binding '" + binding_id + "' output port '" + + logical_name + "' type '" + port_it->type_id + + "' does not match biz ingress key '" + + target_key + "' type '" + ingress_it->type_id + + "'"); + } + } else { + errors.push_back("Binding '" + binding_id + "' output port '" + + logical_name + + "' maps to non-existent biz key: " + target_key); + } + } + } + } + + if (biz_def) { + for (const auto& egress_port : biz_def->egress) { + if (!egress_port.required) continue; + bool covered = + std::any_of(binding.output_ports.begin(), + binding.output_ports.end(), [&](const auto& kv) { + return kv.second == egress_port.blackboard_key; + }); + if (!covered) { + errors.push_back("Binding '" + binding_id + + "' missing required biz egress port: " + + egress_port.blackboard_key); + } + } + } + + for (const auto& port : out_conv->logical_ports) { + if (!port.required) continue; + if (binding.output_ports.find(port.logical_name) == + binding.output_ports.end()) { + errors.push_back("Binding '" + binding_id + + "' missing required output converter logical port " + "mapping: " + + port.logical_name); + } + } + } + + // 4. 若为 Operator 传输协议,检查对应槽位的 ValueType 绑定 + if (binding.transport == "operator") { + if (in_conv) { + for (const auto& slot : in_conv->external_slots) { + if (slot.direction != PortDirection::kInput) continue; + const auto* val_binding = + OperatorValueTypeRegistry::Instance().GetBindingBySuffix( + slot.type_suffix); + if (!val_binding) { + errors.push_back( + "Binding '" + binding_id + "' input slot '" + slot.slot_name + + "' uses unregistered ValueType suffix: " + slot.type_suffix); + } else if (!val_binding->validate_external) { + errors.push_back("Binding '" + binding_id + "' input slot '" + + slot.slot_name + "' ValueType suffix '" + + slot.type_suffix + "' missing validate_external"); + } + } + } + if (out_conv) { + for (const auto& slot : out_conv->external_slots) { + if (slot.direction != PortDirection::kOutput) continue; + const auto* val_binding = + OperatorValueTypeRegistry::Instance().GetOutputBinding( + slot.type_suffix, ""); + if (!val_binding) { + errors.push_back( + "Binding '" + binding_id + "' output slot '" + slot.slot_name + + "' uses unregistered ValueType suffix: " + slot.type_suffix); + } + } + } + } + } + + // 4. 检查生产曝光集合是否都有可用绑定 + for (const auto& [biz_name, exposure] : exposures_) { + for (const auto& req_transport : exposure.required_transports) { + bool found = false; + for (const auto& [_, binding] : bindings_) { + if (binding.biz_name == biz_name && + binding.transport == req_transport) { + found = true; + break; + } + } + if (!found) { + errors.push_back( + "Production exposure for biz '" + biz_name + + "' lacks valid binding for required transport: " + req_transport); + } + } + } + + if (out_errors) { + *out_errors = errors; + } + return errors.empty(); +} + +void IoBindingRegistry::ClearForTesting() { + std::lock_guard lock(mutex_); + bindings_.clear(); + exposures_.clear(); + conflict_errors_.clear(); +} + +void IoBindingRegistry::ResetConflictForTesting() { + std::lock_guard lock(mutex_); + conflict_errors_.clear(); +} + +} // namespace llm_edgeflow diff --git a/src/adapter/io_binding_resolver.cpp b/src/adapter/io_binding_resolver.cpp new file mode 100644 index 00000000..25bef7b0 --- /dev/null +++ b/src/adapter/io_binding_resolver.cpp @@ -0,0 +1,456 @@ +#include "adapter/io_binding_resolver.h" + +#include +#include +#include + +#include "adapter/deployment_model_resolver.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter_registry.h" +#include "adapter/operator/operator_config_resolver.h" +#include "adapter/operator/operator_value_type_registry.h" +#include "core/diagnostic_code.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { + +namespace fs = std::filesystem; + +int IoBindingResolver::ResolveFromFile( + const std::string& config_path, const std::string& transport, + const std::string& model_root_dir, + std::unique_ptr* out_plan, std::string* out_error) { + DeploymentIoConfig config; + std::string err; + if (!DeploymentIoConfig::ReadFromFile(config_path, transport, &config, + &err)) { + if (out_error) *out_error = err; + return -2; + } + return ResolveFromConfig(config, transport, model_root_dir, out_plan, + out_error); +} + +int IoBindingResolver::ResolveFromConfig( + const DeploymentIoConfig& config, const std::string& transport, + const std::string& model_root_dir, + std::unique_ptr* out_plan, std::string* out_error) { + if (!out_plan) { + if (out_error) *out_error = "Null out_plan pointer"; + return -1; + } + *out_plan = nullptr; + + // 1. 查找绑定定义 + const auto* binding = + IoBindingRegistry::Instance().FindBinding(config.io_binding); + if (!binding) { + if (out_error) { + *out_error = "Unknown or unregistered io_binding: " + config.io_binding + + " (at data.io_binding)"; + } + return -2; + } + + // 2. 检查入口类型匹配 + if (binding->transport != transport) { + if (out_error) { + *out_error = "Binding transport mismatch for '" + config.io_binding + + "': expected '" + transport + "', but binding declared '" + + binding->transport + "'"; + } + return -2; + } + + // 3. 查找输入与输出转换器 + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + binding->input_converter_id); + if (!in_conv) { + if (out_error) { + *out_error = "Binding references unregistered input converter: " + + binding->input_converter_id; + } + return -2; + } + + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + binding->output_converter_id); + if (!out_conv) { + if (out_error) { + *out_error = "Binding references unregistered output converter: " + + binding->output_converter_id; + } + return -2; + } + + // 4. 计算有效批次上限 + size_t max_batch = + std::min(in_conv->max_batch_size, out_conv->max_batch_size); + const auto* exposure = + IoBindingRegistry::Instance().FindExposure(binding->biz_name); + if (exposure) { + max_batch = std::min(max_batch, exposure->max_batch_size); + } + + // 5. 若为 Operator 入口,校验 outputs 配置与槽位 + std::unordered_map output_specs; + std::unordered_map output_params; + if (transport == "operator") { + // 5.1 拒绝未在输出转换器中声明的未知槽位配置 + for (auto it = config.outputs.begin(); it != config.outputs.end(); ++it) { + bool found = false; + for (const auto& slot : out_conv->external_slots) { + if (slot.direction == PortDirection::kOutput && + slot.slot_name == it.key()) { + found = true; + break; + } + } + if (!found) { + if (out_error) { + *out_error = "Unknown configured output slot: " + it.key(); + } + return -2; + } + } + + // 5.2 校验并解析每个输出槽位配置 (复用统一的 OperatorConfigResolver 规范) + for (const auto& slot : out_conv->external_slots) { + if (slot.direction != PortDirection::kOutput) continue; + if (!config.outputs.contains(slot.slot_name)) { + if (slot.required) { + if (out_error) { + *out_error = "Missing required Operator output slot '" + + slot.slot_name + "' in data.outputs"; + } + return -2; + } + continue; + } + const auto& slot_cfg = config.outputs[slot.slot_name]; + ResolvedOutputPoolSpec pool_spec; + std::string param_text; + std::string alloc_err; + int alloc_ret = OperatorConfigResolver::ResolveOutputAllocation( + slot_cfg, slot, &pool_spec, ¶m_text, &alloc_err); + if (alloc_ret != 0) { + if (out_error) *out_error = alloc_err; + return alloc_ret; + } + output_specs[slot.slot_name] = std::move(pool_spec); + output_params[slot.slot_name] = std::move(param_text); + } + + // 5.3 默认深度下的句柄池载荷总预算校验 + size_t total_handle_pool_bytes = 0; + for (const auto& [slot_name, pool_spec] : output_specs) { + const auto* output_binding = + OperatorValueTypeRegistry::Instance().GetOutputBinding( + pool_spec.type, pool_spec.allocator); + if (!output_binding || + output_binding->direction != IoDirection::kOutput) { + if (out_error) { + *out_error = "Missing output value binding for suffix '" + + pool_spec.type + "'"; + } + return -2; + } + size_t slot_pool_bytes = 0; + std::string budget_err; + if (!ComputeOutputPoolPayloadBytes(*output_binding, pool_spec, + kDefaultOutputPoolDepth, + &slot_pool_bytes, &budget_err)) { + if (out_error) { + *out_error = "Output pool budget calculation failed: " + budget_err; + } + return -2; + } + if (!CheckedAdd(total_handle_pool_bytes, slot_pool_bytes, + &total_handle_pool_bytes)) { + if (out_error) *out_error = "Handle pool budget addition overflowed"; + return -2; + } + } + if (total_handle_pool_bytes > kMaxHandlePoolPayloadBytes) { + if (out_error) { + *out_error = "Total output pool payload (" + + std::to_string(total_handle_pool_bytes) + + " bytes) exceeds per-handle payload budget (" + + std::to_string(kMaxHandlePoolPayloadBytes) + " bytes)"; + } + return -2; + } + } + + // 6. 读取 Pipeline JSON + std::ifstream pipe_ifs(config.resolved_pipe_path); + if (!pipe_ifs.is_open()) { + if (out_error) { + *out_error = "Failed to open pipeline file: " + config.resolved_pipe_path; + } + return -2; + } + nlohmann::json raw_pipe_json; + try { + pipe_ifs >> raw_pipe_json; + } catch (const std::exception& e) { + if (out_error) { + *out_error = "JSON parse exception in pipeline file: " + + config.resolved_pipe_path + ": " + e.what(); + } + return -2; + } + + // 7. 应用覆盖与解析模型路径 + nlohmann::json staged_pipe_json = raw_pipe_json; + if (!config.model_paths.empty()) { + std::unordered_set known_model_ids; + if (staged_pipe_json.contains("models") && + staged_pipe_json["models"].is_array()) { + for (const auto& m : staged_pipe_json["models"]) { + if (m.is_object() && m.contains("model_id")) { + known_model_ids.insert(m["model_id"].get()); + } + } + } + for (const auto& [mid, _] : config.model_paths) { + if (!known_model_ids.count(mid)) { + if (out_error) { + *out_error = "Unknown model_id '" + mid + "' in 'model_paths'"; + } + return -2; + } + } + for (auto& m : staged_pipe_json["models"]) { + if (m.is_object() && m.contains("model_id")) { + std::string mid = m["model_id"].get(); + auto it = config.model_paths.find(mid); + if (it != config.model_paths.end()) { + m["model_path"] = it->second; + } + } + } + } + + nlohmann::json resolved_pipeline_json; + std::string model_resolve_err; + if (!ResolveDeploymentModelPaths(staged_pipe_json, model_root_dir, + &resolved_pipeline_json, + &model_resolve_err)) { + if (out_error) *out_error = model_resolve_err; + return -2; + } + + // 8. 构造中性 PipelineIoBoundary + PipelineIoBoundary io_boundary; + for (const auto& port : in_conv->logical_ports) { + std::string key = port.logical_name; + auto bit = binding->input_ports.find(port.logical_name); + if (bit != binding->input_ports.end()) { + key = bit->second; + } + io_boundary.input_published_ports.emplace_back( + key, port.type_id, port.required, port.cardinality, + port.provenance_policy, port.lifetime, port.lifetime_config_field); + } + + for (const auto& port : out_conv->logical_ports) { + std::string key = port.logical_name; + auto bit = binding->output_ports.find(port.logical_name); + if (bit != binding->output_ports.end()) { + key = bit->second; + } + io_boundary.output_consumed_ports.emplace_back( + key, port.type_id, port.required, port.cardinality, + port.provenance_policy, port.lifetime, port.lifetime_config_field); + } + + // 9. 调用 PipelineValidator 进行统一中性计划验证 + auto plan = std::make_unique( + PipelineValidator::ValidateAndPlan( + resolved_pipeline_json, ValidationPolicy::kStrict, &io_boundary)); + + if (!plan->report.ok) { + if (out_error) { + if (!plan->report.diagnostics.empty()) { + const auto& d = plan->report.diagnostics.front(); + *out_error = + "Validation failed: " + std::string(DiagnosticCodeName(d.code)) + + " at " + d.path + ": " + d.message; + } else { + *out_error = "Validation failed without diagnostics"; + } + } + return -3; + } + + // 10. 核对 Pipeline biz_name 与 binding biz_name + if (plan->config.biz_name != binding->biz_name) { + if (out_error) { + *out_error = "Pipeline biz_name '" + plan->config.biz_name + + "' does not match binding biz_name '" + binding->biz_name + + "'"; + } + return -3; + } + + // 11. 组装不可变接入计划 + auto io_plan = std::make_unique(); + io_plan->binding = *binding; + io_plan->input_converter = in_conv; + io_plan->output_converter = out_conv; + io_plan->input_port_bindings = InputPortBindings(binding->input_ports); + io_plan->output_port_bindings = OutputPortBindings(binding->output_ports); + io_plan->effective_max_batch_size = max_batch; + io_plan->operator_output_specs = std::move(output_specs); + io_plan->operator_output_parameter_texts = std::move(output_params); + io_plan->resolved_pipeline_json = resolved_pipeline_json; + io_plan->pipeline_plan = std::move(plan); + + *out_plan = std::move(io_plan); + return 0; +} + +int IoBindingResolver::ResolveFromPipelineJson( + const nlohmann::json& pipeline_json, const std::string& binding_id, + const std::string& transport, const std::string& model_root_dir, + std::unique_ptr* out_plan, std::string* out_error) { + if (!out_plan) { + if (out_error) *out_error = "Null out_plan pointer"; + return -1; + } + *out_plan = nullptr; + + if (binding_id.empty()) { + if (out_error) *out_error = "binding_id must not be empty"; + return -2; + } + + // 1. 查找绑定定义 + const auto* binding = IoBindingRegistry::Instance().FindBinding(binding_id); + if (!binding) { + if (out_error) { + *out_error = "Unknown or unregistered io_binding: " + binding_id; + } + return -2; + } + + // 2. 检查入口类型匹配 + if (!transport.empty() && binding->transport != transport) { + if (out_error) { + *out_error = "Binding transport mismatch for '" + binding_id + + "': expected '" + transport + "', but binding declared '" + + binding->transport + "'"; + } + return -2; + } + + // 3. 查找输入与输出转换器 + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + binding->input_converter_id); + if (!in_conv) { + if (out_error) { + *out_error = "Binding references unregistered input converter: " + + binding->input_converter_id; + } + return -2; + } + + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + binding->output_converter_id); + if (!out_conv) { + if (out_error) { + *out_error = "Binding references unregistered output converter: " + + binding->output_converter_id; + } + return -2; + } + + // 4. 计算有效批次上限 + size_t max_batch = + std::min(in_conv->max_batch_size, out_conv->max_batch_size); + const auto* exposure = + IoBindingRegistry::Instance().FindExposure(binding->biz_name); + if (exposure) { + max_batch = std::min(max_batch, exposure->max_batch_size); + } + + // 5. 解析模型路径 (若提供 model_root_dir) + nlohmann::json resolved_pipeline_json; + std::string model_resolve_err; + if (!ResolveDeploymentModelPaths(pipeline_json, model_root_dir, + &resolved_pipeline_json, + &model_resolve_err)) { + if (out_error) *out_error = model_resolve_err; + return -2; + } + + // 6. 构造中性 PipelineIoBoundary + PipelineIoBoundary io_boundary; + for (const auto& port : in_conv->logical_ports) { + std::string key = port.logical_name; + auto bit = binding->input_ports.find(port.logical_name); + if (bit != binding->input_ports.end()) { + key = bit->second; + } + io_boundary.input_published_ports.emplace_back( + key, port.type_id, port.required, port.cardinality, + port.provenance_policy, port.lifetime, port.lifetime_config_field); + } + + for (const auto& port : out_conv->logical_ports) { + std::string key = port.logical_name; + auto bit = binding->output_ports.find(port.logical_name); + if (bit != binding->output_ports.end()) { + key = bit->second; + } + io_boundary.output_consumed_ports.emplace_back( + key, port.type_id, port.required, port.cardinality, + port.provenance_policy, port.lifetime, port.lifetime_config_field); + } + + // 7. 调用 PipelineValidator 进行统一中性计划验证 (包含 I/O 边界验证) + auto plan = std::make_unique( + PipelineValidator::ValidateAndPlan( + resolved_pipeline_json, ValidationPolicy::kStrict, &io_boundary)); + + if (!plan->report.ok) { + if (out_error) { + if (!plan->report.diagnostics.empty()) { + const auto& d = plan->report.diagnostics.front(); + *out_error = + "Validation failed: " + std::string(DiagnosticCodeName(d.code)) + + " at " + d.path + ": " + d.message; + } else { + *out_error = "Validation failed without diagnostics"; + } + } + return -3; + } + + // 8. 核对 Pipeline biz_name 与 binding biz_name + if (plan->config.biz_name != binding->biz_name) { + if (out_error) { + *out_error = "Pipeline biz_name '" + plan->config.biz_name + + "' does not match binding biz_name '" + binding->biz_name + + "'"; + } + return -3; + } + + // 9. 组装不可变接入计划 + auto io_plan = std::make_unique(); + io_plan->binding = *binding; + io_plan->input_converter = in_conv; + io_plan->output_converter = out_conv; + io_plan->input_port_bindings = InputPortBindings(binding->input_ports); + io_plan->output_port_bindings = OutputPortBindings(binding->output_ports); + io_plan->effective_max_batch_size = max_batch; + io_plan->resolved_pipeline_json = resolved_pipeline_json; + io_plan->pipeline_plan = std::move(plan); + + *out_plan = std::move(io_plan); + return 0; +} + +} // namespace llm_edgeflow diff --git a/src/adapter/io_binding_resolver.h b/src/adapter/io_binding_resolver.h new file mode 100644 index 00000000..ba64fccb --- /dev/null +++ b/src/adapter/io_binding_resolver.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include "adapter/deployment_io_config.h" +#include "adapter/io_binding.h" +#include "adapter/io_converter.h" +#include "adapter/operator_io_contracts.h" +#include "core/pipeline_validator.h" + +namespace llm_edgeflow { + +/** + * @brief 已验证的不可变接入计划 (同时包含 I/O 转换器绑定与内部 Pipeline 计划) + */ +struct ValidatedIoPlan { + IoBindingDefinition binding; + const InputConverterDefinition* input_converter = nullptr; + const OutputConverterDefinition* output_converter = nullptr; + InputPortBindings input_port_bindings; + OutputPortBindings output_port_bindings; + size_t effective_max_batch_size = 64; + + std::unordered_map operator_output_specs; + std::unordered_map operator_output_parameter_texts; + nlohmann::json resolved_pipeline_json; + + std::unique_ptr pipeline_plan; +}; + +/** + * @brief 接入绑定解析器 (负责配置、转换器组合校验并调用 PipelineValidator + * 进行中性边界验证) + */ +class IoBindingResolver { + public: + static int ResolveFromConfig( + const DeploymentIoConfig& config, + const std::string& transport, // "cabi" 或 "operator" + const std::string& model_root_dir, + std::unique_ptr* out_plan, std::string* out_error); + + static int ResolveFromFile(const std::string& config_path, + const std::string& transport, + const std::string& model_root_dir, + std::unique_ptr* out_plan, + std::string* out_error); + + static int ResolveFromPipelineJson( + const nlohmann::json& pipeline_json, const std::string& binding_id, + const std::string& transport, // "cabi" 或 "operator" + const std::string& model_root_dir, + std::unique_ptr* out_plan, std::string* out_error); +}; + +} // namespace llm_edgeflow diff --git a/src/adapter/io_catalog.cpp b/src/adapter/io_catalog.cpp new file mode 100644 index 00000000..e15073ae --- /dev/null +++ b/src/adapter/io_catalog.cpp @@ -0,0 +1,198 @@ +#include "adapter/io_catalog.h" + +#include +#include +#include +#include + +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter_registry.h" + +namespace llm_edgeflow { +namespace { + +nlohmann::json SlotJson(const ExternalSlotDefinition& slot) { + return {{"slot_name", slot.slot_name}, + {"direction", + slot.direction == PortDirection::kInput ? "input" : "output"}, + {"value_type", slot.value_type}, + {"required", slot.required}, + {"capacity_fields", slot.capacity_fields}}; +} + +nlohmann::json LogicalPortJson(const NodePortDefinition& port) { + nlohmann::json res = {{"key", port.logical_name}, + {"type_id", port.type_id}, + {"required", port.required}, + {"cardinality", port.cardinality}, + {"provenance_policy", port.provenance_policy}, + {"lifetime", port.lifetime}}; + if (!port.lifetime_config_field.empty()) { + res["lifetime_config_field"] = port.lifetime_config_field; + } + return res; +} + +nlohmann::json BizPortJson(const BizPortDefinition& port) { + nlohmann::json res = {{"key", port.blackboard_key}, + {"type_id", port.type_id}, + {"required", port.required}, + {"cardinality", port.cardinality}, + {"provenance_policy", port.provenance_policy}, + {"lifetime", port.lifetime}}; + if (!port.lifetime_config_field.empty()) { + res["lifetime_config_field"] = port.lifetime_config_field; + } + return res; +} + +nlohmann::json InputConverterToJson(const InputConverterDefinition& conv) { + nlohmann::json slots = nlohmann::json::array(); + for (const auto& s : conv.external_slots) slots.push_back(SlotJson(s)); + nlohmann::json ports = nlohmann::json::array(); + for (const auto& p : conv.logical_ports) ports.push_back(LogicalPortJson(p)); + + return {{"converter_id", conv.converter_id}, + {"transport", conv.transport}, + {"schema_id", conv.schema_id}, + {"schema_version", conv.schema_version}, + {"external_type", conv.external_type}, + {"max_batch_size", conv.max_batch_size}, + {"external_slots", std::move(slots)}, + {"logical_ports", std::move(ports)}}; +} + +nlohmann::json OutputConverterToJson(const OutputConverterDefinition& conv) { + nlohmann::json slots = nlohmann::json::array(); + for (const auto& s : conv.external_slots) slots.push_back(SlotJson(s)); + nlohmann::json ports = nlohmann::json::array(); + for (const auto& p : conv.logical_ports) ports.push_back(LogicalPortJson(p)); + + return {{"converter_id", conv.converter_id}, + {"transport", conv.transport}, + {"schema_id", conv.schema_id}, + {"schema_version", conv.schema_version}, + {"external_type", conv.external_type}, + {"max_batch_size", conv.max_batch_size}, + {"cardinality", conv.cardinality}, + {"capacity_policy", conv.capacity_policy}, + {"external_slots", std::move(slots)}, + {"logical_ports", std::move(ports)}}; +} + +nlohmann::json IoBindingToJson(const IoBindingDefinition& b) { + return {{"binding_id", b.binding_id}, + {"biz_name", b.biz_name}, + {"transport", b.transport}, + {"input_converter_id", b.input_converter_id}, + {"output_converter_id", b.output_converter_id}, + {"input_port_mapping", b.input_ports}, + {"output_port_mapping", b.output_ports}, + {"max_batch_size", b.max_batch_size}}; +} + +} // namespace + +nlohmann::json IoCatalog::ToJson(const PipelineCatalogSnapshot& snapshot, + const std::string& biz_filter) { + nlohmann::json nodes = nlohmann::json::array(); + for (const auto& item : snapshot.nodes) { + if (!biz_filter.empty() && !item.biz_names.empty() && + std::find(item.biz_names.begin(), item.biz_names.end(), biz_filter) == + item.biz_names.end()) { + continue; + } + nodes.push_back(PipelineCatalog::NodeToJson(item)); + } + + nlohmann::json models = nlohmann::json::array(); + for (const auto& item : PipelineCatalog::Models()) { + models.push_back(PipelineCatalog::ModelToJson(item)); + } + + nlohmann::json backends = nlohmann::json::array(); + for (const auto& item : PipelineCatalog::Backends()) { + backends.push_back(PipelineCatalog::BackendToJson(item)); + } + + nlohmann::json bizs = nlohmann::json::array(); + for (const auto& item : snapshot.bizs) { + if (!biz_filter.empty() && item.biz_name != biz_filter) continue; + nlohmann::json ingress = nlohmann::json::array(); + nlohmann::json egress = nlohmann::json::array(); + for (const auto& port : item.ingress) ingress.push_back(BizPortJson(port)); + for (const auto& port : item.egress) egress.push_back(BizPortJson(port)); + bizs.push_back({{"biz_name", item.biz_name}, + {"demo_biz", item.demo_biz}, + {"display_name", item.display_name}, + {"ingress", std::move(ingress)}, + {"egress", std::move(egress)}}); + } + + // 聚合 IO Bindings 与 Converters + auto all_bindings = IoBindingRegistry::Instance().AllBindings(); + std::sort(all_bindings.begin(), all_bindings.end(), + [](const IoBindingDefinition& a, const IoBindingDefinition& b) { + return a.binding_id < b.binding_id; + }); + + std::set active_input_converters; + std::set active_output_converters; + + nlohmann::json io_bindings = nlohmann::json::array(); + for (const auto& b : all_bindings) { + if (!biz_filter.empty() && b.biz_name != biz_filter) continue; + active_input_converters.insert(b.input_converter_id); + active_output_converters.insert(b.output_converter_id); + io_bindings.push_back(IoBindingToJson(b)); + } + + auto all_input_converters = + IoConverterRegistry::Instance().AllInputConverters(); + std::sort( + all_input_converters.begin(), all_input_converters.end(), + [](const InputConverterDefinition& a, const InputConverterDefinition& b) { + return a.converter_id < b.converter_id; + }); + + nlohmann::json input_converters = nlohmann::json::array(); + for (const auto& c : all_input_converters) { + if (!biz_filter.empty() && active_input_converters.find(c.converter_id) == + active_input_converters.end()) { + continue; + } + input_converters.push_back(InputConverterToJson(c)); + } + + auto all_output_converters = + IoConverterRegistry::Instance().AllOutputConverters(); + std::sort(all_output_converters.begin(), all_output_converters.end(), + [](const OutputConverterDefinition& a, + const OutputConverterDefinition& b) { + return a.converter_id < b.converter_id; + }); + + nlohmann::json output_converters = nlohmann::json::array(); + for (const auto& c : all_output_converters) { + if (!biz_filter.empty() && active_output_converters.find(c.converter_id) == + active_output_converters.end()) { + continue; + } + output_converters.push_back(OutputConverterToJson(c)); + } + + return {{"schema_version", 4}, + {"nodes", std::move(nodes)}, + {"models", std::move(models)}, + {"backends", std::move(backends)}, + {"bizs", std::move(bizs)}, + {"input_converters", std::move(input_converters)}, + {"output_converters", std::move(output_converters)}, + {"io_bindings", std::move(io_bindings)}}; +} + +nlohmann::json IoCatalog::ToJson(const std::string& biz_filter) { + return ToJson(PipelineCatalog::Snapshot(), biz_filter); +} + +} // namespace llm_edgeflow diff --git a/src/adapter/io_catalog.h b/src/adapter/io_catalog.h new file mode 100644 index 00000000..a4de3f0e --- /dev/null +++ b/src/adapter/io_catalog.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { + +/** + * @brief Integration 层的 Catalog 聚合门面 (Schema Version 4) + * + * 聚合 Core 的 PipelineCatalogSnapshot 与 Integration 的 + * IoConverterRegistry 及 IoBindingRegistry,生成对外统一 Schema 4 Catalog + * JSON。 + */ +class IoCatalog { + public: + static nlohmann::json ToJson(const PipelineCatalogSnapshot& snapshot, + const std::string& biz_filter = std::string()); + + static nlohmann::json ToJson(const std::string& biz_filter = std::string()); +}; + +} // namespace llm_edgeflow diff --git a/src/adapter/io_converter_registry.cpp b/src/adapter/io_converter_registry.cpp new file mode 100644 index 00000000..c1eeb2f2 --- /dev/null +++ b/src/adapter/io_converter_registry.cpp @@ -0,0 +1,257 @@ +#include "adapter/io_converter_registry.h" + +namespace llm_edgeflow { + +IoConverterRegistry& IoConverterRegistry::Instance() { + static IoConverterRegistry instance; + return instance; +} + +bool IoConverterRegistry::RegisterInputConverter( + const InputConverterDefinition& def) { + std::lock_guard lock(mutex_); + if (def.converter_id.empty()) { + conflict_errors_.push_back( + "Empty converter_id in InputConverterDefinition"); + return false; + } + if (!def.decode_fn) { + conflict_errors_.push_back( + "Missing decode_fn in InputConverterDefinition for: " + + def.converter_id); + return false; + } + if (def.transport != "cabi" && def.transport != "operator") { + conflict_errors_.push_back( + "Invalid transport '" + def.transport + + "' in InputConverterDefinition for: " + def.converter_id); + return false; + } + if (def.schema_id.empty()) { + conflict_errors_.push_back( + "Empty schema_id in InputConverterDefinition for: " + def.converter_id); + return false; + } + if (def.schema_version < 1) { + conflict_errors_.push_back( + "Invalid schema_version (" + std::to_string(def.schema_version) + + ") in InputConverterDefinition for: " + def.converter_id); + return false; + } + if (def.external_type.empty()) { + conflict_errors_.push_back( + "Empty external_type in InputConverterDefinition for: " + + def.converter_id); + return false; + } + if (def.external_slots.empty()) { + conflict_errors_.push_back( + "Empty external_slots in InputConverterDefinition for: " + + def.converter_id); + return false; + } + for (const auto& slot : def.external_slots) { + if (slot.slot_name.empty() || slot.type_id.empty()) { + conflict_errors_.push_back( + "Invalid external_slot (empty slot_name or type_id) in " + "InputConverterDefinition for: " + + def.converter_id); + return false; + } + if (def.transport == "operator" && slot.type_suffix.empty()) { + conflict_errors_.push_back( + "Empty type_suffix for operator slot '" + slot.slot_name + + "' in InputConverterDefinition for: " + def.converter_id); + return false; + } + } + if (def.logical_ports.empty()) { + conflict_errors_.push_back( + "Empty logical_ports in InputConverterDefinition for: " + + def.converter_id); + return false; + } + for (const auto& port : def.logical_ports) { + if (port.logical_name.empty() || port.type_id.empty()) { + conflict_errors_.push_back( + "Invalid logical_port (empty logical_name or type_id) in " + "InputConverterDefinition for: " + + def.converter_id); + return false; + } + } + if (def.max_batch_size == 0) { + conflict_errors_.push_back( + "Invalid max_batch_size (0) in InputConverterDefinition for: " + + def.converter_id); + return false; + } + + auto it = input_converters_.find(def.converter_id); + if (it != input_converters_.end()) { + conflict_errors_.push_back("Duplicate InputConverter registration: " + + def.converter_id); + return false; + } + + input_converters_[def.converter_id] = def; + return true; +} + +bool IoConverterRegistry::RegisterOutputConverter( + const OutputConverterDefinition& def) { + std::lock_guard lock(mutex_); + if (def.converter_id.empty()) { + conflict_errors_.push_back( + "Empty converter_id in OutputConverterDefinition"); + return false; + } + if (!def.encode_fn) { + conflict_errors_.push_back( + "Missing encode_fn in OutputConverterDefinition for: " + + def.converter_id); + return false; + } + if (def.transport != "cabi" && def.transport != "operator") { + conflict_errors_.push_back( + "Invalid transport '" + def.transport + + "' in OutputConverterDefinition for: " + def.converter_id); + return false; + } + if (def.schema_id.empty()) { + conflict_errors_.push_back( + "Empty schema_id in OutputConverterDefinition for: " + + def.converter_id); + return false; + } + if (def.schema_version < 1) { + conflict_errors_.push_back( + "Invalid schema_version (" + std::to_string(def.schema_version) + + ") in OutputConverterDefinition for: " + def.converter_id); + return false; + } + if (def.external_type.empty()) { + conflict_errors_.push_back( + "Empty external_type in OutputConverterDefinition for: " + + def.converter_id); + return false; + } + if (def.external_slots.empty()) { + conflict_errors_.push_back( + "Empty external_slots in OutputConverterDefinition for: " + + def.converter_id); + return false; + } + for (const auto& slot : def.external_slots) { + if (slot.slot_name.empty() || slot.type_id.empty()) { + conflict_errors_.push_back( + "Invalid external_slot (empty slot_name or type_id) in " + "OutputConverterDefinition for: " + + def.converter_id); + return false; + } + if (def.transport == "operator" && slot.type_suffix.empty()) { + conflict_errors_.push_back( + "Empty type_suffix for operator slot '" + slot.slot_name + + "' in OutputConverterDefinition for: " + def.converter_id); + return false; + } + } + if (def.logical_ports.empty()) { + conflict_errors_.push_back( + "Empty logical_ports in OutputConverterDefinition for: " + + def.converter_id); + return false; + } + for (const auto& port : def.logical_ports) { + if (port.logical_name.empty() || port.type_id.empty()) { + conflict_errors_.push_back( + "Invalid logical_port (empty logical_name or type_id) in " + "OutputConverterDefinition for: " + + def.converter_id); + return false; + } + } + if (def.max_batch_size == 0) { + conflict_errors_.push_back( + "Invalid max_batch_size (0) in OutputConverterDefinition for: " + + def.converter_id); + return false; + } + + auto it = output_converters_.find(def.converter_id); + if (it != output_converters_.end()) { + conflict_errors_.push_back("Duplicate OutputConverter registration: " + + def.converter_id); + return false; + } + + output_converters_[def.converter_id] = def; + return true; +} + +const InputConverterDefinition* IoConverterRegistry::FindInputConverter( + const std::string& converter_id) const { + std::lock_guard lock(mutex_); + auto it = input_converters_.find(converter_id); + if (it != input_converters_.end()) { + return &it->second; + } + return nullptr; +} + +const OutputConverterDefinition* IoConverterRegistry::FindOutputConverter( + const std::string& converter_id) const { + std::lock_guard lock(mutex_); + auto it = output_converters_.find(converter_id); + if (it != output_converters_.end()) { + return &it->second; + } + return nullptr; +} + +std::vector IoConverterRegistry::AllInputConverters() + const { + std::lock_guard lock(mutex_); + std::vector result; + result.reserve(input_converters_.size()); + for (const auto& [_, def] : input_converters_) { + result.push_back(def); + } + return result; +} + +std::vector +IoConverterRegistry::AllOutputConverters() const { + std::lock_guard lock(mutex_); + std::vector result; + result.reserve(output_converters_.size()); + for (const auto& [_, def] : output_converters_) { + result.push_back(def); + } + return result; +} + +bool IoConverterRegistry::HasConflict() const { + std::lock_guard lock(mutex_); + return !conflict_errors_.empty(); +} + +std::vector IoConverterRegistry::GetConflictErrors() const { + std::lock_guard lock(mutex_); + return conflict_errors_; +} + +void IoConverterRegistry::ClearForTesting() { + std::lock_guard lock(mutex_); + input_converters_.clear(); + output_converters_.clear(); + conflict_errors_.clear(); +} + +void IoConverterRegistry::ResetConflictForTesting() { + std::lock_guard lock(mutex_); + conflict_errors_.clear(); +} + +} // namespace llm_edgeflow diff --git a/src/adapter/operator/operator_adapter.cpp b/src/adapter/operator/operator_adapter.cpp index 76093376..f2cb1fa4 100644 --- a/src/adapter/operator/operator_adapter.cpp +++ b/src/adapter/operator/operator_adapter.cpp @@ -5,7 +5,6 @@ #include #include -#include "adapter/operator/operator_biz_bridge_registry.h" #include "adapter/operator/operator_config_resolver.h" #include "adapter/operator/operator_control_registry.h" #include "adapter/operator/operator_output_pool.h" @@ -29,8 +28,9 @@ struct OperatorHandle { std::unique_ptr runtime; uint32_t max_frame_depth = 25; uint32_t effective_process_batch_limit = 25; - CompanyAlgBizType biz_type = ALG_BIZ_TYPE_UNKNOWN; - const llm_edgeflow::OperatorBizBridgeDescriptor* bridge = nullptr; + std::string io_binding; + const llm_edgeflow::InputConverterDefinition* input_converter = nullptr; + const llm_edgeflow::OutputConverterDefinition* output_converter = nullptr; llm_edgeflow::ResolvedOperatorConfig resolved_conf; std::unordered_map> @@ -122,13 +122,6 @@ int Operator_Init() noexcept { "OperatorValueTypeRegistry"); return ret; } - std::string bridge_diagnostic; - ret = llm_edgeflow::OperatorBizBridgeRegistry::Instance().GlobalInit( - &bridge_diagnostic); - if (ret != 0) { - SetLastError(bridge_diagnostic); - return ret; - } return 0; } catch (const std::exception& e) { SetLastError(e.what()); @@ -185,7 +178,7 @@ int Operator_Create(void** handle, const CreateParam* param) noexcept { return -2; } - // 1. 双路径安全解析 .conf + // 1. 安全解析部署配置 (.conf) 与接入绑定 llm_edgeflow::ResolvedOperatorConfig resolved_conf; std::string resolve_err; int res_code = llm_edgeflow::OperatorConfigResolver::Resolve( @@ -197,8 +190,8 @@ int Operator_Create(void** handle, const CreateParam* param) noexcept { } // 2. 组装运行时参数 - uint32_t adapter_max_batch = static_cast( - resolved_conf.adapter->GetDescriptor().max_batch_size); + uint32_t adapter_max_batch = + static_cast(resolved_conf.io_plan->effective_max_batch_size); uint32_t effective_batch_limit = std::min(effective_depth, adapter_max_batch); @@ -210,19 +203,16 @@ int Operator_Create(void** handle, const CreateParam* param) noexcept { runtime_options.depth_num = effective_depth; runtime_options.device_id = param->device_id; runtime_options.has_device_id = (param->device_id >= 0); - runtime_options.biz_type = static_cast(resolved_conf.biz_type); runtime_options.biz_name = resolved_conf.biz_name; - // 3. 构建内部共享运行时 + // 3. 构建内部共享运行时 (通过已验证的 IoPlan) std::unique_ptr runtime; std::string create_err; - int create_ret = - llm_edgeflow::SharedAlgorithmRuntime::CreateFromPipelineJson( - resolved_conf.synthetic_pipeline_json, param->device_id, - "", // 模型路径已全量绝对规范化 - resolved_conf.biz_type, &runtime, &create_err, &runtime_options); + int create_ret = llm_edgeflow::SharedAlgorithmRuntime::CreateFromIoPlan( + std::move(resolved_conf.io_plan), param->device_id, &runtime_options, + &runtime, &create_err); if (create_ret != 0) { - SetLastError("SharedAlgorithmRuntime::CreateFromPipelineJson failed: " + + SetLastError("SharedAlgorithmRuntime::CreateFromIoPlan failed: " + create_err); return create_ret; } @@ -231,16 +221,25 @@ int Operator_Create(void** handle, const CreateParam* param) noexcept { std::unordered_map> pools; - for (const auto& out_slot : resolved_conf.bridge_descriptor->output_slots) { - const auto& allocation = - resolved_conf.output_pool_specs.at(out_slot.logical_name); + for (const auto& out_slot : + runtime->GetIoPlan()->output_converter->external_slots) { + if (out_slot.direction != llm_edgeflow::PortDirection::kOutput) continue; + auto pit = resolved_conf.output_pool_specs.find(out_slot.slot_name); + if (pit == resolved_conf.output_pool_specs.end()) { + if (out_slot.required) { + SetLastError("Missing output pool configuration for slot " + + out_slot.slot_name); + return -2; + } + continue; + } + const auto& allocation = pit->second; const auto* binding = llm_edgeflow::OperatorValueTypeRegistry::Instance().GetOutputBinding( out_slot.type_suffix, allocation.allocator); if (!binding) { - SetLastError("Missing output allocator for slot " + - out_slot.logical_name + " (type " + out_slot.type_suffix + - ")"); + SetLastError("Missing output allocator for slot " + out_slot.slot_name + + " (type " + out_slot.type_suffix + ")"); return -5; } std::shared_ptr pool; @@ -250,21 +249,22 @@ int Operator_Create(void** handle, const CreateParam* param) noexcept { &pool_err); if (pool_ret != 0 || !pool) { SetLastError("Failed to create output pool for slot " + - out_slot.logical_name + " (type " + out_slot.type_suffix + + out_slot.slot_name + " (type " + out_slot.type_suffix + "): " + pool_err); return pool_ret != 0 ? pool_ret : -4; } - pools[out_slot.logical_name] = std::move(pool); + pools[out_slot.slot_name] = std::move(pool); } auto handle_instance = std::make_unique(); - handle_instance->runtime = std::move(runtime); handle_instance->max_frame_depth = effective_depth; handle_instance->effective_process_batch_limit = effective_batch_limit; - handle_instance->biz_type = resolved_conf.biz_type; - handle_instance->bridge = resolved_conf.bridge_descriptor; + handle_instance->input_converter = runtime->GetIoPlan()->input_converter; + handle_instance->output_converter = runtime->GetIoPlan()->output_converter; + handle_instance->io_binding = resolved_conf.io_binding; handle_instance->resolved_conf = std::move(resolved_conf); handle_instance->output_pools = std::move(pools); + handle_instance->runtime = std::move(runtime); OperatorHandle* raw_h = handle_instance.get(); OperatorHandleManager::Instance().Register(raw_h); @@ -323,95 +323,117 @@ int Operator_Process(void* handle, const NamedIoBatch& inputs, std::lock_guard lock(h->mutex); - if (!h->runtime || !h->bridge) { - SetLastError("Handle runtime or bridge is null in Process"); + if (!h->runtime || !h->input_converter || !h->output_converter) { + SetLastError( + "Handle runtime or converter definitions are null in Process"); return -1; } - size_t batch_size = inputs.size(); - llm_edgeflow::ProcessLocalShadowStorage shadow_storage; - std::vector internal_in_dtos(batch_size, nullptr); - std::string binding_error; - int binding_result = llm_edgeflow::ConvertOperatorInputs( - inputs, *h->bridge, h->resolved_conf.input_limits, &shadow_storage, - &internal_in_dtos, &binding_error); - if (binding_result != 0) { - SetLastError(binding_error); - return binding_result; + // 1. 验证并提取外部输入槽 + llm_edgeflow::ExternalInputBatchView in_view; + std::string in_err; + int in_ret = llm_edgeflow::ValidateAndExtractOperatorInputs( + inputs, *h->input_converter, h->resolved_conf.input_limits, &in_view, + &in_err); + if (in_ret != 0) { + SetLastError(in_err); + return in_ret; } + // 2. 验证并解析输出槽绑定 std::vector> frame_out_bindings; - binding_result = llm_edgeflow::ResolveOperatorOutputs( - outputs, *h->bridge, &frame_out_bindings, &binding_error); - if (binding_result != 0) { - SetLastError(binding_error); - return binding_result; - } - + std::string out_err; + int out_ret = llm_edgeflow::ResolveOperatorOutputs( + outputs, *h->output_converter, &frame_out_bindings, &out_err); + if (out_ret != 0) { + SetLastError(out_err); + return out_ret; + } + + // 3. 执行统一输入解码 + // (在租用输出块之前完成业务校验;若校验失败则零输出块被租用) + llm_edgeflow::AlgContext req_ctx; + llm_edgeflow::InputDecodeOptions in_options; + in_options.binding_id = h->io_binding; + in_options.converter_id = h->input_converter->converter_id; + in_options.transport = "operator"; + in_options.max_batch_size = h->effective_process_batch_limit; + + llm_edgeflow::AdapterStatus decode_status; + int decode_ret = h->input_converter->decode_fn( + in_view, in_options, h->runtime->GetIoPlan()->input_port_bindings, + &req_ctx, &decode_status); + if (decode_ret != 0) { + SetLastError("DecodeInput failed for " + + h->input_converter->converter_id + ": " + + decode_status.ToString()); + return decode_ret; + } + + // 4. 租用输出池内存块 (受 ScopedOutputLeaseGuard 保护,失败自动归还) llm_edgeflow::ScopedOutputLeaseGuard lease_guard; std::vector acquired_blocks; - binding_result = llm_edgeflow::AcquireOperatorOutputBlocks( + std::string acq_err; + int acq_ret = llm_edgeflow::AcquireOperatorOutputBlocks( frame_out_bindings, h->output_pools, &lease_guard, &acquired_blocks, - &binding_error); - if (binding_result != 0) { - SetLastError(binding_error); - return binding_result; - } - - // 4. 执行内部 Runtime 计算 - std::vector internal_out_dtos(batch_size, nullptr); - for (size_t i = 0; i < batch_size; ++i) { - if (h->bridge->create_shadow_output_dto) { - internal_out_dtos[i] = - h->bridge->create_shadow_output_dto(shadow_storage); - } + &acq_err); + if (acq_ret != 0) { + SetLastError("AcquireOperatorOutputBlocks failed: " + acq_err); + return acq_ret; } - int num_outputs = static_cast(batch_size); - std::string exec_err; - int exec_ret = h->runtime->ExecuteBatch( - internal_in_dtos.data(), static_cast(batch_size), - internal_out_dtos.data(), &num_outputs, &exec_err, true); + + // 5. 执行 Pipeline 计算 + int exec_ret = h->runtime->GetPipeline()->Execute(&req_ctx); if (exec_ret != 0) { - SetLastError("ExecuteBatch failed: " + exec_err); + SetLastError("Pipeline::Execute failed with code " + + std::to_string(exec_ret) + ": " + req_ctx.GetErrorMessage()); return exec_ret; } - if (num_outputs != static_cast(batch_size)) { - SetLastError("ExecuteBatch output count mismatch: expected " + - std::to_string(batch_size) + ", got " + - std::to_string(num_outputs)); - return -4; - } - // 5. 将内部输出结果转换到已租用的池化外部结构中 - for (const auto& acq : acquired_blocks) { - const void* internal_dto = internal_out_dtos[acq.frame_idx]; - if (!internal_dto) { - SetLastError("Internal output DTO is null for frame " + - std::to_string(acq.frame_idx)); - return -4; - } - std::string conv_out_err; - llm_edgeflow::ConvertSampleOutputFn convert = nullptr; - for (const auto& slot : h->bridge->output_slots) { - if (slot.logical_name == acq.logical_name) { - convert = slot.convert_output; - break; - } - } - if (!convert) { - SetLastError("No output conversion for slot " + acq.logical_name); - return -4; + // 6. 执行统一输出编码 (将结果写入已租用的外部结构块) + llm_edgeflow::ExternalOutputBatchView out_view; + out_view.count = inputs.size(); + out_view.type_id = h->output_converter->external_type; + for (const auto& slot : h->output_converter->external_slots) { + if (slot.direction == PortDirection::kOutput) { + out_view.slot_types[slot.slot_name] = slot.type_id; } - int conv_ret = - convert(internal_dto, acq.raw_block, acq.pool->Spec(), &conv_out_err); - if (conv_ret != 0) { - SetLastError("ConvertSampleOutput failed for key " + acq.key + ": " + - conv_out_err); - return conv_ret; + } + for (const auto& acq : acquired_blocks) { + out_view.leased_slots[acq.logical_name].push_back(acq.raw_block); + out_view.pool_specs[acq.logical_name] = acq.pool->Spec(); + for (const auto& cap : acq.pool->Spec().capacities) { + out_view.slot_capacities[acq.logical_name][cap.first] = cap.second; } } + llm_edgeflow::OutputEncodeOptions out_options; + out_options.binding_id = h->io_binding; + out_options.converter_id = h->output_converter->converter_id; + out_options.transport = "operator"; + out_options.max_batch_size = h->effective_process_batch_limit; + + size_t written_count = 0; + llm_edgeflow::AdapterStatus encode_status; + int encode_ret = h->output_converter->encode_fn( + &req_ctx, h->runtime->GetIoPlan()->output_port_bindings, out_options, + &out_view, &written_count, &encode_status); + if (encode_ret != 0) { + SetLastError("EncodeOutput failed for " + + h->output_converter->converter_id + ": " + + encode_status.ToString()); + return encode_ret; + } + if (written_count != inputs.size()) { + SetLastError("EncodeOutput written count (" + + std::to_string(written_count) + + ") does not match input count (" + + std::to_string(inputs.size()) + ")"); + return -4; + } + + // 7. 发布输出 (两阶段发布并解除 guard) llm_edgeflow::PublishOperatorOutputs(acquired_blocks, &outputs, &lease_guard); return 0; @@ -541,7 +563,7 @@ const char* GetOperatorLastError() noexcept { int ValidateOperatorConfigBinding(const char* model_path, const char* cfg_file_name, - int32_t expected_biz_type, + const char* expected_binding_id, char* out_error_msg, size_t error_buf_size) noexcept { try { @@ -559,6 +581,13 @@ int ValidateOperatorConfigBinding(const char* model_path, } return -2; } + if (!expected_binding_id || expected_binding_id[0] == '\0') { + if (out_error_msg && error_buf_size > 0) { + std::snprintf(out_error_msg, error_buf_size, + "Null or empty expected_binding_id"); + } + return -2; + } llm_edgeflow::ResolvedOperatorConfig resolved; std::string err; @@ -571,14 +600,12 @@ int ValidateOperatorConfigBinding(const char* model_path, return ret; } - if (expected_biz_type != 0 && - resolved.biz_type != - static_cast(expected_biz_type)) { + if (resolved.io_binding != expected_binding_id) { if (out_error_msg && error_buf_size > 0) { - std::snprintf( - out_error_msg, error_buf_size, - "Biz mismatch: Config resolves to biz_type %d, but expected %d", - static_cast(resolved.biz_type), expected_biz_type); + std::snprintf(out_error_msg, error_buf_size, + "Binding mismatch: Config resolves to binding '%s', but " + "expected '%s'", + resolved.io_binding.c_str(), expected_binding_id); } return -3; } diff --git a/src/adapter/operator/operator_biz_bridge_registry.cpp b/src/adapter/operator/operator_biz_bridge_registry.cpp deleted file mode 100644 index cdb836e0..00000000 --- a/src/adapter/operator/operator_biz_bridge_registry.cpp +++ /dev/null @@ -1,315 +0,0 @@ -#include "adapter/operator/operator_biz_bridge_registry.h" - -#include -#include -#include - -#include "adapter/biz_adapter_registry.h" -#include "adapter/operator/operator_value_type_registry.h" -#include "contracts/diagnostic.h" - -namespace llm_edgeflow { - -bool RegisterOperatorBizBridge(OperatorBizBridgeDescriptor descriptor) { - return OperatorBizBridgeRegistry::Instance().RegisterBridge( - std::move(descriptor)); -} - -int CopyToOperatorString(const char* source, CompanyString* destination, - uint32_t capacity, const char* field_name, - std::string* diagnostic) noexcept { - return OperatorBizBridgeRegistry::CopyToPooledString( - source, destination, capacity, field_name, diagnostic); -} - -OperatorBizBridgeRegistry& OperatorBizBridgeRegistry::Instance() { - static OperatorBizBridgeRegistry instance; - return instance; -} - -int OperatorBizBridgeRegistry::CopyToPooledString(const char* src, - CompanyString* dest, - uint32_t capacity, - const char* field_name, - std::string* err) noexcept { - try { - if (!dest || !dest->data) { - if (err) - *err = std::string(field_name ? field_name : "string") + - " in destination pool block is null"; - return -4; - } - if (!src) { - dest->length = 0; - dest->data[0] = '\0'; - return 0; - } - size_t len = std::strlen(src); - if (len > capacity) { - if (err) - *err = std::string(field_name ? field_name : "string") + - " output length (" + std::to_string(len) + - ") exceeds pool capacity (" + std::to_string(capacity) + ")"; - return -4; - } - std::memcpy(dest->data, src, len); - dest->data[len] = '\0'; - dest->length = static_cast(len); - return 0; - } catch (const std::exception& e) { - SetDiagnosticNoexcept(err, e.what()); - } catch (...) { - SetDiagnosticNoexcept(err, "Failed to copy pooled string"); - } - return -4; -} - -bool OperatorBizBridgeRegistry::RegisterBridge( - OperatorBizBridgeDescriptor desc) { - std::lock_guard lock(mutex_); - if (audited_) { - return false; - } - const auto reject = [&](std::initializer_list reason) { - RecordConflict(desc.biz_type, desc.adapter_name, reason); - return false; - }; - - int32_t key = static_cast(desc.biz_type); - if (key == 0 || desc.adapter_name.empty()) { - return reject( - {"Bridge requires a nonzero BizType and nonempty adapter_name"}); - } - auto it = bridges_by_biz_type_.find(key); - if (it != bridges_by_biz_type_.end()) { - if (it->second.registration_identity == desc.registration_identity && - it->second == desc) { - return true; - } - return reject({"Conflicting bridge registration '", - desc.registration_identity, "'; already registered by '", - it->second.registration_identity, "'"}); - } - - // 校验槽位命名唯一性与方向 - std::unordered_set in_names, in_suffixes; - for (const auto& s : desc.input_slots) { - if (s.logical_name.empty() || s.type_suffix.empty()) { - return reject( - {"Input slot requires nonempty logical_name and type_suffix; slot '", - s.logical_name, "', type '", s.type_suffix, "'"}); - } - if (!s.key_suffix.empty() || s.convert_output) { - return reject( - {"Input slot '", s.logical_name, - "' cannot declare output-only key_suffix or convert_output"}); - } - if (s.direction != IoDirection::kInput) { - return reject({"Input slot '", s.logical_name, "' (", s.type_suffix, - ") must have input direction"}); - } - if (!in_names.insert(s.logical_name).second || - !in_suffixes.insert(s.type_suffix).second) { - return reject({"Duplicate input slot name or type suffix: '", - s.logical_name, "' (", s.type_suffix, ")"}); - } - } - - std::unordered_set out_names, out_suffixes; - if (desc.output_slots.empty()) { - 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() || - s.key_suffix.empty()) { - return reject( - {"Output slot requires nonempty logical_name, type_suffix, and " - "key_suffix; slot '", - s.logical_name, "', type '", s.type_suffix, "'"}); - } - if (s.key_suffix.find('.') != std::string::npos) { - return reject( - {"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.key_suffix).second) { - return reject({"Duplicate output slot name or key suffix: '", - s.logical_name, "' (", s.key_suffix, ")"}); - } - if (!s.convert_output) { - return reject({"Output slot '", s.logical_name, - "' requires its own convert_output callback"}); - } - } - - if (!desc.convert_sample_input || !desc.create_shadow_output_dto) { - return reject( - {"Bridge requires convert_sample_input and create_shadow_output_dto " - "callbacks"}); - } - - bridges_by_biz_type_[key] = std::move(desc); - return true; -} - -const OperatorBizBridgeDescriptor* OperatorBizBridgeRegistry::GetBridge( - CompanyAlgBizType biz_type) const { - std::lock_guard lock(mutex_); - auto it = bridges_by_biz_type_.find(static_cast(biz_type)); - if (it != bridges_by_biz_type_.end()) { - return &it->second; - } - return nullptr; -} - -void OperatorBizBridgeRegistry::RecordConflict( - CompanyAlgBizType biz_type, std::string_view adapter_name, - std::initializer_list reason) noexcept { - if (has_conflict_) return; - has_conflict_ = true; - // A diagnostic allocation failure must not change a rejected registration - // into an exception or erase the first conflict with a later audit failure. - try { - conflict_diagnostic_ = "OperatorBizBridgeRegistry: adapter '"; - conflict_diagnostic_.append(adapter_name); - conflict_diagnostic_ += - "' (BizType " + std::to_string(static_cast(biz_type)) + "): "; - for (const auto part : reason) conflict_diagnostic_.append(part); - } catch (...) { - conflict_diagnostic_.clear(); - } -} - -int OperatorBizBridgeRegistry::ReportConflict( - std::string* diagnostic) const noexcept { - SetDiagnosticNoexcept( - diagnostic, - conflict_diagnostic_.empty() - ? std::string_view( - "OperatorBizBridgeRegistry registration or audit failed") - : std::string_view(conflict_diagnostic_)); - return -6; -} - -int OperatorBizBridgeRegistry::GlobalInit(std::string* diagnostic) { - std::lock_guard lock(mutex_); - SetDiagnosticNoexcept(diagnostic, ""); - if (has_conflict_) { - return ReportConflict(diagnostic); - } - if (audited_) { - return 0; - } - const auto reject = [&](CompanyAlgBizType biz_type, - std::string_view adapter_name, - std::initializer_list reason) { - RecordConflict(biz_type, adapter_name, reason); - return ReportConflict(diagnostic); - }; - - // 以实际 Adapter 注册快照为完整性事实源,新业务无需维护中央 ID 范围。 - const auto adapters = BizAdapterRegistry::Instance().GetAdaptersSnapshot(); - if (adapters.empty()) { - return reject(ALG_BIZ_TYPE_UNKNOWN, "", {"No registered BizAdapters"}); - } - - std::unordered_set adapter_biz_types; - adapter_biz_types.reserve(adapters.size()); - for (const auto& adapter : adapters) { - if (!adapter) { - return reject(ALG_BIZ_TYPE_UNKNOWN, "", {"Null registered BizAdapter"}); - } - const int32_t biz_type = static_cast(adapter->BizType()); - if (biz_type == static_cast(ALG_BIZ_TYPE_UNKNOWN) || - !adapter_biz_types.insert(biz_type).second) { - return reject(adapter->BizType(), adapter->AdapterName(), - {"BizAdapter has an unknown or duplicate BizType"}); - } - - auto bridge_it = bridges_by_biz_type_.find(biz_type); - if (bridge_it == bridges_by_biz_type_.end()) { - return reject(adapter->BizType(), adapter->AdapterName(), - {"Missing Operator bridge for registered BizAdapter"}); - } - const auto& desc = bridge_it->second; - if (desc.biz_type != adapter->BizType()) { - return reject(adapter->BizType(), adapter->AdapterName(), - {"Bridge BizType does not match its BizAdapter"}); - } - - const auto& adapter_desc = adapter->GetDescriptor(); - if (desc.internal_input_type_name != adapter_desc.input_type_name) { - return reject(desc.biz_type, desc.adapter_name, - {"Internal input type '", desc.internal_input_type_name, - "' does not match BizAdapter type '", - adapter_desc.input_type_name, "'"}); - } - if (desc.internal_output_type_name != adapter->ResultTypeName()) { - return reject(desc.biz_type, desc.adapter_name, - {"Internal output type '", desc.internal_output_type_name, - "' does not match BizAdapter result '", - adapter->ResultTypeName(), "'"}); - } - 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) { - if (slot.direction != IoDirection::kInput) { - return reject( - desc.biz_type, desc.adapter_name, - {"Input slot '", slot.logical_name, "' must have input direction"}); - } - const auto* binding = - OperatorValueTypeRegistry::Instance().GetBindingBySuffix( - slot.type_suffix); - if (!binding || binding->canonical_suffix != slot.type_suffix || - binding->direction != IoDirection::kInput || - !binding->validate_external) { - return reject(desc.biz_type, desc.adapter_name, - {"Input slot '", slot.logical_name, - "' requires canonical value type '", slot.type_suffix, - "' with input direction and validate_external"}); - } - } - for (const auto& slot : desc.output_slots) { - if (slot.direction != IoDirection::kOutput) { - return reject(desc.biz_type, desc.adapter_name, - {"Output slot '", slot.logical_name, - "' must have output direction"}); - } - const auto* binding = - OperatorValueTypeRegistry::Instance().GetBindingBySuffix( - slot.type_suffix); - if (!binding || binding->canonical_suffix != slot.type_suffix || - binding->direction != IoDirection::kOutput || - !binding->output_layout.compute_block_payload_bytes || - !binding->allocate_external || !binding->reset_external || - !binding->destroy_external) { - return reject(desc.biz_type, desc.adapter_name, - {"Output slot '", slot.logical_name, - "' requires canonical value type '", slot.type_suffix, - "' with output direction, layout and " - "allocation/reset/destroy callbacks"}); - } - } - } - - // 反向拒绝没有 Adapter 的孤儿 Bridge。 - for (const auto& [biz_type, desc] : bridges_by_biz_type_) { - if (adapter_biz_types.find(biz_type) == adapter_biz_types.end()) { - return reject(desc.biz_type, desc.adapter_name, - {"Bridge has no registered BizAdapter"}); - } - } - audited_ = true; - return 0; -} - -} // namespace llm_edgeflow diff --git a/src/adapter/operator/operator_biz_bridge_registry.h b/src/adapter/operator/operator_biz_bridge_registry.h deleted file mode 100644 index 378e8621..00000000 --- a/src/adapter/operator/operator_biz_bridge_registry.h +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "adapter/operator_biz_bridge.h" -#include "edgeflow/c_api.h" - -namespace llm_edgeflow { - -/** - * @brief Operator 业务桥接注册表 (SSOT 与自注册中心) - */ -class OperatorBizBridgeRegistry { - public: - static OperatorBizBridgeRegistry& Instance(); - - /** - * @brief 注册业务桥接描述符 (严格审计并在 Init 后冻结) - */ - bool RegisterBridge(OperatorBizBridgeDescriptor desc); - - /** - * @brief 获取业务桥接描述符 - */ - const OperatorBizBridgeDescriptor* GetBridge( - CompanyAlgBizType biz_type) const; - - /** - * @brief 全局初始化与一致性原子审计 (返回 -6 若存在任何冲突或缺漏) - */ - int GlobalInit(std::string* diagnostic = nullptr); - - /** - * @brief 检查是否存在冲突 - */ - bool HasConflict() const { - std::lock_guard lock(mutex_); - return has_conflict_; - } - - /** - * @brief 辅助函数:将 C 字符串安全复制至池化 CompanyString - */ - static int CopyToPooledString(const char* src, CompanyString* dest, - uint32_t capacity, const char* field_name, - std::string* err) noexcept; - - OperatorBizBridgeRegistry() = default; - - private: - void RecordConflict(CompanyAlgBizType biz_type, std::string_view adapter_name, - std::initializer_list reason) noexcept; - int ReportConflict(std::string* diagnostic) const noexcept; - - mutable std::mutex mutex_; - bool has_conflict_ = false; - bool audited_ = false; - std::string conflict_diagnostic_; - std::unordered_map bridges_by_biz_type_; -}; - -} // namespace llm_edgeflow diff --git a/src/adapter/operator/operator_config_resolver.cpp b/src/adapter/operator/operator_config_resolver.cpp index 1210b29e..c81ebb2e 100644 --- a/src/adapter/operator/operator_config_resolver.cpp +++ b/src/adapter/operator/operator_config_resolver.cpp @@ -6,7 +6,8 @@ #include #include -#include "adapter/biz_adapter_registry.h" +#include "adapter/deployment_io_config.h" +#include "adapter/io_binding_resolver.h" #include "adapter/operator/json_output_config_reader.h" #include "contracts/diagnostic.h" #include "contracts/path_utils.h" @@ -15,122 +16,6 @@ namespace llm_edgeflow { namespace { -int ResolveOutputAllocation(const nlohmann::json& config, - const OperatorBizSlot& slot, - ResolvedOutputPoolSpec* result, - std::string* parameter_text, std::string* error) { - if (!config.is_object()) { - if (error) *error = "Output allocation must be an object"; - return -2; - } - static const std::unordered_set fields = { - "type", "allocator", "params", - "meta_num", "metadata_type_id", "capacities"}; - for (const auto& [field, value] : config.items()) { - if (!fields.count(field)) { - if (error) *error = "Unknown output allocation field: " + field; - return -2; - } - } - if (!config.contains("type") || !config["type"].is_string()) { - if (error) *error = "Missing required 'type' string in output allocation"; - return -2; - } - ResolvedOutputPoolSpec requested; - requested.type = config["type"].get(); - if (requested.type != slot.type_suffix) { - if (error) - *error = "Output type '" + requested.type + "' does not match slot '" + - slot.logical_name + "'"; - return -2; - } - if (config.contains("allocator")) { - if (!config["allocator"].is_string() || - config["allocator"].get().empty()) { - if (error) *error = "Output allocator must be a nonempty string"; - return -2; - } - requested.allocator = config["allocator"].get(); - } - const auto* binding = OperatorValueTypeRegistry::Instance().GetOutputBinding( - requested.type, requested.allocator); - if (!binding) { - if (error) - *error = "No registered output allocator '" + requested.allocator + - "' for type '" + requested.type + "'"; - return -2; - } - const JsonOutputConfigReader reader(config); - if (!reader.Read(OutputConfigField::kParameters, parameter_text, error)) { - return -2; - } - if (!NormalizeOutputParameters(*binding, *parameter_text, &requested.params, - error)) { - return -2; - } - if (config.contains("meta_num")) { - if (!config["meta_num"].is_number_unsigned()) { - if (error) *error = "config.meta_num must be non-negative integer"; - return -2; - } - uint64_t mnum = config["meta_num"].get(); - if (mnum > std::numeric_limits::max()) { - if (error) *error = "config.meta_num exceeds uint32 range"; - return -2; - } - requested.meta_num = static_cast(mnum); - } - - if (config.contains("metadata_type_id")) { - if (config["metadata_type_id"].is_number_unsigned()) { - uint64_t uval = config["metadata_type_id"].get(); - if (uval > static_cast(std::numeric_limits::max())) { - if (error) *error = "config.metadata_type_id exceeds int32 range"; - return -2; - } - requested.metadata_type_id = static_cast(uval); - } else if (config["metadata_type_id"].is_number_integer()) { - int64_t ival = config["metadata_type_id"].get(); - if (ival < std::numeric_limits::min() || - ival > std::numeric_limits::max()) { - if (error) *error = "config.metadata_type_id exceeds int32 range"; - return -2; - } - requested.metadata_type_id = static_cast(ival); - } else { - if (error) *error = "config.metadata_type_id must be integer"; - return -2; - } - } - - if (config.contains("capacities")) { - if (!config["capacities"].is_object()) { - if (error) *error = "config.capacities must be an object"; - return -2; - } - for (const auto& [cap_field, cap_val] : config["capacities"].items()) { - if (!cap_val.is_number_unsigned()) { - if (error) { - *error = "Capacity for field '" + cap_field + - "' must be positive unsigned integer"; - } - return -2; - } - uint64_t uval = cap_val.get(); - if (uval == 0 || uval > std::numeric_limits::max()) { - if (error) { - *error = "Capacity for field '" + cap_field + - "' must fit a positive uint32"; - } - return -2; - } - requested.capacities[cap_field] = static_cast(uval); - } - } - - return ResolveOutputPoolSpec(*binding, requested, result, error) ? 0 : -2; -} - int ResolveContainedPath(const std::filesystem::path& canonical_root, const std::string& relative_value, const char* field_name, bool check_exists, @@ -218,7 +103,6 @@ int ResolveContainedPath(const std::filesystem::path& canonical_root, } } - // 组件级严格包含校验,杜绝前缀混淆 (/root/a vs /root/ab) 与 symlink 逃逸 if (!IsPathWithinRoot(canonical_root, canon_p)) { if (error_msg) { *error_msg = std::string(field_name) + @@ -269,6 +153,122 @@ int ResolveRequiredFileUnderRoot(const std::filesystem::path& canonical_root, } // namespace +int OperatorConfigResolver::ResolveOutputAllocation( + const nlohmann::json& config, const ExternalSlotDefinition& slot, + ResolvedOutputPoolSpec* result, std::string* parameter_text, + std::string* error) { + if (!config.is_object()) { + if (error) *error = "Output allocation must be an object"; + return -2; + } + static const std::unordered_set fields = { + "type", "allocator", "params", + "meta_num", "metadata_type_id", "capacities"}; + for (const auto& [field, value] : config.items()) { + if (!fields.count(field)) { + if (error) *error = "Unknown output allocation field: " + field; + return -2; + } + } + if (!config.contains("type") || !config["type"].is_string()) { + if (error) *error = "Missing required 'type' string in output allocation"; + return -2; + } + ResolvedOutputPoolSpec requested; + requested.type = config["type"].get(); + if (requested.type != slot.type_suffix) { + if (error) + *error = "Output type '" + requested.type + "' does not match slot '" + + slot.slot_name + "'"; + return -2; + } + if (config.contains("allocator")) { + if (!config["allocator"].is_string() || + config["allocator"].get().empty()) { + if (error) *error = "Output allocator must be a nonempty string"; + return -2; + } + requested.allocator = config["allocator"].get(); + } + const auto* binding = OperatorValueTypeRegistry::Instance().GetOutputBinding( + requested.type, requested.allocator); + if (!binding) { + if (error) + *error = "No registered output allocator '" + requested.allocator + + "' for type '" + requested.type + "'"; + return -2; + } + const JsonOutputConfigReader reader(config); + if (!reader.Read(OutputConfigField::kParameters, parameter_text, error)) { + return -2; + } + if (!NormalizeOutputParameters(*binding, *parameter_text, &requested.params, + error)) { + return -2; + } + if (config.contains("meta_num")) { + if (!config["meta_num"].is_number_unsigned()) { + if (error) *error = "config.meta_num must be non-negative integer"; + return -2; + } + uint64_t mnum = config["meta_num"].get(); + if (mnum > std::numeric_limits::max()) { + if (error) *error = "config.meta_num exceeds uint32 range"; + return -2; + } + requested.meta_num = static_cast(mnum); + } + + if (config.contains("metadata_type_id")) { + if (config["metadata_type_id"].is_number_unsigned()) { + uint64_t uval = config["metadata_type_id"].get(); + if (uval > static_cast(std::numeric_limits::max())) { + if (error) *error = "config.metadata_type_id exceeds int32 range"; + return -2; + } + requested.metadata_type_id = static_cast(uval); + } else if (config["metadata_type_id"].is_number_integer()) { + int64_t ival = config["metadata_type_id"].get(); + if (ival < std::numeric_limits::min() || + ival > std::numeric_limits::max()) { + if (error) *error = "config.metadata_type_id exceeds int32 range"; + return -2; + } + requested.metadata_type_id = static_cast(ival); + } else { + if (error) *error = "config.metadata_type_id must be integer"; + return -2; + } + } + + if (config.contains("capacities")) { + if (!config["capacities"].is_object()) { + if (error) *error = "config.capacities must be an object"; + return -2; + } + for (const auto& [cap_field, cap_val] : config["capacities"].items()) { + if (!cap_val.is_number_unsigned()) { + if (error) { + *error = "Capacity for field '" + cap_field + + "' must be positive unsigned integer"; + } + return -2; + } + uint64_t uval = cap_val.get(); + if (uval == 0 || uval > std::numeric_limits::max()) { + if (error) { + *error = "Capacity for field '" + cap_field + + "' must fit a positive uint32"; + } + return -2; + } + requested.capacities[cap_field] = static_cast(uval); + } + } + + return ResolveOutputPoolSpec(*binding, requested, result, error) ? 0 : -2; +} + int OperatorConfigResolver::ResolveModelReferenceUnderRoot( const std::filesystem::path& root, const std::string& rel_or_abs, const char* field_name, std::filesystem::path* out_path, @@ -353,290 +353,84 @@ int OperatorConfigResolver::Resolve(const char* model_path, return -2; } - // 统一沙箱解析 cfg_file_name (必须存在且为常规文件) + // 沙箱解析 cfg_file_name std::filesystem::path full_cfg; int ret = ResolveRequiredFileUnderRoot( canon_root, cfg_file_name, "cfg_file_name", &full_cfg, error_msg); if (ret != 0) return ret; - // 读取并解析 .conf JSON - std::ifstream conf_ifs(full_cfg); - if (!conf_ifs.is_open()) { - if (error_msg) *error_msg = "Cannot open conf file: " + full_cfg.string(); - return -2; - } - - nlohmann::json conf_json; - try { - conf_ifs >> conf_json; - } catch (const std::exception& e) { - if (error_msg) - *error_msg = "Invalid JSON in conf file: " + std::string(e.what()); - return -2; - } - - if (!conf_json.is_object()) { - if (error_msg) *error_msg = "Conf root must be a JSON object"; - return -2; - } - - if (conf_json.size() != 1 || !conf_json.contains("data") || - !conf_json["data"].is_object()) { - if (error_msg) { - *error_msg = - "Conf root must contain only the required object field 'data'"; - } - return -2; - } - const nlohmann::json* data_obj = &conf_json["data"]; - static const std::unordered_set kAllowedDataFields = { - "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) { - *error_msg = "Unknown field in conf data: '" + it.key() + "'"; - } - return -2; - } - } - - if (!data_obj->contains("pipe_path") || - !(*data_obj)["pipe_path"].is_string()) { - if (error_msg) *error_msg = "Missing or invalid 'pipe_path' in conf file"; - return -2; - } - - std::string pipe_rel = (*data_obj)["pipe_path"].get(); - std::filesystem::path full_pipe; - ret = ResolveRequiredFileUnderRoot(canon_root, pipe_rel, "pipe_path", - &full_pipe, error_msg); - if (ret != 0) return ret; - - std::ifstream pipe_ifs(full_pipe); - if (!pipe_ifs.is_open()) { - if (error_msg) - *error_msg = "Cannot open pipeline JSON file: " + full_pipe.string(); - return -2; - } - - nlohmann::json pipe_json; - try { - pipe_ifs >> pipe_json; - } catch (const std::exception& e) { - if (error_msg) - *error_msg = "Invalid JSON in pipeline file: " + std::string(e.what()); - return -2; - } - - std::string biz_name; - if (pipe_json.is_object() && pipe_json.contains("biz_name") && - pipe_json["biz_name"].is_string()) { - biz_name = pipe_json["biz_name"].get(); - } else { - if (error_msg) - *error_msg = "Pipeline JSON must contain string 'biz_name'"; - return -2; - } - - BizAdapterRegistry::AdapterLookupStatus lookup_status; - auto adapter = BizAdapterRegistry::Instance().GetAdapterByPipelineName( - biz_name, &lookup_status); - if (lookup_status == - BizAdapterRegistry::AdapterLookupStatus::kAmbiguousMatch) { - if (error_msg) { - *error_msg = "Ambiguous pipeline name '" + biz_name + "'"; - } - return -5; - } - if (!adapter) { - if (error_msg) { - *error_msg = "No registered BizAdapter for '" + biz_name + "'"; - } - return -5; - } - - const auto* bridge_desc = - OperatorBizBridgeRegistry::Instance().GetBridge(adapter->BizType()); - if (!bridge_desc) { - if (error_msg) { - *error_msg = "No OperatorBizBridgeDescriptor for BizType " + - std::to_string(adapter->BizType()); - } - return -5; - } - - if (!data_obj->contains("outputs")) { - if (error_msg) *error_msg = "Missing required 'outputs' object in conf"; - return -2; - } - if (!(*data_obj)["outputs"].is_object()) { - if (error_msg) - *error_msg = - "data.outputs must be an object keyed by logical output slot"; - 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 (!(*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 = (*data_obj)["outputs"][slot.logical_name]; - ResolvedOutputPoolSpec spec; - std::string parameter_text; - std::string allocation_error; - if (ResolveOutputAllocation(config, slot, &spec, ¶meter_text, - &allocation_error) != 0) { - if (error_msg) - *error_msg = "Invalid output allocation for slot '" + - slot.logical_name + "': " + allocation_error; - return -2; - } - pool_specs.emplace(slot.logical_name, std::move(spec)); - parameter_texts.emplace(slot.logical_name, std::move(parameter_text)); - } - - // 计算实际深度下的单句柄所有输出池总预算校验 (Checked Add/Multiply) - size_t total_handle_pool_bytes = 0; - for (const auto& out_slot : bridge_desc->output_slots) { - const auto& pool_spec = pool_specs.at(out_slot.logical_name); - const auto* output_binding = - OperatorValueTypeRegistry::Instance().GetOutputBinding( - out_slot.type_suffix, pool_spec.allocator); - if (!output_binding || - output_binding->direction != IoDirection::kOutput) { - if (error_msg) { - *error_msg = "Missing output value binding for suffix '" + - out_slot.type_suffix + "'"; - } - return -2; - } - size_t slot_pool_bytes = 0; - std::string budget_err; - if (!ComputeOutputPoolPayloadBytes(*output_binding, pool_spec, - effective_depth, &slot_pool_bytes, - &budget_err)) { - if (error_msg) { - *error_msg = "Output pool budget calculation failed: " + budget_err; - } - return -2; - } - if (!CheckedAdd(total_handle_pool_bytes, slot_pool_bytes, - &total_handle_pool_bytes)) { - if (error_msg) *error_msg = "Handle pool budget addition overflowed"; - return -2; - } - } - if (total_handle_pool_bytes > kMaxHandlePoolPayloadBytes) { - if (error_msg) { - *error_msg = "Total output pool payload (" + - std::to_string(total_handle_pool_bytes) + - " bytes) exceeds per-handle payload budget (" + - std::to_string(kMaxHandlePoolPayloadBytes) + " bytes)"; - } - return -2; - } - - // 1. 严格预检 Pipeline JSON 中的原始模型路径。模型最终文件可以尚未 - // 部署,但引用本身必须是非空相对路径且不能经现存 symlink 前缀逃逸。 - if (pipe_json.contains("models") && pipe_json["models"].is_array()) { - for (const auto& item : pipe_json["models"]) { - if (!item.contains("model_path")) continue; - if (!item["model_path"].is_string()) { + // 读取并解析部署配置文件 (Schema 1) + DeploymentIoConfig dep_config; + std::string dep_err; + if (!DeploymentIoConfig::ReadFromFile(full_cfg.string(), "operator", + &dep_config, &dep_err)) { + if (error_msg) *error_msg = dep_err; + return -2; + } + + // 解析接入绑定计划 + std::unique_ptr io_plan; + std::string plan_err; + int plan_ret = IoBindingResolver::ResolveFromConfig( + dep_config, "operator", canon_root.string(), &io_plan, &plan_err); + if (plan_ret != 0) { + if (error_msg) *error_msg = plan_err; + return plan_ret; + } + + // 如果有效深度不是默认深度,重新核对该深度下的总预算 + if (effective_depth != kDefaultOutputPoolDepth) { + size_t total_handle_pool_bytes = 0; + for (const auto& [slot_name, pool_spec] : + io_plan->operator_output_specs) { + const auto* output_binding = + OperatorValueTypeRegistry::Instance().GetOutputBinding( + pool_spec.type, pool_spec.allocator); + if (!output_binding || + output_binding->direction != IoDirection::kOutput) { if (error_msg) { - *error_msg = "Pipeline JSON model_path must be a string"; + *error_msg = "Missing output value binding for suffix '" + + pool_spec.type + "'"; } return -2; } - const std::string mp = item["model_path"].get(); - std::filesystem::path ignored; - ret = ResolveModelReferenceUnderRoot( - canon_root, mp, "pipeline model_path", &ignored, error_msg); - if (ret != 0) return ret; - } - } - - // 2. 解析与校验规范 model_id -> model_path 映射 - std::unordered_map map_overrides; - if (data_obj->contains("model_paths")) { - if (!(*data_obj)["model_paths"].is_object()) { - if (error_msg) *error_msg = "'model_paths' in conf must be an object"; - return -2; - } - for (const auto& [mid, mval] : (*data_obj)["model_paths"].items()) { - if (mid.empty() || !mval.is_string()) { - if (error_msg) *error_msg = "Invalid entry in 'model_paths'"; - return -2; - } - std::string mstr = mval.get(); - std::filesystem::path full_mpath; - ret = ResolveModelReferenceUnderRoot( - canon_root, mstr, "model_paths entry", &full_mpath, error_msg); - if (ret != 0) return ret; - - bool matched = false; - if (pipe_json.contains("models") && pipe_json["models"].is_array()) { - for (auto& item : pipe_json["models"]) { - if (item.contains("model_id") && item["model_id"] == mid) { - map_overrides[mid] = full_mpath.string(); - matched = true; - break; - } - } - } - if (!matched) { + size_t slot_pool_bytes = 0; + std::string budget_err; + if (!ComputeOutputPoolPayloadBytes(*output_binding, pool_spec, + effective_depth, &slot_pool_bytes, + &budget_err)) { if (error_msg) { - *error_msg = "Unknown model_id '" + mid + "' in 'model_paths'"; + *error_msg = "Output pool budget calculation failed: " + budget_err; } return -2; } + if (!CheckedAdd(total_handle_pool_bytes, slot_pool_bytes, + &total_handle_pool_bytes)) { + if (error_msg) *error_msg = "Handle pool budget addition overflowed"; + return -2; + } } - } - - // 3. 全量规范化模型路径 (将未覆盖项通过沙箱解析为绝对规范路径) - if (pipe_json.contains("models") && pipe_json["models"].is_array()) { - for (auto& item : pipe_json["models"]) { - std::string mid = - item.contains("model_id") && item["model_id"].is_string() - ? item["model_id"].get() - : ""; - if (!mid.empty() && map_overrides.find(mid) != map_overrides.end()) { - item["model_path"] = map_overrides[mid]; - } else if (item.contains("model_path") && - item["model_path"].is_string()) { - std::string mp = item["model_path"].get(); - std::filesystem::path full_mpath; - ret = ResolveModelReferenceUnderRoot( - canon_root, mp, "pipeline model_path", &full_mpath, error_msg); - if (ret != 0) return ret; - item["model_path"] = full_mpath.string(); + if (total_handle_pool_bytes > kMaxHandlePoolPayloadBytes) { + if (error_msg) { + *error_msg = "Total output pool payload (" + + std::to_string(total_handle_pool_bytes) + + " bytes) exceeds per-handle payload budget (" + + std::to_string(kMaxHandlePoolPayloadBytes) + " bytes)"; } + return -2; } } result->conf_path = full_cfg; - result->pipeline_path = full_pipe; + result->pipeline_path = dep_config.resolved_pipe_path; result->model_root_path = canon_root; - result->biz_name = biz_name; - result->biz_type = adapter->BizType(); - result->adapter = adapter; - result->bridge_descriptor = bridge_desc; - result->synthetic_pipeline_json = std::move(pipe_json); - result->output_pool_specs = std::move(pool_specs); - result->output_parameter_text = std::move(parameter_texts); + result->biz_name = io_plan->binding.biz_name; + result->io_binding = io_plan->binding.binding_id; + result->synthetic_pipeline_json = io_plan->resolved_pipeline_json; + result->output_pool_specs = io_plan->operator_output_specs; + result->output_parameter_text = io_plan->operator_output_parameter_texts; + result->input_limits = ResolvedInputLimits{}; + result->io_plan = std::move(io_plan); return 0; } catch (const std::exception& e) { diff --git a/src/adapter/operator/operator_config_resolver.h b/src/adapter/operator/operator_config_resolver.h index f82a05de..752d3fad 100644 --- a/src/adapter/operator/operator_config_resolver.h +++ b/src/adapter/operator/operator_config_resolver.h @@ -1,12 +1,11 @@ #pragma once #include -#include +#include #include #include -#include "adapter/biz_adapter_interface.h" -#include "adapter/operator/operator_biz_bridge_registry.h" +#include "adapter/io_binding_resolver.h" #include "adapter/operator/operator_value_type_registry.h" #include "edgeflow/operator/interface.h" #include "nlohmann/json.hpp" @@ -14,20 +13,17 @@ namespace llm_edgeflow { /** - * @brief 解析后的公司部署配置与合成 Pipeline JSON (v4 规范) + * @brief 解析后的公司部署配置与合成 Pipeline JSON */ struct ResolvedOperatorConfig { std::filesystem::path conf_path; std::filesystem::path pipeline_path; std::filesystem::path model_root_path; std::string biz_name; - CompanyAlgBizType biz_type = ALG_BIZ_TYPE_UNKNOWN; - std::shared_ptr adapter; - const OperatorBizBridgeDescriptor* bridge_descriptor = nullptr; + std::string io_binding; + std::unique_ptr io_plan; nlohmann::json synthetic_pipeline_json; std::unordered_map output_pool_specs; - // Text supplied to each structure's parser. Kept at the configuration/tooling - // boundary; it does not include defaults chosen inside custom parsers. std::unordered_map output_parameter_text; ResolvedInputLimits input_limits; }; @@ -37,19 +33,18 @@ struct ResolvedOperatorConfig { */ class OperatorConfigResolver { public: - /** - * @brief - * 校验并规范化模型引用路径(允许文件尚不存在,但严格限制在沙箱根目录下) - */ + static int ResolveOutputAllocation(const nlohmann::json& config, + const ExternalSlotDefinition& slot, + ResolvedOutputPoolSpec* result, + std::string* parameter_text, + std::string* error); + static int ResolveModelReferenceUnderRoot(const std::filesystem::path& root, const std::string& rel_or_abs, const char* field_name, std::filesystem::path* out_path, std::string* error_msg) noexcept; - /** - * @brief 基于 model_path 根目录与相对 cfg_file_name 解析配置 - */ static int Resolve(const char* model_path, const char* cfg_file_name, ResolvedOperatorConfig* result, std::string* error_msg, uint32_t max_frame_depth = 25) noexcept; diff --git a/src/adapter/operator/operator_output_pool.h b/src/adapter/operator/operator_output_pool.h index 6e097fa5..63e03273 100644 --- a/src/adapter/operator/operator_output_pool.h +++ b/src/adapter/operator/operator_output_pool.h @@ -117,8 +117,15 @@ class ScopedOutputLeaseGuard { void Reserve(size_t count) { leases_.reserve(count); } - void Track(std::shared_ptr pool, void* block) { - leases_.push_back({std::move(pool), block}); + void Track(const std::shared_ptr& pool, void* block) { + try { + leases_.push_back({pool, block}); + } catch (...) { + if (pool && block) { + pool->ReturnBlock(block); + } + throw; + } } void Untrack(void* block) noexcept { diff --git a/src/adapter/operator/operator_process_binding.cpp b/src/adapter/operator/operator_process_binding.cpp index 409ed45e..43f5bcf2 100644 --- a/src/adapter/operator/operator_process_binding.cpp +++ b/src/adapter/operator/operator_process_binding.cpp @@ -1,31 +1,38 @@ #include "adapter/operator/operator_process_binding.h" -#include #include #include namespace llm_edgeflow { -int ConvertOperatorInputs( +int ValidateAndExtractOperatorInputs( const llm_edgeflow::operator_api::NamedIoBatch& inputs, - const OperatorBizBridgeDescriptor& bridge, - const ResolvedInputLimits& limits, - ProcessLocalShadowStorage* shadow_storage, - std::vector* internal_dtos, std::string* error) { - if (!shadow_storage || !internal_dtos || - internal_dtos->size() != inputs.size()) { - if (error) *error = "Invalid input conversion destination"; + const InputConverterDefinition& in_conv, const ResolvedInputLimits& limits, + ExternalInputBatchView* out_view, std::string* error) { + if (!out_view) { + if (error) *error = "Null out_view pointer"; return -3; } + out_view->items = nullptr; + out_view->count = inputs.size(); + out_view->type_id = in_conv.external_type; + out_view->slots.clear(); + out_view->slot_types.clear(); + for (const auto& slot : in_conv.external_slots) { + if (slot.direction == PortDirection::kInput) { + out_view->slot_types[slot.slot_name] = slot.type_id; + } + } for (size_t i = 0; i < inputs.size(); ++i) { const auto& in_map = inputs[i]; - std::unordered_map slots_by_logical; std::unordered_set recognized_keys; - for (const auto& required_slot : bridge.input_slots) { + for (const auto& slot : in_conv.external_slots) { + if (slot.direction != PortDirection::kInput) continue; std::string found_key; - const void* payload = nullptr; + std::shared_ptr payload = nullptr; + for (const auto& [key, value] : in_map) { std::string suffix; if (!OperatorValueTypeRegistry::ParseKey(key, nullptr, &suffix)) { @@ -35,14 +42,13 @@ int ConvertOperatorInputs( } return -3; } - if (suffix != required_slot.type_suffix) { + if (suffix != slot.KeySuffix()) { continue; } if (!found_key.empty()) { if (error) { *error = "Duplicate input slot mapping for suffix '" + - required_slot.type_suffix + "' in frame " + - std::to_string(i); + slot.KeySuffix() + "' in frame " + std::to_string(i); } return -3; } @@ -51,15 +57,14 @@ int ConvertOperatorInputs( if (error) *error = "Null input shared_ptr for key: " + key; return -3; } - payload = value.get(); + payload = value; recognized_keys.insert(key); } - if (!payload && required_slot.required) { + if (!payload && slot.required) { if (error) { *error = "Missing required input slot for suffix '" + - required_slot.type_suffix + "' in frame " + - std::to_string(i); + slot.KeySuffix() + "' in frame " + std::to_string(i); } return -3; } @@ -67,20 +72,27 @@ int ConvertOperatorInputs( if (payload) { const auto* binding = OperatorValueTypeRegistry::Instance().GetBindingBySuffix( - required_slot.type_suffix); - if (binding && binding->validate_external) { - std::string validation_error; - const int validation_result = - binding->validate_external(payload, limits, &validation_error); - if (validation_result != 0) { - if (error) { - *error = "Validation failed for input key " + found_key + ": " + - validation_error; - } - return validation_result; + slot.type_suffix); + if (!binding || !binding->validate_external) { + if (error) { + *error = + "Missing Operator input ValueType binding or validate_external " + "for suffix '" + + slot.type_suffix + "'"; } + return -3; } - slots_by_logical[required_slot.logical_name] = payload; + std::string validation_error; + int validation_result = binding->validate_external( + payload.get(), limits, &validation_error); + if (validation_result != 0) { + if (error) { + *error = "Validation failed for input key " + found_key + ": " + + validation_error; + } + return validation_result; + } + out_view->slots[slot.slot_name].push_back(std::move(payload)); } } @@ -91,38 +103,31 @@ int ConvertOperatorInputs( } return -3; } - - std::string conversion_error; - const int conversion_result = - bridge.convert_sample_input(slots_by_logical, *shadow_storage, - &(*internal_dtos)[i], &conversion_error); - if (conversion_result != 0 || !(*internal_dtos)[i]) { - if (error) { - *error = "ConvertSampleInput failed in frame " + std::to_string(i) + - ": " + conversion_error; - } - return conversion_result != 0 ? conversion_result : -3; - } } + return 0; } int ResolveOperatorOutputs( const llm_edgeflow::operator_api::NamedIoBatch& outputs, - const OperatorBizBridgeDescriptor& bridge, + const OutputConverterDefinition& out_conv, std::vector>* frame_bindings, std::string* error) { if (!frame_bindings) { - if (error) *error = "Invalid output binding destination"; + if (error) *error = "Null frame_bindings pointer"; return -4; } - frame_bindings->assign(outputs.size(), {}); + frame_bindings->clear(); + frame_bindings->resize(outputs.size()); for (size_t i = 0; i < outputs.size(); ++i) { const auto& out_map = outputs[i]; std::unordered_set recognized_keys; - for (const auto& required_slot : bridge.output_slots) { + + for (const auto& slot : out_conv.external_slots) { + if (slot.direction != PortDirection::kOutput) continue; std::string found_key; + for (const auto& [key, value] : out_map) { std::string suffix; if (!OperatorValueTypeRegistry::ParseKey(key, nullptr, &suffix)) { @@ -132,39 +137,38 @@ int ResolveOperatorOutputs( } return -4; } - if (suffix != required_slot.KeySuffix()) { + if (suffix != slot.KeySuffix()) { continue; } if (!found_key.empty()) { if (error) { *error = "Duplicate output slot mapping for suffix '" + - required_slot.KeySuffix() + "' in frame " + - std::to_string(i); + slot.KeySuffix() + "' in frame " + std::to_string(i); } return -4; } found_key = key; - if (value && value.get()) { + if (value != nullptr) { if (error) { - *error = "Output slot key '" + key + - "' must be initialized to empty shared_ptr"; + *error = "Output shared_ptr must be null for key " + key + + " in frame " + std::to_string(i); } return -4; } recognized_keys.insert(key); } - if (found_key.empty() && required_slot.required) { + if (found_key.empty() && slot.required) { if (error) { - *error = "Missing required output slot key for suffix '" + - required_slot.KeySuffix() + "' in frame " + - std::to_string(i); + *error = "Missing required output slot for suffix '" + + slot.KeySuffix() + "' in frame " + std::to_string(i); } return -4; } + if (!found_key.empty()) { (*frame_bindings)[i].push_back( - {std::move(found_key), required_slot.logical_name}); + {found_key, slot.slot_name, slot.type_suffix}); } } @@ -176,6 +180,7 @@ int ResolveOperatorOutputs( return -4; } } + return 0; } @@ -186,41 +191,42 @@ int AcquireOperatorOutputBlocks( ScopedOutputLeaseGuard* lease_guard, std::vector* acquired_blocks, std::string* error) { if (!lease_guard || !acquired_blocks) { - if (error) *error = "Invalid output acquisition destination"; + if (error) *error = "Null destination in AcquireOperatorOutputBlocks"; return -4; } + acquired_blocks->clear(); size_t total_slots = 0; - for (const auto& frame : frame_bindings) total_slots += frame.size(); + for (const auto& fb : frame_bindings) { + total_slots += fb.size(); + } lease_guard->Reserve(total_slots); - acquired_blocks->clear(); acquired_blocks->reserve(total_slots); - for (size_t i = 0; i < frame_bindings.size(); ++i) { - for (const auto& binding : frame_bindings[i]) { - const auto pool_it = output_pools.find(binding.logical_name); - if (pool_it == output_pools.end() || !pool_it->second) { + for (size_t f = 0; f < frame_bindings.size(); ++f) { + for (const auto& binding : frame_bindings[f]) { + auto pit = output_pools.find(binding.logical_name); + if (pit == output_pools.end() || !pit->second) { if (error) { - *error = - "Output pool not found for logical slot: " + binding.logical_name; + *error = "Missing output pool for slot " + binding.logical_name; } - return -4; + return -5; } - void* raw_block = nullptr; - const int acquire_result = pool_it->second->Acquire(&raw_block); - if (acquire_result != 0 || !raw_block) { + auto pool = pit->second; + void* block = nullptr; + int acq_ret = pool->Acquire(&block); + if (acq_ret != 0 || !block) { if (error) { - *error = - "Failed to acquire output block from pool for logical slot " + - binding.logical_name; + *error = "Output pool exhausted for slot " + binding.logical_name; } return -4; } - lease_guard->Track(pool_it->second, raw_block); + lease_guard->Track(pool, block); acquired_blocks->push_back( - {i, binding.key, pool_it->second, raw_block, binding.logical_name}); + {f, binding.key, pool, block, binding.logical_name}); } } + return 0; } @@ -228,27 +234,22 @@ void PublishOperatorOutputs( const std::vector& acquired_blocks, llm_edgeflow::operator_api::NamedIoBatch* outputs, ScopedOutputLeaseGuard* lease_guard) { - struct PendingOutput { - std::shared_ptr* destination = nullptr; - std::shared_ptr value; - }; - std::vector pending_outputs; - pending_outputs.reserve(acquired_blocks.size()); - - for (const auto& acquired : acquired_blocks) { - OutputPoolDeleter deleter{acquired.pool, acquired.raw_block}; - auto value = std::shared_ptr(acquired.raw_block, deleter); - auto* destination = &(*outputs)[acquired.frame_idx][acquired.key]; - lease_guard->Untrack(acquired.raw_block); - pending_outputs.push_back({destination, std::move(value)}); + if (!outputs || !lease_guard) return; + + std::vector>>> + staged; + staged.reserve(acquired_blocks.size()); + for (const auto& acq : acquired_blocks) { + std::shared_ptr sp(acq.raw_block, + OutputPoolDeleter{acq.pool, acq.raw_block}); + staged.emplace_back(acq.frame_idx, std::make_pair(acq.key, std::move(sp))); } - lease_guard->Commit(); - for (auto& pending : pending_outputs) { - if (pending.destination) { - *pending.destination = std::move(pending.value); - } + for (auto& [frame_idx, kv] : staged) { + (*outputs)[frame_idx][kv.first] = std::move(kv.second); } + + lease_guard->Commit(); } } // namespace llm_edgeflow diff --git a/src/adapter/operator/operator_process_binding.h b/src/adapter/operator/operator_process_binding.h index 2bfcf4d0..1f77b58a 100644 --- a/src/adapter/operator/operator_process_binding.h +++ b/src/adapter/operator/operator_process_binding.h @@ -5,8 +5,9 @@ #include #include -#include "adapter/operator/operator_biz_bridge_registry.h" +#include "adapter/io_converter.h" #include "adapter/operator/operator_output_pool.h" +#include "adapter/operator/operator_value_type_registry.h" #include "edgeflow/operator/interface.h" namespace llm_edgeflow { @@ -14,6 +15,7 @@ namespace llm_edgeflow { struct FrameOutputBinding { std::string key; std::string logical_name; + std::string type_suffix; }; struct AcquiredOutputBlock { @@ -24,16 +26,14 @@ struct AcquiredOutputBlock { std::string logical_name; }; -int ConvertOperatorInputs( +int ValidateAndExtractOperatorInputs( const llm_edgeflow::operator_api::NamedIoBatch& inputs, - const OperatorBizBridgeDescriptor& bridge, - const ResolvedInputLimits& limits, - ProcessLocalShadowStorage* shadow_storage, - std::vector* internal_dtos, std::string* error); + const InputConverterDefinition& in_conv, const ResolvedInputLimits& limits, + ExternalInputBatchView* out_view, std::string* error); int ResolveOperatorOutputs( const llm_edgeflow::operator_api::NamedIoBatch& outputs, - const OperatorBizBridgeDescriptor& bridge, + const OutputConverterDefinition& out_conv, std::vector>* frame_bindings, std::string* error); diff --git a/src/adapter/output/README.md b/src/adapter/output/README.md new file mode 100644 index 00000000..942ef612 --- /dev/null +++ b/src/adapter/output/README.md @@ -0,0 +1,8 @@ +# Output Converters (输出转换器) + +本目录包含所有内部 Pipeline Blackboard 端口到外部输出协议的独立输出转换器。 + +## 规范与契约 +- 每个输出转换器通过 `REGISTER_OUTPUT_CONVERTER` 注册 `OutputConverterDefinition`。 +- 负责从 `AlgContext` 读取执行产物,校验基数与来源连续性(provenance),编码并安全写入已分配好的外部输出目标或 Operator 输出池。 +- 输出容量检查失败时返回缓冲区不足并标明约束,不静默截断字符串。 diff --git a/src/adapter/output/audio_result_output.cpp b/src/adapter/output/audio_result_output.cpp new file mode 100644 index 00000000..c6cddd1f --- /dev/null +++ b/src/adapter/output/audio_result_output.cpp @@ -0,0 +1,252 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/result_validation.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +int EncodeCAbiAudioResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* transcripts = + context->Read(bindings.GetActualKey("transcripts")); + if (!transcripts) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: transcripts", "transcripts", + options.converter_id.c_str()); + } + + const auto* intent_slots = + context->Read(bindings.GetActualKey("intent_slots")); + if (!intent_slots) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: intent_slots", "intent_slots", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + int count = static_cast(transcripts->size()); + int cap = static_cast(destination->capacity > 0 ? destination->capacity + : destination->count); + int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( + destination->items, &cap, count, options.converter_id.c_str(), status); + if (valid_ret != 0) return valid_ret; + + std::vector transcripts_by_request; + if (!IndexResults(transcripts, raw_req_ids, &transcripts_by_request, + "transcripts", options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + std::vector intent_slots_by_request; + if (!IndexResults(intent_slots, raw_req_ids, &intent_slots_by_request, + "intent_slots", options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (int i = 0; i < count; ++i) { + auto* out_ptr = destination->GetCAbi(i); + out_ptr->request_id = (*raw_req_ids)[i]; + out_ptr->status_code = intent_slots_by_request[i]->data.status_code; + + const std::string& slot_json = + intent_slots_by_request[i]->data.match_result_json; + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->transcribed_text, sizeof(out_ptr->transcribed_text), + transcripts_by_request[i]->data.c_str(), + "outputs[i].transcribed_text", i, options.converter_id.c_str(), + status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->intent_slot_json, sizeof(out_ptr->intent_slot_json), + slot_json.c_str(), "outputs[i].intent_slot_json", i, + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + } + + if (written_count) *written_count = static_cast(count); + return COMPANY_ALG_SUCCESS; +} + +int EncodeOperatorAudioResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* transcripts = + context->Read(bindings.GetActualKey("transcripts")); + if (!transcripts) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: transcripts", "transcripts", + options.converter_id.c_str()); + } + + const auto* intent_slots = + context->Read(bindings.GetActualKey("intent_slots")); + if (!intent_slots) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: intent_slots", "intent_slots", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + size_t count = transcripts->size(); + if (destination->count < count) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Destination item count is less than output count", + "destination", options.converter_id.c_str()); + } + + std::vector transcripts_by_request; + if (!IndexResults(transcripts, raw_req_ids, &transcripts_by_request, + "transcripts", options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + std::vector intent_slots_by_request; + if (!IndexResults(intent_slots, raw_req_ids, &intent_slots_by_request, + "intent_slots", options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (size_t i = 0; i < count; ++i) { + auto* out = + destination->GetSlot("audio_out", i); + if (!out) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing audio_out slot item", "audio_out", + options.converter_id.c_str(), static_cast(i)); + } + + out->request_id = (*raw_req_ids)[i]; + out->status_code = intent_slots_by_request[i]->data.status_code; + + const std::string& slot_json = + intent_slots_by_request[i]->data.match_result_json; + + std::string err; + int ret = CopyToOperatorString( + transcripts_by_request[i]->data.c_str(), out->transcribed_text, + destination->GetSlotCapacity("audio_out", "transcribed_text", 511), + "transcribed_text", &err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, + err.empty() ? "Buffer too small for transcribed_text" : err.c_str(), + "transcribed_text", options.converter_id.c_str(), + static_cast(i)); + } + + ret = CopyToOperatorString( + slot_json.c_str(), out->intent_slot_json, + destination->GetSlotCapacity("audio_out", "intent_slot_json", 1023), + "intent_slot_json", &err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, + err.empty() ? "Buffer too small for intent_slot_json" : err.c_str(), + "intent_slot_json", options.converter_id.c_str(), + static_cast(i)); + } + } + + if (written_count) *written_count = count; + return COMPANY_ALG_SUCCESS; +} + +OutputConverterDefinition MakeCAbiAudioResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "audio_result.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "audio_result.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyAudioOutputStruct"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"outputs", + "CompanyAudioOutputStruct", + PortDirection::kOutput, + true, + "CompanyAudioOutputStruct", + "", + {"transcribed_text", "intent_slot_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("transcripts", "TextBatch", true, "1:1"), + NodePortDefinition("intent_slots", "RuleMatchBatch", true, "1:1")}; + def.encode_fn = &EncodeCAbiAudioResult; + return def; +} + +OutputConverterDefinition MakeOperatorAudioResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "audio_result.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "audio_result.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyOperatorAudioOutput"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"audio_out", + "CompanyOperatorAudioOutput", + PortDirection::kOutput, + true, + "CompanyOperatorAudioOutput", + "audio_out", + {"transcribed_text", "intent_slot_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("transcripts", "TextBatch", true, "1:1"), + NodePortDefinition("intent_slots", "RuleMatchBatch", true, "1:1")}; + def.encode_fn = &EncodeOperatorAudioResult; + return def; +} + +REGISTER_OUTPUT_CONVERTER(MakeCAbiAudioResultOutputConverter()); +REGISTER_OUTPUT_CONVERTER(MakeOperatorAudioResultOutputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/output/audit_result_output.cpp b/src/adapter/output/audit_result_output.cpp new file mode 100644 index 00000000..db2561b4 --- /dev/null +++ b/src/adapter/output/audit_result_output.cpp @@ -0,0 +1,345 @@ +#include +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/result_validation.h" +#include "contracts/inference_payloads.h" +#include "core/common_contracts.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +int EncodeCAbiAuditResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* verdicts = context->Read( + bindings.GetActualKey("structured_verdicts")); + if (!verdicts) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: structured_verdicts", + "verdicts", options.converter_id.c_str()); + } + + const auto* matched_policies = + context->Read(bindings.GetActualKey("matched_policies")); + if (!matched_policies) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: matched_policies", + "matched_policies", options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + int count = static_cast(verdicts->size()); + int cap = static_cast(destination->capacity > 0 ? destination->capacity + : destination->count); + int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( + destination->items, &cap, count, options.converter_id.c_str(), status); + if (valid_ret != 0) return valid_ret; + + if (matched_policies->size() < static_cast(count)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "matched_policies count mismatch in AlgContext", + "matched_policies", options.converter_id.c_str()); + } + + std::vector verdicts_by_request; + if (!IndexResults(verdicts, raw_req_ids, &verdicts_by_request, "verdicts", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::vector matched_policies_by_request; + if (!IndexResults(matched_policies, raw_req_ids, &matched_policies_by_request, + "matched_policies", options.converter_id.c_str(), status, + true)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (int i = 0; i < count; ++i) { + auto* out_ptr = destination->GetCAbi(i); + out_ptr->request_id = (*raw_req_ids)[i]; + + const auto& verdict_item = verdicts_by_request[i]->data; + if (matched_policies_by_request[i]->data.rank != 1 || + !IsSuccessfulDocument(verdict_item) || + !verdict_item.structured_data.contains("risk_level") || + !verdict_item.structured_data.contains("risk_score") || + !verdict_item.structured_data["risk_level"].is_string() || + !verdict_item.structured_data["risk_score"].is_number()) { + return AdapterValidationHelper::ReturnInvalidInput( + status, + "structured_data missing or invalid risk_level/risk_score types", + "structured_verdicts", options.converter_id.c_str(), i); + } + + std::string risk_level = + verdict_item.structured_data["risk_level"].get(); + float risk_score = verdict_item.structured_data["risk_score"].get(); + if (!std::isfinite(risk_score) || risk_score < 0 || risk_score > 1 || + (risk_level != "SAFE" && risk_level != "LOW_RISK" && + risk_level != "MEDIUM_RISK" && risk_level != "HIGH_RISK")) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid risk level or score", "structured_verdicts", + options.converter_id.c_str(), i); + } + + const std::string& verdict_json = verdict_item.json_payload; + std::string policy_clause = matched_policies_by_request[i]->data.text; + + out_ptr->risk_score = risk_score; + out_ptr->status_code = 0; + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->risk_level, sizeof(out_ptr->risk_level), + risk_level.c_str(), "outputs[i].risk_level", i, + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->matched_policy_clause, + sizeof(out_ptr->matched_policy_clause), policy_clause.c_str(), + "outputs[i].matched_policy_clause", i, options.converter_id.c_str(), + status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->audit_verdict_json, sizeof(out_ptr->audit_verdict_json), + verdict_json.c_str(), "outputs[i].audit_verdict_json", i, + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + } + + if (written_count) *written_count = static_cast(count); + return COMPANY_ALG_SUCCESS; +} + +int EncodeOperatorAuditResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* verdicts = context->Read( + bindings.GetActualKey("structured_verdicts")); + if (!verdicts) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: structured_verdicts", + "verdicts", options.converter_id.c_str()); + } + + const auto* matched_policies = + context->Read(bindings.GetActualKey("matched_policies")); + if (!matched_policies) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: matched_policies", + "matched_policies", options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + size_t count = verdicts->size(); + if (destination->count < count) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Destination item count is less than output count", + "destination", options.converter_id.c_str()); + } + + if (matched_policies->size() < count) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "matched_policies count mismatch in AlgContext", + "matched_policies", options.converter_id.c_str()); + } + + std::vector verdicts_by_request; + if (!IndexResults(verdicts, raw_req_ids, &verdicts_by_request, "verdicts", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::vector matched_policies_by_request; + if (!IndexResults(matched_policies, raw_req_ids, &matched_policies_by_request, + "matched_policies", options.converter_id.c_str(), status, + true)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (size_t i = 0; i < count; ++i) { + auto* out = + destination->GetSlot("audit_out", i); + if (!out) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing audit_out slot item", "audit_out", + options.converter_id.c_str(), static_cast(i)); + } + + out->request_id = (*raw_req_ids)[i]; + + const auto& verdict_item = verdicts_by_request[i]->data; + if (matched_policies_by_request[i]->data.rank != 1 || + !IsSuccessfulDocument(verdict_item) || + !verdict_item.structured_data.contains("risk_level") || + !verdict_item.structured_data.contains("risk_score") || + !verdict_item.structured_data["risk_level"].is_string() || + !verdict_item.structured_data["risk_score"].is_number()) { + return AdapterValidationHelper::ReturnInvalidInput( + status, + "structured_data missing or invalid risk_level/risk_score types", + "structured_verdicts", options.converter_id.c_str(), + static_cast(i)); + } + + std::string risk_level = + verdict_item.structured_data["risk_level"].get(); + float risk_score = verdict_item.structured_data["risk_score"].get(); + if (!std::isfinite(risk_score) || risk_score < 0 || risk_score > 1 || + (risk_level != "SAFE" && risk_level != "LOW_RISK" && + risk_level != "MEDIUM_RISK" && risk_level != "HIGH_RISK")) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid risk level or score", "structured_verdicts", + options.converter_id.c_str(), static_cast(i)); + } + + const std::string& verdict_json = verdict_item.json_payload; + std::string policy_clause = matched_policies_by_request[i]->data.text; + + out->risk_score = risk_score; + out->status_code = 0; + + std::string err; + int ret = CopyToOperatorString( + risk_level.c_str(), out->risk_level, + destination->GetSlotCapacity("audit_out", "risk_level", 31), + "risk_level", &err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, err.empty() ? "Buffer too small for risk_level" : err.c_str(), + "risk_level", options.converter_id.c_str(), static_cast(i)); + } + + ret = CopyToOperatorString( + policy_clause.c_str(), out->matched_policy_clause, + destination->GetSlotCapacity("audit_out", "matched_policy_clause", 255), + "matched_policy_clause", &err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, + err.empty() ? "Buffer too small for matched_policy_clause" + : err.c_str(), + "matched_policy_clause", options.converter_id.c_str(), + static_cast(i)); + } + + ret = CopyToOperatorString( + verdict_json.c_str(), out->audit_verdict_json, + destination->GetSlotCapacity("audit_out", "audit_verdict_json", 1023), + "audit_verdict_json", &err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, + err.empty() ? "Buffer too small for audit_verdict_json" : err.c_str(), + "audit_verdict_json", options.converter_id.c_str(), + static_cast(i)); + } + } + + if (written_count) *written_count = count; + return COMPANY_ALG_SUCCESS; +} + +OutputConverterDefinition MakeCAbiAuditResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "audit_result.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "audit_result.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyAuditOutputStruct"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = { + {"outputs", + "CompanyAuditOutputStruct", + PortDirection::kOutput, + true, + "CompanyAuditOutputStruct", + "", + {"risk_level", "matched_policy_clause", "audit_verdict_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("structured_verdicts", "StructuredDocumentBatch", true, + "1:1"), + NodePortDefinition("matched_policies", "RankedTextBatch", true, "N:1")}; + def.encode_fn = &EncodeCAbiAuditResult; + return def; +} + +OutputConverterDefinition MakeOperatorAuditResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "audit_result.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "audit_result.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyOperatorAuditOutput"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = { + {"audit_out", + "CompanyOperatorAuditOutput", + PortDirection::kOutput, + true, + "CompanyOperatorAuditOutput", + "audit_out", + {"risk_level", "matched_policy_clause", "audit_verdict_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("structured_verdicts", "StructuredDocumentBatch", true, + "1:1"), + NodePortDefinition("matched_policies", "RankedTextBatch", true, "N:1")}; + def.encode_fn = &EncodeOperatorAuditResult; + return def; +} + +REGISTER_OUTPUT_CONVERTER(MakeCAbiAuditResultOutputConverter()); +REGISTER_OUTPUT_CONVERTER(MakeOperatorAuditResultOutputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/output/doc_answer_output.cpp b/src/adapter/output/doc_answer_output.cpp new file mode 100644 index 00000000..bdd6fde0 --- /dev/null +++ b/src/adapter/output/doc_answer_output.cpp @@ -0,0 +1,270 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/result_validation.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +int EncodeCAbiDocAnswer(AlgContext* context, const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* answers = + context->Read(bindings.GetActualKey("llm_answers")); + if (!answers) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: llm_answers", "answers", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + const auto* intent_matches = + context->Read(bindings.GetActualKey("intent_matches")); + if (!intent_matches) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: intent_matches", + "intent_matches", options.converter_id.c_str()); + } + + const auto* chunk_counts = + context->Read(bindings.GetActualKey("doc_chunk_counts")); + if (!chunk_counts) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: doc_chunk_counts", + "doc_chunk_counts", options.converter_id.c_str()); + } + + int count = static_cast(answers->size()); + int cap = static_cast(destination->capacity > 0 ? destination->capacity + : destination->count); + int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( + destination->items, &cap, count, options.converter_id.c_str(), status); + if (valid_ret != 0) return valid_ret; + + std::vector answers_by_req; + std::vector intents_by_req; + std::vector chunks_by_req; + + if (!IndexResults(answers, raw_req_ids, &answers_by_req, "answers", + options.converter_id.c_str(), status) || + !IndexResults(intent_matches, raw_req_ids, &intents_by_req, + "intent_matches", options.converter_id.c_str(), status) || + !IndexResults(chunk_counts, raw_req_ids, &chunks_by_req, + "doc_chunk_counts", options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (int i = 0; i < count; ++i) { + auto* out_ptr = destination->GetCAbi(i); + out_ptr->request_id = (*raw_req_ids)[i]; + + const auto& match = intents_by_req[i]->data; + out_ptr->confidence = match.score; + out_ptr->chunk_count = chunks_by_req[i]->data; + out_ptr->status_code = match.status_code; + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->intent_name, sizeof(out_ptr->intent_name), + match.category.c_str(), "outputs[i].intent_name", i, + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->answer_text, sizeof(out_ptr->answer_text), + answers_by_req[i]->data.c_str(), "outputs[i].answer_text", i, + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + } + + if (written_count) *written_count = static_cast(count); + return COMPANY_ALG_SUCCESS; +} + +int EncodeOperatorDocAnswer(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* answers = + context->Read(bindings.GetActualKey("llm_answers")); + if (!answers) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: llm_answers", "answers", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + const auto* intent_matches = + context->Read(bindings.GetActualKey("intent_matches")); + if (!intent_matches) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: intent_matches", + "intent_matches", options.converter_id.c_str()); + } + + const auto* chunk_counts = + context->Read(bindings.GetActualKey("doc_chunk_counts")); + if (!chunk_counts) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: doc_chunk_counts", + "doc_chunk_counts", options.converter_id.c_str()); + } + + size_t count = answers->size(); + if (destination->count < count) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Destination item count is less than output count", + "destination", options.converter_id.c_str()); + } + + std::vector answers_by_req; + std::vector intents_by_req; + std::vector chunks_by_req; + + if (!IndexResults(answers, raw_req_ids, &answers_by_req, "answers", + options.converter_id.c_str(), status) || + !IndexResults(intent_matches, raw_req_ids, &intents_by_req, + "intent_matches", options.converter_id.c_str(), status) || + !IndexResults(chunk_counts, raw_req_ids, &chunks_by_req, + "doc_chunk_counts", options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (size_t i = 0; i < count; ++i) { + auto* out = destination->GetSlot("doc_out", i); + if (!out) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing doc_out slot item", "doc_out", + options.converter_id.c_str(), static_cast(i)); + } + + out->request_id = (*raw_req_ids)[i]; + const auto& match = intents_by_req[i]->data; + out->confidence = match.score; + out->chunk_count = chunks_by_req[i]->data; + out->status_code = match.status_code; + + std::string err; + int ret = CopyToOperatorString( + match.category.c_str(), out->intent_name, + destination->GetSlotCapacity("doc_out", "intent_name", 63), + "intent_name", &err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, + err.empty() ? "Buffer too small for intent_name" : err.c_str(), + "intent_name", options.converter_id.c_str(), static_cast(i)); + } + + ret = CopyToOperatorString( + answers_by_req[i]->data.c_str(), out->answer_text, + destination->GetSlotCapacity("doc_out", "answer_text", 1023), + "answer_text", &err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, + err.empty() ? "Buffer too small for answer_text" : err.c_str(), + "answer_text", options.converter_id.c_str(), static_cast(i)); + } + } + + if (written_count) *written_count = count; + return COMPANY_ALG_SUCCESS; +} + +OutputConverterDefinition MakeCAbiDocAnswerOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "doc_answer.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "doc_answer.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyDocOutputStruct"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"outputs", + "CompanyDocOutputStruct", + PortDirection::kOutput, + true, + "CompanyDocOutputStruct", + "", + {"intent_name", "answer_text"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("llm_answers", "TextBatch", true, "1:1"), + NodePortDefinition("intent_matches", "RuleMatchBatch", true, "1:1"), + NodePortDefinition("doc_chunk_counts", "Int32Batch", true, "1:1")}; + def.encode_fn = &EncodeCAbiDocAnswer; + return def; +} + +OutputConverterDefinition MakeOperatorDocAnswerOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "doc_answer.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "doc_answer.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyOperatorDocOutput"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"doc_out", + "CompanyOperatorDocOutput", + PortDirection::kOutput, + true, + "CompanyOperatorDocOutput", + "doc_out", + {"intent_name", "answer_text"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("llm_answers", "TextBatch", true, "1:1"), + NodePortDefinition("intent_matches", "RuleMatchBatch", true, "1:1"), + NodePortDefinition("doc_chunk_counts", "Int32Batch", true, "1:1")}; + def.encode_fn = &EncodeOperatorDocAnswer; + return def; +} + +REGISTER_OUTPUT_CONVERTER(MakeCAbiDocAnswerOutputConverter()); +REGISTER_OUTPUT_CONVERTER(MakeOperatorDocAnswerOutputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/output/invoice_result_output.cpp b/src/adapter/output/invoice_result_output.cpp new file mode 100644 index 00000000..ea706286 --- /dev/null +++ b/src/adapter/output/invoice_result_output.cpp @@ -0,0 +1,245 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/result_validation.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +int EncodeCAbiInvoiceResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* invoice_jsons = context->Read( + bindings.GetActualKey("extracted_invoice_json")); + if (!invoice_jsons) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: extracted_invoice_json", + "extracted_invoice_json", options.converter_id.c_str()); + } + + const auto* ocr_docs = + context->Read(bindings.GetActualKey("ocr_docs")); + if (!ocr_docs) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: ocr_docs", "ocr_docs", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + int count = static_cast(invoice_jsons->size()); + int cap = static_cast(destination->capacity > 0 ? destination->capacity + : destination->count); + int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( + destination->items, &cap, count, options.converter_id.c_str(), status); + if (valid_ret != 0) return valid_ret; + + std::vector + invoice_jsons_by_request; + if (!IndexResults(invoice_jsons, raw_req_ids, &invoice_jsons_by_request, + "invoice_jsons", options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + std::vector ocr_docs_by_request; + if (!IndexResults(ocr_docs, raw_req_ids, &ocr_docs_by_request, "ocr_docs", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (int i = 0; i < count; ++i) { + auto* out_ptr = destination->GetCAbi(i); + out_ptr->request_id = (*raw_req_ids)[i]; + out_ptr->detected_box_count = + static_cast(ocr_docs_by_request[i]->data.boxes.size()); + if (!IsSuccessfulDocument(invoice_jsons_by_request[i]->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Structured result failed or used fallback", "invoice_jsons", + options.converter_id.c_str(), i); + } + out_ptr->status_code = 0; + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->extracted_invoice_json, + sizeof(out_ptr->extracted_invoice_json), + invoice_jsons_by_request[i]->data.json_payload.c_str(), + "outputs[i].extracted_invoice_json", i, + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + } + + if (written_count) *written_count = static_cast(count); + return COMPANY_ALG_SUCCESS; +} + +int EncodeOperatorInvoiceResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* invoice_jsons = context->Read( + bindings.GetActualKey("extracted_invoice_json")); + if (!invoice_jsons) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: extracted_invoice_json", + "extracted_invoice_json", options.converter_id.c_str()); + } + + const auto* ocr_docs = + context->Read(bindings.GetActualKey("ocr_docs")); + if (!ocr_docs) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Missing required context value: ocr_docs", "ocr_docs", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + size_t count = invoice_jsons->size(); + if (destination->count < count) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Destination item count is less than output count", + "destination", options.converter_id.c_str()); + } + + std::vector + invoice_jsons_by_request; + if (!IndexResults(invoice_jsons, raw_req_ids, &invoice_jsons_by_request, + "invoice_jsons", options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + std::vector ocr_docs_by_request; + if (!IndexResults(ocr_docs, raw_req_ids, &ocr_docs_by_request, "ocr_docs", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (size_t i = 0; i < count; ++i) { + auto* out = destination->GetSlot("od_out", i); + if (!out) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing od_out slot item", "od_out", + options.converter_id.c_str(), static_cast(i)); + } + + out->request_id = (*raw_req_ids)[i]; + out->detected_box_count = + static_cast(ocr_docs_by_request[i]->data.boxes.size()); + if (!IsSuccessfulDocument(invoice_jsons_by_request[i]->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Structured result failed or used fallback", "invoice_jsons", + options.converter_id.c_str(), static_cast(i)); + } + out->status_code = 0; + + std::string err; + int ret = CopyToOperatorString( + invoice_jsons_by_request[i]->data.json_payload.c_str(), + out->result_json, + destination->GetSlotCapacity("od_out", "result_json", 1023), + "result_json", &err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, + err.empty() ? "Buffer too small for result_json" : err.c_str(), + "result_json", options.converter_id.c_str(), static_cast(i)); + } + } + + if (written_count) *written_count = count; + return COMPANY_ALG_SUCCESS; +} + +OutputConverterDefinition MakeCAbiInvoiceResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "invoice_result.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "invoice_result.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyOcrDocOutputStruct"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"outputs", + "CompanyOcrDocOutputStruct", + PortDirection::kOutput, + true, + "CompanyOcrDocOutputStruct", + "", + {"extracted_invoice_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("extracted_invoice_json", "StructuredDocumentBatch", + true, "1:1"), + NodePortDefinition("ocr_docs", "OcrDocumentBatch", true, "1:1")}; + def.encode_fn = &EncodeCAbiInvoiceResult; + return def; +} + +OutputConverterDefinition MakeOperatorInvoiceResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "invoice_result.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "invoice_result.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyOdOutput"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"od_out", + "CompanyOdOutput", + PortDirection::kOutput, + true, + "CompanyOdOutput", + "od_out", + {"result_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("extracted_invoice_json", "StructuredDocumentBatch", + true, "1:1"), + NodePortDefinition("ocr_docs", "OcrDocumentBatch", true, "1:1")}; + def.encode_fn = &EncodeOperatorInvoiceResult; + return def; +} + +REGISTER_OUTPUT_CONVERTER(MakeCAbiInvoiceResultOutputConverter()); +REGISTER_OUTPUT_CONVERTER(MakeOperatorInvoiceResultOutputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/output/keyword_result_output.cpp b/src/adapter/output/keyword_result_output.cpp new file mode 100644 index 00000000..44fbe1f8 --- /dev/null +++ b/src/adapter/output/keyword_result_output.cpp @@ -0,0 +1,193 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/biz_results.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/result_validation.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +int EncodeCAbiKeywordResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* res = + context->Read(bindings.GetActualKey("rule_matches")); + if (!res) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: rule_matches", "res", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + int count = static_cast(res->size()); + int cap = static_cast(destination->capacity > 0 ? destination->capacity + : destination->count); + int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( + destination->items, &cap, count, options.converter_id.c_str(), status); + if (valid_ret != 0) return valid_ret; + + std::vector res_by_request; + if (!IndexResults(res, raw_req_ids, &res_by_request, "res", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (int i = 0; i < count; ++i) { + auto* out_ptr = destination->GetCAbi(i); + out_ptr->request_id = (*raw_req_ids)[i]; + out_ptr->is_hit = res_by_request[i]->data.is_hit; + out_ptr->status_code = res_by_request[i]->data.status_code; + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->match_result_json, sizeof(out_ptr->match_result_json), + res_by_request[i]->data.match_result_json.c_str(), + "outputs[i].match_result_json", i, options.converter_id.c_str(), + status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + } + + if (written_count) *written_count = static_cast(count); + return COMPANY_ALG_SUCCESS; +} + +int EncodeOperatorKeywordResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* res = + context->Read(bindings.GetActualKey("rule_matches")); + if (!res) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: rule_matches", "res", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + size_t count = res->size(); + std::vector res_by_request; + if (!IndexResults(res, raw_req_ids, &res_by_request, "res", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::string diag_err; + for (size_t i = 0; i < count; ++i) { + auto* out = + destination->GetSlot("keyword_out", i); + if (!out) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing keyword_out slot block in output view", + "keyword_out", options.converter_id.c_str(), static_cast(i)); + } + out->request_id = (*raw_req_ids)[i]; + out->is_hit = res_by_request[i]->data.is_hit; + out->status_code = res_by_request[i]->data.status_code; + + uint32_t cap = + destination->GetSlotCapacity("keyword_out", "match_result_json", 2047); + int ret = CopyToOperatorString( + res_by_request[i]->data.match_result_json.c_str(), + out->match_result_json, cap, "match_result_json", &diag_err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, diag_err.c_str(), "match_result_json", + options.converter_id.c_str(), static_cast(i)); + } + } + + if (written_count) *written_count = count; + return COMPANY_ALG_SUCCESS; +} + +OutputConverterDefinition MakeCAbiKeywordResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "keyword.result.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "keyword.result.response"; + def.schema_version = 1; + def.external_type = "CompanyKeywordOutputStruct"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"match_result_json", + "CompanyKeywordOutputStruct", + PortDirection::kOutput, + true, + "CompanyKeywordOutputStruct", + "", + {"match_result_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("rule_matches", "RuleMatchBatch", true, "1:1")}; + def.encode_fn = &EncodeCAbiKeywordResult; + return def; +} + +OutputConverterDefinition MakeOperatorKeywordResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "keyword.result.operator.v1"; + def.transport = "operator"; + def.schema_id = "keyword.result.response"; + def.schema_version = 1; + def.external_type = "CompanyOperatorKeywordOutput"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"keyword_out", + "CompanyOperatorKeywordOutput", + PortDirection::kOutput, + true, + "CompanyOperatorKeywordOutput", + "keyword_out", + {"match_result_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("rule_matches", "RuleMatchBatch", true, "1:1")}; + def.encode_fn = &EncodeOperatorKeywordResult; + return def; +} + +REGISTER_OUTPUT_CONVERTER(MakeCAbiKeywordResultOutputConverter()); +REGISTER_OUTPUT_CONVERTER(MakeOperatorKeywordResultOutputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/output/rerank_result_output.cpp b/src/adapter/output/rerank_result_output.cpp new file mode 100644 index 00000000..ff549a8a --- /dev/null +++ b/src/adapter/output/rerank_result_output.cpp @@ -0,0 +1,237 @@ +#include +#include +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/result_validation.h" +#include "contracts/inference_payloads.h" +#include "core/common_contracts.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +int EncodeCAbiRerankResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* res = + context->Read(bindings.GetActualKey("ranked_results")); + if (!res) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: ranked_results", + "ranked_results", options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + std::vector first; + if (!IndexResults(res, raw_req_ids, &first, "ranked_results", + options.converter_id.c_str(), status, true)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::unordered_map> req_map; + for (const auto& item : *res) { + req_map[item.req_id].push_back(item.data); + } + + for (auto& entry : req_map) { + auto& list = entry.second; + std::sort(list.begin(), list.end(), + [](const auto& a, const auto& b) { return a.rank < b.rank; }); + for (size_t k = 0; k < list.size(); ++k) { + if (list.size() > 8 || list[k].rank != static_cast(k + 1) || + list[k].original_sub_id >= 8) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid ranked result", "ranked_results", + options.converter_id.c_str()); + } + } + } + + int count = static_cast(raw_req_ids->size()); + int cap = static_cast(destination->capacity > 0 ? destination->capacity + : destination->count); + int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( + destination->items, &cap, count, options.converter_id.c_str(), status); + if (valid_ret != 0) return valid_ret; + + for (int i = 0; i < count; ++i) { + auto* out_ptr = destination->GetCAbi(i); + out_ptr->request_id = (*raw_req_ids)[i]; + + const auto& cand_list = req_map[static_cast(i)]; + int item_cnt = std::min(static_cast(cand_list.size()), 8); + out_ptr->count = item_cnt; + out_ptr->status_code = 0; + + for (int k = 0; k < item_cnt; ++k) { + out_ptr->scores[k] = cand_list[k].score; + out_ptr->sorted_indices[k] = + static_cast(cand_list[k].original_sub_id); + } + } + + if (written_count) *written_count = static_cast(count); + return COMPANY_ALG_SUCCESS; +} + +int EncodeOperatorRerankResult(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* res = + context->Read(bindings.GetActualKey("ranked_results")); + if (!res) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: ranked_results", + "ranked_results", options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + std::vector first; + if (!IndexResults(res, raw_req_ids, &first, "ranked_results", + options.converter_id.c_str(), status, true)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::unordered_map> req_map; + for (const auto& item : *res) { + req_map[item.req_id].push_back(item.data); + } + + for (auto& entry : req_map) { + auto& list = entry.second; + std::sort(list.begin(), list.end(), + [](const auto& a, const auto& b) { return a.rank < b.rank; }); + for (size_t k = 0; k < list.size(); ++k) { + if (list.size() > 8 || list[k].rank != static_cast(k + 1) || + list[k].original_sub_id >= 8) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Invalid ranked result", "ranked_results", + options.converter_id.c_str()); + } + } + } + + size_t count = raw_req_ids->size(); + if (destination->count < count) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Destination count is less than output count", "destination", + options.converter_id.c_str()); + } + + for (size_t i = 0; i < count; ++i) { + auto* out = + destination->GetSlot("rerank_out", i); + if (!out) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing rerank_out slot item", "rerank_out", + options.converter_id.c_str(), static_cast(i)); + } + + out->request_id = (*raw_req_ids)[i]; + const auto& cand_list = req_map[static_cast(i)]; + int item_cnt = std::min(static_cast(cand_list.size()), 8); + out->count = item_cnt; + out->status_code = 0; + + for (int k = 0; k < item_cnt; ++k) { + out->scores[k] = cand_list[k].score; + out->sorted_indices[k] = static_cast(cand_list[k].original_sub_id); + } + } + + if (written_count) *written_count = count; + return COMPANY_ALG_SUCCESS; +} + +OutputConverterDefinition MakeCAbiRerankResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "rerank_result.plain.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "rerank_result.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyRerankBatchOutputStruct"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"outputs", + "CompanyRerankBatchOutputStruct", + PortDirection::kOutput, + true, + "CompanyRerankBatchOutputStruct", + "", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("ranked_results", "RankedTextBatch", true, "N:1")}; + def.encode_fn = &EncodeCAbiRerankResult; + return def; +} + +OutputConverterDefinition MakeOperatorRerankResultOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "rerank_result.plain.operator.v1"; + def.transport = "operator"; + def.schema_id = "rerank_result.plain.response"; + def.schema_version = 1; + def.external_type = "CompanyOperatorRerankOutput"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"rerank_out", + "CompanyOperatorRerankOutput", + PortDirection::kOutput, + true, + "CompanyOperatorRerankOutput", + "rerank_out", + {}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("ranked_results", "RankedTextBatch", true, "N:1")}; + def.encode_fn = &EncodeOperatorRerankResult; + return def; +} + +REGISTER_OUTPUT_CONVERTER(MakeCAbiRerankResultOutputConverter()); +REGISTER_OUTPUT_CONVERTER(MakeOperatorRerankResultOutputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/output/structured_document_output.cpp b/src/adapter/output/structured_document_output.cpp new file mode 100644 index 00000000..3aef8327 --- /dev/null +++ b/src/adapter/output/structured_document_output.cpp @@ -0,0 +1,204 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/biz_results.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/result_validation.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { +namespace { + +int EncodeCAbiStructuredDocument(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* res = context->Read( + bindings.GetActualKey("extracted_entities")); + if (!res) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: extracted_entities", "res", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + int count = static_cast(res->size()); + int cap = static_cast(destination->capacity > 0 ? destination->capacity + : destination->count); + int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( + destination->items, &cap, count, options.converter_id.c_str(), status); + if (valid_ret != 0) return valid_ret; + + std::vector res_by_request; + if (!IndexResults(res, raw_req_ids, &res_by_request, "res", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + for (int i = 0; i < count; ++i) { + auto* out_ptr = destination->GetCAbi(i); + out_ptr->request_id = (*raw_req_ids)[i]; + if (!IsSuccessfulDocument(res_by_request[i]->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Structured result failed or used fallback", "res", + options.converter_id.c_str(), i); + } + out_ptr->status_code = 0; + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->entities_json, sizeof(out_ptr->entities_json), + res_by_request[i]->data.json_payload.c_str(), + "outputs[i].entities_json", i, options.converter_id.c_str(), + status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + } + + if (written_count) *written_count = static_cast(count); + return COMPANY_ALG_SUCCESS; +} + +int EncodeOperatorStructuredDocument(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, + AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* res = context->Read( + bindings.GetActualKey("extracted_entities")); + if (!res) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: extracted_entities", "res", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + size_t count = res->size(); + std::vector res_by_request; + if (!IndexResults(res, raw_req_ids, &res_by_request, "res", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::string diag_err; + for (size_t i = 0; i < count; ++i) { + auto* out = + destination->GetSlot("entity_out", i); + if (!out) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing entity_out slot block in output view", "entity_out", + options.converter_id.c_str(), static_cast(i)); + } + out->request_id = (*raw_req_ids)[i]; + if (!IsSuccessfulDocument(res_by_request[i]->data)) { + return AdapterValidationHelper::ReturnInvalidInput( + status, "Structured result failed or used fallback", "res", + options.converter_id.c_str(), static_cast(i)); + } + out->status_code = 0; + + uint32_t cap = + destination->GetSlotCapacity("entity_out", "entities_json", 2047); + int ret = CopyToOperatorString(res_by_request[i]->data.json_payload.c_str(), + out->entities_json, cap, "entities_json", + &diag_err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, diag_err.c_str(), "entities_json", + options.converter_id.c_str(), static_cast(i)); + } + } + + if (written_count) *written_count = count; + return COMPANY_ALG_SUCCESS; +} + +OutputConverterDefinition MakeCAbiStructuredDocumentOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "document.structured.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "document.structured.response"; + def.schema_version = 1; + def.external_type = "CompanyEntityOutputStruct"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"entities_json", + "CompanyEntityOutputStruct", + PortDirection::kOutput, + true, + "CompanyEntityOutputStruct", + "", + {"entities_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("extracted_entities", "StructuredDocumentBatch", true, + "1:1")}; + def.encode_fn = &EncodeCAbiStructuredDocument; + return def; +} + +OutputConverterDefinition MakeOperatorStructuredDocumentOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "document.structured.operator.v1"; + def.transport = "operator"; + def.schema_id = "document.structured.response"; + def.schema_version = 1; + def.external_type = "CompanyOperatorEntityOutput"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"entity_out", + "CompanyOperatorEntityOutput", + PortDirection::kOutput, + true, + "CompanyOperatorEntityOutput", + "entity_out", + {"entities_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("extracted_entities", "StructuredDocumentBatch", true, + "1:1")}; + def.encode_fn = &EncodeOperatorStructuredDocument; + return def; +} + +REGISTER_OUTPUT_CONVERTER(MakeCAbiStructuredDocumentOutputConverter()); +REGISTER_OUTPUT_CONVERTER(MakeOperatorStructuredDocumentOutputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/output/translation_json_output.cpp b/src/adapter/output/translation_json_output.cpp new file mode 100644 index 00000000..2e0cd3c3 --- /dev/null +++ b/src/adapter/output/translation_json_output.cpp @@ -0,0 +1,200 @@ +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/result_validation.h" +#include "contracts/inference_payloads.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" +#include "nlohmann/json.hpp" + +namespace llm_edgeflow { +namespace { + +int EncodeCAbiTranslationJson(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* res = + context->Read(bindings.GetActualKey("llm_answers")); + if (!res) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: llm_answers", "res", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + int count = static_cast(res->size()); + std::vector res_by_request; + if (!IndexResults(res, raw_req_ids, &res_by_request, "res", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::vector payloads(count); + for (int i = 0; i < count; ++i) { + nlohmann::json response = {{"translated", res_by_request[i]->data}}; + payloads[i] = response.dump(); + } + + int cap = static_cast(destination->capacity > 0 ? destination->capacity + : destination->count); + int valid_ret = AdapterValidationHelper::ValidateBatchOutputs( + destination->items, &cap, count, options.converter_id.c_str(), status); + if (valid_ret != 0) return valid_ret; + + for (int i = 0; i < count; ++i) { + auto* out_ptr = destination->GetCAbi(i); + out_ptr->request_id = (*raw_req_ids)[i]; + out_ptr->status_code = 0; + + if (!AdapterValidationHelper::CheckedStringCopy( + out_ptr->entities_json, sizeof(out_ptr->entities_json), + payloads[i].c_str(), "outputs[i].entities_json", i, + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; + } + } + + if (written_count) *written_count = static_cast(count); + return COMPANY_ALG_SUCCESS; +} + +int EncodeOperatorTranslationJson(AlgContext* context, + const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, + AdapterStatus* status) { + if (!context) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Null AlgContext passed to Encode", "context", + options.converter_id.c_str()); + } + + const auto* res = + context->Read(bindings.GetActualKey("llm_answers")); + if (!res) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: llm_answers", "res", + options.converter_id.c_str()); + } + + const auto* raw_req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + if (!raw_req_ids) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing required context value: raw_request_ids", + "raw_request_ids", options.converter_id.c_str()); + } + + size_t count = res->size(); + std::vector res_by_request; + if (!IndexResults(res, raw_req_ids, &res_by_request, "res", + options.converter_id.c_str(), status)) { + return COMPANY_ALG_ERR_INVALID_INPUT; + } + + std::string diag_err; + for (size_t i = 0; i < count; ++i) { + auto* out = + destination->GetSlot("entity_out", i); + if (!out) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, "Missing entity_out slot block in output view", "entity_out", + options.converter_id.c_str(), static_cast(i)); + } + out->request_id = (*raw_req_ids)[i]; + out->status_code = 0; + + nlohmann::json response = {{"translated", res_by_request[i]->data}}; + std::string payload = response.dump(); + + uint32_t cap = + destination->GetSlotCapacity("entity_out", "entities_json", 2047); + int ret = CopyToOperatorString(payload.c_str(), out->entities_json, cap, + "entities_json", &diag_err); + if (ret != 0) { + return AdapterValidationHelper::ReturnBufferTooSmall( + status, diag_err.c_str(), "entities_json", + options.converter_id.c_str(), static_cast(i)); + } + } + + if (written_count) *written_count = count; + return COMPANY_ALG_SUCCESS; +} + +OutputConverterDefinition MakeCAbiTranslationJsonOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "translate.json.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "translate.json.response"; + def.schema_version = 1; + def.external_type = "CompanyEntityOutputStruct"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"entities_json", + "CompanyEntityOutputStruct", + PortDirection::kOutput, + true, + "CompanyEntityOutputStruct", + "", + {"entities_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("llm_answers", "TextBatch", true, "1:1")}; + def.encode_fn = &EncodeCAbiTranslationJson; + return def; +} + +OutputConverterDefinition MakeOperatorTranslationJsonOutputConverter() { + OutputConverterDefinition def; + def.converter_id = "translate.json.operator.v1"; + def.transport = "operator"; + def.schema_id = "translate.json.response"; + def.schema_version = 1; + def.external_type = "CompanyOperatorEntityOutput"; + def.cardinality = "1:1"; + def.max_batch_size = 64; + def.capacity_policy = "reject_overflow"; + def.thread_model = "stateless"; + def.external_slots = {{"entity_out", + "CompanyOperatorEntityOutput", + PortDirection::kOutput, + true, + "CompanyOperatorEntityOutput", + "entity_out", + {"entities_json"}}}; + def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("llm_answers", "TextBatch", true, "1:1")}; + def.encode_fn = &EncodeOperatorTranslationJson; + return def; +} + +REGISTER_OUTPUT_CONVERTER(MakeCAbiTranslationJsonOutputConverter()); +REGISTER_OUTPUT_CONVERTER(MakeOperatorTranslationJsonOutputConverter()); + +} // namespace +} // namespace llm_edgeflow diff --git a/src/adapter/shared_algorithm_runtime.cpp b/src/adapter/shared_algorithm_runtime.cpp index 1571a403..90df0a09 100644 --- a/src/adapter/shared_algorithm_runtime.cpp +++ b/src/adapter/shared_algorithm_runtime.cpp @@ -3,54 +3,27 @@ #include #include -#include "adapter/biz_adapter_registry.h" +#include "adapter/adapter_status.h" +#include "adapter/adapter_validation_helper.h" #include "adapter/deployment_model_resolver.h" -#include "adapter/operator/operator_biz_bridge_registry.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter_registry.h" #include "adapter/operator/operator_value_type_registry.h" #include "contracts/diagnostic.h" #include "core/alg_context.h" #include "core/diagnostic_code.h" #include "core/node_registry.h" +#include "core/pipeline_validator.h" #include "core/session_context.h" #include "edgeflow/log.h" #include "engine/backend_registry.h" #include "engine/model_registry.h" namespace llm_edgeflow { -namespace { - -bool ReadPipelineJson(const std::string& config_path, - nlohmann::json* pipeline_json, std::string* diagnostic) { - std::ifstream stream(config_path); - if (!stream.is_open()) { - if (diagnostic) *diagnostic = "Failed to open config file: " + config_path; - return false; - } - try { - stream >> *pipeline_json; - return true; - } catch (const std::exception& exception) { - if (diagnostic) { - *diagnostic = - "JSON parse exception in " + config_path + ": " + exception.what(); - } - return false; - } -} - -} // namespace int SharedAlgorithmRuntime::GlobalInit() noexcept { try { - // 1. BizAdapterRegistry 冲突审计 - if (BizAdapterRegistry::Instance().HasRegistrationConflict()) { - ALG_LOG_ERROR( - "[SharedAlgorithmRuntime] GlobalInit failed: Registration conflict " - "in BizAdapterRegistry.\n"); - return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 - } - - // 2. NodeRegistry 冲突审计 + // 1. NodeRegistry 冲突审计 if (NodeRegistry::Instance().HasConflict()) { ALG_LOG_ERROR( "[SharedAlgorithmRuntime] GlobalInit failed: Registration conflict " @@ -58,7 +31,7 @@ int SharedAlgorithmRuntime::GlobalInit() noexcept { return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 } - // 3. Model/Backend Registry 冲突审计 + // 2. Model/Backend Registry 冲突审计 if (ModelRegistry::Instance().HasConflict() || BackendRegistry::Instance().HasConflict()) { ALG_LOG_ERROR( @@ -67,7 +40,7 @@ int SharedAlgorithmRuntime::GlobalInit() noexcept { return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 } - // 4. OperatorValueTypeRegistry 冲突审计 + // 3. OperatorValueTypeRegistry 冲突审计 if (OperatorValueTypeRegistry::Instance().HasConflict()) { ALG_LOG_ERROR( "[SharedAlgorithmRuntime] GlobalInit failed: Registration conflict " @@ -75,11 +48,31 @@ int SharedAlgorithmRuntime::GlobalInit() noexcept { return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 } - // 5. OperatorBizBridgeRegistry 冲突审计 - if (OperatorBizBridgeRegistry::Instance().HasConflict()) { + // 4. IoConverterRegistry 冲突审计 + if (IoConverterRegistry::Instance().HasConflict()) { + ALG_LOG_ERROR( + "[SharedAlgorithmRuntime] GlobalInit failed: Registration conflict " + "in IoConverterRegistry.\n"); + return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 + } + + // 5. IoBindingRegistry 冲突审计 + if (IoBindingRegistry::Instance().HasConflict()) { ALG_LOG_ERROR( "[SharedAlgorithmRuntime] GlobalInit failed: Registration conflict " - "in OperatorBizBridgeRegistry.\n"); + "in IoBindingRegistry.\n"); + return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 + } + + // 6. IoBinding 全量接入审计 + std::vector audit_errors; + if (!IoBindingRegistry::Instance().Audit(&audit_errors)) { + ALG_LOG_ERROR( + "[SharedAlgorithmRuntime] GlobalInit failed: IoBindingRegistry Audit " + "failed:\n"); + for (const auto& err : audit_errors) { + ALG_LOG_ERROR(" - %s\n", err.c_str()); + } return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 } @@ -103,7 +96,7 @@ int SharedAlgorithmRuntime::GlobalDeinit() noexcept { int SharedAlgorithmRuntime::CreateFromConfigFile( const std::string& config_path, int device_id, - const std::string& model_root_dir, CompanyAlgBizType biz_type, + const std::string& model_root_dir, std::unique_ptr* out_runtime, std::string* out_error) noexcept { try { @@ -113,80 +106,22 @@ int SharedAlgorithmRuntime::CreateFromConfigFile( } *out_runtime = nullptr; - if (biz_type == ALG_BIZ_TYPE_UNKNOWN) { - if (out_error) *out_error = "Cannot create with ALG_BIZ_TYPE_UNKNOWN"; - return COMPANY_ALG_ERR_UNSUPPORTED_BIZ; // -5 - } - - auto adapter = BizAdapterRegistry::Instance().GetAdapter(biz_type); - if (!adapter) { - if (out_error) { - *out_error = - "Unsupported or unregistered biz_type: " + std::to_string(biz_type); - } - return COMPANY_ALG_ERR_UNSUPPORTED_BIZ; // -5 - } - if (config_path.empty()) { if (out_error) *out_error = "Empty config_file_path"; return COMPANY_ALG_ERR_INVALID_PARAM; // -2 } - nlohmann::json pipeline_json; - std::string deployment_error; - if (!ReadPipelineJson(config_path, &pipeline_json, &deployment_error)) { - if (out_error) *out_error = deployment_error; - return COMPANY_ALG_ERR_INVALID_INPUT; - } - nlohmann::json resolved_pipeline_json; - if (!ResolveDeploymentModelPaths(pipeline_json, model_root_dir, - &resolved_pipeline_json, - &deployment_error)) { - if (out_error) *out_error = deployment_error; - return COMPANY_ALG_ERR_INVALID_INPUT; + std::unique_ptr io_plan; + std::string resolve_err; + int ret = IoBindingResolver::ResolveFromFile( + config_path, "cabi", model_root_dir, &io_plan, &resolve_err); + if (ret != 0) { + if (out_error) *out_error = resolve_err; + return ret; } - auto runtime = std::make_unique(); - runtime->biz_type_ = biz_type; - runtime->adapter_ = adapter; - runtime->pipeline_ = std::make_unique(); - - RuntimeOptions options; - options.device_id = device_id; - options.has_device_id = (device_id >= 0); - options.biz_type = static_cast(biz_type); - options.biz_name = adapter->AdapterName(); - - runtime->pipeline_->GetSessionContext().SetRuntimeOptions(options); - - PipelineDiagnostic diagnostic; - if (!runtime->pipeline_->BuildFromJson(resolved_pipeline_json, - &diagnostic)) { - if (out_error) { - *out_error = - "Failed to build pipeline from config: " + diagnostic.message + - " (code: " + std::string(DiagnosticCodeName(diagnostic.code)) + - ", path: " + diagnostic.path + ")"; - } - if (diagnostic.code == DiagnosticCode::kRegistryConflict) { - return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 - } - // 保持 main 既有纯 C ABI 契约:只要 BuildFromConfigFile 失败, - // 文件打开、JSON 解析和配置语义错误均返回 -3。 - return COMPANY_ALG_ERR_INVALID_INPUT; // -3 - } - - if (!adapter->ValidatePipelineBinding(runtime->pipeline_->GetBizName())) { - if (out_error) { - *out_error = "Pipeline biz_name '" + runtime->pipeline_->GetBizName() + - "' does not match adapter '" + adapter->AdapterName() + - "'"; - } - return COMPANY_ALG_ERR_UNSUPPORTED_BIZ; // -5 - } - - *out_runtime = std::move(runtime); - return COMPANY_ALG_SUCCESS; + return CreateFromIoPlan(std::move(io_plan), device_id, nullptr, out_runtime, + out_error); } catch (const std::exception& e) { SetDiagnosticNoexcept(out_error, e.what()); return COMPANY_ALG_ERR_EXCEPTION; @@ -198,7 +133,7 @@ int SharedAlgorithmRuntime::CreateFromConfigFile( int SharedAlgorithmRuntime::CreateFromPipelineJson( const nlohmann::json& pipeline_json, int device_id, - const std::string& model_root_dir, CompanyAlgBizType biz_type, + const std::string& model_root_dir, const std::string& binding_id, std::unique_ptr* out_runtime, std::string* out_error, const RuntimeOptions* extra_runtime_options) noexcept { @@ -209,33 +144,51 @@ int SharedAlgorithmRuntime::CreateFromPipelineJson( } *out_runtime = nullptr; - if (biz_type == ALG_BIZ_TYPE_UNKNOWN) { - if (out_error) *out_error = "Cannot create with ALG_BIZ_TYPE_UNKNOWN"; - return COMPANY_ALG_ERR_UNSUPPORTED_BIZ; // -5 + if (binding_id.empty()) { + if (out_error) *out_error = "binding_id must not be empty"; + return COMPANY_ALG_ERR_INVALID_PARAM; // -2 } - auto adapter = BizAdapterRegistry::Instance().GetAdapter(biz_type); - if (!adapter) { - if (out_error) { - *out_error = - "Unsupported or unregistered biz_type: " + std::to_string(biz_type); - } - return COMPANY_ALG_ERR_UNSUPPORTED_BIZ; // -5 + std::unique_ptr io_plan; + std::string resolve_err; + int ret = IoBindingResolver::ResolveFromPipelineJson( + pipeline_json, binding_id, "cabi", model_root_dir, &io_plan, + &resolve_err); + if (ret != 0) { + if (out_error) *out_error = resolve_err; + return COMPANY_ALG_ERR_INVALID_PARAM; // -2 + } + + return CreateFromIoPlan(std::move(io_plan), device_id, + extra_runtime_options, out_runtime, out_error); + } catch (const std::exception& e) { + SetDiagnosticNoexcept(out_error, e.what()); + return COMPANY_ALG_ERR_EXCEPTION; + } catch (...) { + SetDiagnosticNoexcept(out_error, "Unknown exception"); + return COMPANY_ALG_ERR_UNKNOWN; + } +} + +int SharedAlgorithmRuntime::CreateFromIoPlan( + std::unique_ptr io_plan, int device_id, + const RuntimeOptions* extra_runtime_options, + std::unique_ptr* out_runtime, + std::string* out_error) noexcept { + try { + if (!out_runtime) { + if (out_error) *out_error = "Null out_runtime pointer"; + return COMPANY_ALG_ERR_INVALID_HANDLE; // -1 } + *out_runtime = nullptr; - nlohmann::json resolved_pipeline_json; - std::string deployment_error; - if (!ResolveDeploymentModelPaths(pipeline_json, model_root_dir, - &resolved_pipeline_json, - &deployment_error)) { - if (out_error) *out_error = deployment_error; - return COMPANY_ALG_ERR_INVALID_PARAM; + if (!io_plan || !io_plan->input_converter || !io_plan->output_converter || + !io_plan->pipeline_plan) { + if (out_error) *out_error = "Invalid or incomplete ValidatedIoPlan"; + return COMPANY_ALG_ERR_INVALID_PARAM; // -2 } - auto runtime = std::make_unique(); - runtime->biz_type_ = biz_type; - runtime->adapter_ = adapter; - runtime->pipeline_ = std::make_unique(); + auto pipeline = std::make_unique(); RuntimeOptions options; if (extra_runtime_options) { @@ -243,34 +196,28 @@ int SharedAlgorithmRuntime::CreateFromPipelineJson( } options.device_id = device_id; options.has_device_id = (device_id >= 0); - options.biz_type = static_cast(biz_type); - options.biz_name = adapter->AdapterName(); + options.biz_name = io_plan->binding.biz_name; - runtime->pipeline_->GetSessionContext().SetRuntimeOptions(options); + pipeline->GetSessionContext().SetRuntimeOptions(options); PipelineDiagnostic diagnostic; - if (!runtime->pipeline_->BuildFromJson(resolved_pipeline_json, - &diagnostic)) { + if (!pipeline->BuildFromPlan(std::move(io_plan->pipeline_plan), + &diagnostic)) { if (out_error) { *out_error = - "Failed to build pipeline from JSON: " + diagnostic.message + + "Failed to build pipeline from plan: " + diagnostic.message + " (code: " + std::string(DiagnosticCodeName(diagnostic.code)) + ", path: " + diagnostic.path + ")"; } if (diagnostic.code == DiagnosticCode::kRegistryConflict) { return COMPANY_ALG_ERR_REGISTRY_CONFLICT; // -6 } - return COMPANY_ALG_ERR_INVALID_PARAM; // -2 + return COMPANY_ALG_ERR_INVALID_INPUT; // -3 } - if (!adapter->ValidatePipelineBinding(runtime->pipeline_->GetBizName())) { - if (out_error) { - *out_error = "Pipeline biz_name '" + runtime->pipeline_->GetBizName() + - "' does not match adapter '" + adapter->AdapterName() + - "'"; - } - return COMPANY_ALG_ERR_UNSUPPORTED_BIZ; // -5 - } + auto runtime = std::make_unique(); + runtime->io_plan_ = std::move(io_plan); + runtime->pipeline_ = std::move(pipeline); *out_runtime = std::move(runtime); return COMPANY_ALG_SUCCESS; @@ -285,42 +232,62 @@ int SharedAlgorithmRuntime::CreateFromPipelineJson( int SharedAlgorithmRuntime::ExecuteBatch(const void** inputs, int num_inputs, void** outputs, int* num_outputs, - std::string* out_error, - bool operator_results) noexcept { + std::string* out_error) noexcept { try { - if (!adapter_) { - if (out_error) *out_error = "Null business adapter in runtime instance"; - return COMPANY_ALG_ERR_UNSUPPORTED_BIZ; // -5 + if (!io_plan_ || !pipeline_) { + if (out_error) + *out_error = "Null IO plan or pipeline in runtime instance"; + return COMPANY_ALG_ERR_INVALID_HANDLE; // -1 } // 1. 批大小与槽位容量预检 - int preflight_ret = - adapter_->ValidateBatch(inputs, num_inputs, outputs, num_outputs); + int preflight_ret = AdapterValidationHelper::ValidateBatchPreFlight( + inputs, num_inputs, outputs, num_outputs, + static_cast(io_plan_->effective_max_batch_size), num_inputs, + io_plan_->binding.binding_id.c_str()); if (preflight_ret != 0) { if (out_error) { *out_error = "ValidateBatch preflight failed with code " + std::to_string(preflight_ret); } + if (num_outputs && *num_outputs >= 0 && + preflight_ret != COMPANY_ALG_ERR_BUFFER_TOO_SMALL) { + *num_outputs = 0; + } return preflight_ret; } // 2. 解包到 AlgContext 请求黑板 AlgContext req_ctx; - AdapterStatus unpack_status; - int unpack_ret = - adapter_->Unpack(inputs, num_inputs, &req_ctx, &unpack_status); - if (unpack_ret != 0) { + ExternalInputBatchView in_view; + in_view.items = inputs; + in_view.count = static_cast(num_inputs); + in_view.type_id = io_plan_->input_converter->external_type; + + InputDecodeOptions in_options; + in_options.binding_id = io_plan_->binding.binding_id; + in_options.converter_id = io_plan_->input_converter->converter_id; + in_options.transport = "cabi"; + in_options.max_batch_size = io_plan_->effective_max_batch_size; + + AdapterStatus decode_status; + int decode_ret = io_plan_->input_converter->decode_fn( + in_view, in_options, io_plan_->input_port_bindings, &req_ctx, + &decode_status); + if (decode_ret != 0) { + if (num_outputs) *num_outputs = 0; if (out_error) { - *out_error = "Unpack failed for " + - std::string(adapter_->AdapterName()) + ": " + - unpack_status.ToString(); + *out_error = "DecodeInput failed for " + + io_plan_->input_converter->converter_id + ": " + + decode_status.ToString(); } - return unpack_ret; + return decode_ret; } // 3. 执行 Pipeline DAG 计算 int exec_ret = pipeline_->Execute(&req_ctx); if (exec_ret != 0) { + if (num_outputs) *num_outputs = 0; if (out_error) { *out_error = "Pipeline::Execute failed with code " + std::to_string(exec_ret) + ": " + @@ -330,26 +297,57 @@ int SharedAlgorithmRuntime::ExecuteBatch(const void** inputs, int num_inputs, } // 4. 打包回 C 结构体输出 - AdapterStatus pack_status; - int pack_ret = - operator_results - ? adapter_->PackResultBatch(&req_ctx, outputs, num_outputs, - &pack_status) - : adapter_->Pack(&req_ctx, outputs, num_outputs, &pack_status); - if (pack_ret != 0) { + ExternalOutputBatchView out_view; + out_view.items = outputs; + out_view.count = static_cast(num_inputs); + out_view.capacity = static_cast(*num_outputs); + out_view.type_id = io_plan_->output_converter->external_type; + + OutputEncodeOptions out_options; + out_options.binding_id = io_plan_->binding.binding_id; + out_options.converter_id = io_plan_->output_converter->converter_id; + out_options.transport = "cabi"; + out_options.max_batch_size = io_plan_->effective_max_batch_size; + + size_t written_count = 0; + AdapterStatus encode_status; + int encode_ret = io_plan_->output_converter->encode_fn( + &req_ctx, io_plan_->output_port_bindings, out_options, &out_view, + &written_count, &encode_status); + if (encode_ret != 0) { + if (encode_ret == COMPANY_ALG_ERR_BUFFER_TOO_SMALL || + encode_status.Code() == COMPANY_ALG_ERR_BUFFER_TOO_SMALL) { + if (num_outputs) *num_outputs = num_inputs; + } else { + if (num_outputs) *num_outputs = 0; + } if (out_error) { - *out_error = "Pack failed for " + std::string(adapter_->AdapterName()) + - ": " + pack_status.ToString(); + *out_error = "EncodeOutput failed for " + + io_plan_->output_converter->converter_id + ": " + + encode_status.ToString(); + } + return encode_ret; + } + + if (written_count != static_cast(num_inputs)) { + if (num_outputs) *num_outputs = 0; + if (out_error) { + *out_error = + "EncodeOutput written count (" + std::to_string(written_count) + + ") does not match input count (" + std::to_string(num_inputs) + ")"; } - return pack_ret; + return COMPANY_ALG_ERR_UNKNOWN; } + if (num_outputs) *num_outputs = static_cast(written_count); return COMPANY_ALG_SUCCESS; } catch (const std::exception& e) { SetDiagnosticNoexcept(out_error, e.what()); + if (num_outputs) *num_outputs = 0; return COMPANY_ALG_ERR_EXCEPTION; } catch (...) { SetDiagnosticNoexcept(out_error, "Unknown exception"); + if (num_outputs) *num_outputs = 0; return COMPANY_ALG_ERR_UNKNOWN; } } diff --git a/src/adapter/shared_algorithm_runtime.h b/src/adapter/shared_algorithm_runtime.h index f5a993b3..b4f1690b 100644 --- a/src/adapter/shared_algorithm_runtime.h +++ b/src/adapter/shared_algorithm_runtime.h @@ -3,17 +3,17 @@ #include #include -#include "adapter/biz_adapter_interface.h" +#include "adapter/io_binding_resolver.h" +#include "adapter/io_converter.h" #include "core/pipeline.h" #include "core/session_context.h" #include "edgeflow/c_api.h" -#include "nlohmann/json.hpp" namespace llm_edgeflow { /** - * @brief 纯 C ABI 与 C++ 平台 Operator 门面共享的内部算法运行时句柄 (接入适配层 - * 内部) + * @brief 纯 C ABI 与 C++ 平台 Operator 门面共享的内部算法运行时句柄 + * (接入适配层内部) */ class SharedAlgorithmRuntime { public: @@ -35,31 +35,39 @@ class SharedAlgorithmRuntime { static int GlobalDeinit() noexcept; /** - * @brief 通过配置文件路径构建运行时 + * @brief 通过接入配置文件构建运行时 (C ABI 路径) */ static int CreateFromConfigFile( const std::string& config_path, int device_id, - const std::string& model_root_dir, CompanyAlgBizType biz_type, + const std::string& model_root_dir, std::unique_ptr* out_runtime, std::string* out_error = nullptr) noexcept; /** - * @brief 通过内存中的 Pipeline JSON 配置对象直接构建运行时 (避免临时文件) + * @brief 通过内存中的 Pipeline JSON 与 IO Binding ID 构建运行时 */ static int CreateFromPipelineJson( const nlohmann::json& pipeline_json, int device_id, - const std::string& model_root_dir, CompanyAlgBizType biz_type, + const std::string& model_root_dir, const std::string& binding_id, std::unique_ptr* out_runtime, std::string* out_error = nullptr, const RuntimeOptions* extra_runtime_options = nullptr) noexcept; /** - * @brief 批量计算通用流 (ValidateBatch -> Unpack -> Pipeline::Execute -> - * Pack) + * @brief 通过已验证的 ValidatedIoPlan 与 RuntimeOptions 构建运行时 + */ + static int CreateFromIoPlan( + std::unique_ptr io_plan, int device_id, + const RuntimeOptions* extra_runtime_options, + std::unique_ptr* out_runtime, + std::string* out_error = nullptr) noexcept; + + /** + * @brief 批量计算通用流 (ValidateBatch -> DecodeInput -> Pipeline::Execute -> + * EncodeOutput) */ int ExecuteBatch(const void** inputs, int num_inputs, void** outputs, - int* num_outputs, std::string* out_error = nullptr, - bool operator_results = false) noexcept; + int* num_outputs, std::string* out_error = nullptr) noexcept; /** * @brief 运行时动态控制指令下发 @@ -70,8 +78,7 @@ class SharedAlgorithmRuntime { // Getters Pipeline* GetPipeline() { return pipeline_.get(); } const Pipeline* GetPipeline() const { return pipeline_.get(); } - std::shared_ptr GetAdapter() const { return adapter_; } - CompanyAlgBizType GetBizType() const { return biz_type_; } + const ValidatedIoPlan* GetIoPlan() const { return io_plan_.get(); } int GetDeviceId() const { return pipeline_ ? pipeline_->GetSessionContext().GetRuntimeOptions().device_id @@ -79,9 +86,8 @@ class SharedAlgorithmRuntime { } private: + std::unique_ptr io_plan_; std::unique_ptr pipeline_; - std::shared_ptr adapter_; - CompanyAlgBizType biz_type_ = ALG_BIZ_TYPE_UNKNOWN; }; } // namespace llm_edgeflow diff --git a/src/adapter/text_carrier.cpp b/src/adapter/text_carrier.cpp deleted file mode 100644 index 6a9ceb7b..00000000 --- a/src/adapter/text_carrier.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "adapter/text_carrier.h" - -namespace llm_edgeflow { - -int ConvertTextCarrierInput(const CompanyOperatorEntityInput& in, - ProcessLocalShadowStorage& storage, - const CompanyEntityInputStruct** out_internal_dto, - std::string* err) { - (void)err; - auto* dto = storage.AllocateShadowDto(); - dto->request_id = in.request_id; - dto->sentence_text = storage.StoreString(in.sentence_text); - *out_internal_dto = dto; - return 0; -} - -int ConvertTextCarrierOutput(const EntityResult& in_dto, - CompanyOperatorEntityOutput& out, - const ResolvedOutputPoolSpec& spec, - std::string* err) { - out.request_id = in_dto.request_id; - out.status_code = in_dto.status_code; - return CopyToOperatorString(in_dto.entities_json.c_str(), out.entities_json, - spec.GetCapacity("entities_json"), - "entities_json", err); -} - -OperatorBizBridgeDescriptor MakeTextCarrierBridge(CompanyAlgBizType biz_type, - std::string adapter_name, - std::string identity) { - return MakeTypedSingleSlotBizBridge< - CompanyEntityInputStruct, EntityResult, CompanyOperatorEntityInput, - CompanyOperatorEntityOutput, &ConvertTextCarrierInput, - &ConvertTextCarrierOutput>( - biz_type, std::move(adapter_name), "CompanyEntityInputStruct", - std::move(identity), "entity_in", "entity_out"); -} - -} // namespace llm_edgeflow diff --git a/src/core/pipeline.cpp b/src/core/pipeline.cpp index b8661f47..1c7bd1cf 100644 --- a/src/core/pipeline.cpp +++ b/src/core/pipeline.cpp @@ -418,6 +418,121 @@ bool Pipeline::BuildFromJson(const nlohmann::json& root_config, return success; } +bool Pipeline::BuildFromPlan(std::unique_ptr plan, + PipelineDiagnostic* diagnostic) { + if (diagnostic) { + diagnostic->Clear(); + } + + // R1-ACC-002: 一次性构建状态检查 + if (state_ != State::kEmpty) { + if (diagnostic) { + diagnostic->code = DiagnosticCode::kInvalidBuildState; + diagnostic->path = "/"; + diagnostic->message = + "Pipeline build can only be attempted once on an empty Pipeline " + "instance"; + } + ALG_LOG_ERROR( + "[Pipeline] Build attempted on non-empty Pipeline (state: %d)\n", + static_cast(state_)); + return false; + } + + state_ = State::kBuilding; + + struct BuildingStateGuard { + State& s; + bool finalized = false; + ~BuildingStateGuard() { + if (!finalized) { + s = State::kFailed; + } + } + } guard{state_}; + + bool success = false; + try { + if (test_internal_hook_) { + test_internal_hook_(); + } + + if (!plan) { + if (diagnostic) { + diagnostic->code = DiagnosticCode::kInternalException; + diagnostic->path = "/"; + diagnostic->message = "Null plan pointer provided to BuildFromPlan"; + } + return false; + } + + if (!plan->report.ok) { + if (!plan->report.diagnostics.empty()) { + const auto& item = plan->report.diagnostics.front(); + const char* code_str = DiagnosticCodeName(item.code); + if (diagnostic) { + diagnostic->code = item.code; + diagnostic->path = item.path; + diagnostic->message = item.message; + } + ALG_LOG_ERROR("[Pipeline] Validation failed: %s at %s: %s\n", code_str, + item.path.c_str(), item.message.c_str()); + } else if (diagnostic) { + diagnostic->code = DiagnosticCode::kInternalException; + diagnostic->path = "/"; + diagnostic->message = "Validation failed without diagnostics"; + } + return false; + } + + RuntimeAssembly assembly; + assembly.plan = std::move(plan); + assembly.session = std::make_unique(); + assembly.session->SetRuntimeOptions(session_ctx_->GetRuntimeOptions()); + + if (!MaterializeModels(*assembly.plan, assembly.session.get(), + diagnostic)) { + return false; + } + if (!MaterializeNodes(&assembly, diagnostic)) { + return false; + } + ConfigureExecutor(assembly.plan->config, &assembly); + + plan_ = std::move(assembly.plan); + session_ctx_ = std::move(assembly.session); + execution_mode_ = assembly.execution_mode; + nodes_ = std::move(assembly.nodes); + node_layers_ = std::move(assembly.node_layers); + thread_pool_ = std::move(assembly.thread_pool); + success = true; + } catch (const std::exception& e) { + success = false; + if (diagnostic) { + diagnostic->code = DiagnosticCode::kInternalException; + diagnostic->path = "/"; + diagnostic->message = + std::string("Internal exception during pipeline build: ") + e.what(); + } + ALG_LOG_ERROR( + "[Pipeline] Unhandled internal exception during pipeline build: %s\n", + e.what()); + } catch (...) { + success = false; + if (diagnostic) { + diagnostic->code = DiagnosticCode::kInternalException; + diagnostic->path = "/"; + diagnostic->message = "Unknown internal exception during pipeline build"; + } + ALG_LOG_ERROR( + "[Pipeline] Unknown internal exception during pipeline build\n"); + } + + state_ = success ? State::kReady : State::kFailed; + guard.finalized = true; + return success; +} + bool Pipeline::BuildInternal(const nlohmann::json& root_config, PipelineDiagnostic* diagnostic, ValidationPolicy policy) { diff --git a/src/core/pipeline_validator.cpp b/src/core/pipeline_validator.cpp index 05f33a4c..7180d9d6 100644 --- a/src/core/pipeline_validator.cpp +++ b/src/core/pipeline_validator.cpp @@ -882,7 +882,8 @@ void PopulateBasicRemediation( ValidatedPipelinePlan ValidateAndPlanInternal( const nlohmann::json& root, ValidationPolicy policy, - const PipelineCatalogSnapshot& catalog) { + const PipelineCatalogSnapshot& catalog, + const PipelineIoBoundary* io_boundary = nullptr) { ValidatedPipelinePlan plan; ValidationReport& report = plan.report; @@ -1155,6 +1156,39 @@ ValidatedPipelinePlan ValidateAndPlanInternal( if (biz) { for (const auto& port : biz->ingress) ingress[port.blackboard_key] = port; } + if (io_boundary) { + std::unordered_map input_pub; + for (const auto& port : io_boundary->input_published_ports) { + input_pub[port.blackboard_key] = port; + } + if (biz) { + for (const auto& req_in : biz->ingress) { + if (!req_in.required) continue; + auto it = input_pub.find(req_in.blackboard_key); + if (it == input_pub.end()) { + Add(&report, DiagnosticCode::kMissingInputProducer, "/io/input", + "IO boundary input does not publish required biz ingress port: " + + req_in.blackboard_key, + "$io_input", req_in.blackboard_key); + } else { + if (it->second.type_id != req_in.type_id) { + Add(&report, DiagnosticCode::kMissingInputProducer, "/io/input", + "IO boundary input port type mismatch for '" + + req_in.blackboard_key + "': expected '" + req_in.type_id + + "', got '" + it->second.type_id + "'", + "$io_input", req_in.blackboard_key); + } else { + ValidatePortFlowContract(it->second, req_in, "/io/input", + "$io_input", req_in.blackboard_key, + "$ingress", &report); + } + } + } + } + for (const auto& port : io_boundary->input_published_ports) { + ingress[port.blackboard_key] = port; + } + } std::unordered_map>> producers; @@ -1432,6 +1466,49 @@ ValidatedPipelinePlan ValidateAndPlanInternal( } } + if (io_boundary) { + for (const auto& consumer : io_boundary->output_consumed_ports) { + auto it = producers.find(consumer.blackboard_key); + if (it == producers.end() || it->second.empty()) { + auto ing_it = ingress.find(consumer.blackboard_key); + if (ing_it != ingress.end()) { + if (ing_it->second.type_id != consumer.type_id) { + Add(&report, DiagnosticCode::kMissingBizOutput, "/io/output", + "IO boundary output type mismatch for '" + + consumer.blackboard_key + "': expected '" + + consumer.type_id + "', got '" + ing_it->second.type_id + + "'", + "$ingress", consumer.blackboard_key, {"$io_output"}); + } else { + ValidatePortFlowContract(ing_it->second, consumer, "/io/output", + "$ingress", consumer.blackboard_key, + "$io_output", &report); + } + continue; + } + if (consumer.required) { + Add(&report, DiagnosticCode::kMissingBizOutput, "/io/output", + "Pipeline does not produce required IO boundary output: " + + consumer.blackboard_key, + {}, consumer.blackboard_key); + } + continue; + } + const auto& [producer_id, producer_port] = it->second.back(); + if (producer_port.type_id != consumer.type_id) { + Add(&report, DiagnosticCode::kMissingBizOutput, "/io/output", + "IO boundary output type mismatch for '" + consumer.blackboard_key + + "': expected '" + consumer.type_id + "', got '" + + producer_port.type_id + "'", + producer_id, consumer.blackboard_key, {"$io_output"}); + continue; + } + ValidatePortFlowContract(producer_port, consumer, "/io/output", + producer_id, consumer.blackboard_key, + "$io_output", &report); + } + } + if (parsed.execution_mode == "parallel") { for (const auto& layer : report.topological_layers) { std::unordered_map writes; @@ -1537,21 +1614,24 @@ nlohmann::json ValidationReport::ToJson() const { } ValidatedPipelinePlan PipelineValidator::ValidateAndPlan( - const nlohmann::json& root, ValidationPolicy policy) { + const nlohmann::json& root, ValidationPolicy policy, + const PipelineIoBoundary* io_boundary) { const auto catalog = PipelineCatalog::Snapshot(); - return ValidateAndPlanInternal(root, policy, catalog); + return ValidateAndPlanInternal(root, policy, catalog, io_boundary); } -ValidationReport PipelineValidator::Validate(const nlohmann::json& root, - ValidationPolicy policy) { - return ValidateAndPlan(root, policy).report; +ValidationReport PipelineValidator::Validate( + const nlohmann::json& root, ValidationPolicy policy, + const PipelineIoBoundary* io_boundary) { + return ValidateAndPlan(root, policy, io_boundary).report; } -ValidationReport PipelineValidator::Explain(const nlohmann::json& root, - ValidationPolicy policy) { +ValidationReport PipelineValidator::Explain( + const nlohmann::json& root, ValidationPolicy policy, + const PipelineIoBoundary* io_boundary) { const auto catalog = PipelineCatalog::Snapshot(); ValidationReport report = - ValidateAndPlanInternal(root, policy, catalog).report; + ValidateAndPlanInternal(root, policy, catalog, io_boundary).report; if (report.ok) { return report; } @@ -1764,7 +1844,8 @@ ValidationReport PipelineValidator::Explain(const nlohmann::json& root, total_verification_attempts++; ValidationReport new_report = - ValidateAndPlanInternal(patched_root, policy, catalog).report; + ValidateAndPlanInternal(patched_root, policy, catalog, io_boundary) + .report; if (new_report.ok) { fix.verification = "pipeline_valid"; diag.remediation->fixes.push_back(std::move(fix)); diff --git a/src/tools/alg_pipeline_tool.cpp b/src/tools/alg_pipeline_tool.cpp index 3cabd9ce..a6cf47fd 100644 --- a/src/tools/alg_pipeline_tool.cpp +++ b/src/tools/alg_pipeline_tool.cpp @@ -6,6 +6,9 @@ #include #include +#include "adapter/deployment_io_config.h" +#include "adapter/io_binding_resolver.h" +#include "adapter/io_catalog.h" #include "adapter/operator/operator_config_resolver.h" #include "core/diagnostic_code.h" #include "core/pipeline_catalog.h" @@ -76,7 +79,7 @@ std::optional ProfilePipeline( if (!conf_stream.is_open()) return std::nullopt; nlohmann::json conf; conf_stream >> conf; - if (!conf.is_object() || conf.size() != 1 || !conf.contains("data") || + if (!conf.is_object() || !conf.contains("data") || !conf["data"].is_object() || !conf["data"].contains("pipe_path") || !conf["data"]["pipe_path"].is_string()) { return std::nullopt; @@ -198,6 +201,8 @@ void Usage() { << " alg_pipeline_tool plan FILE|--stdin [--explain]\n"; std::cerr << " alg_pipeline_tool resolve-conf FILE [--root DIR] [--depth N]\n" + << " alg_pipeline_tool validate-io CONFIG --transport " + "[--model-root DIR]\n" << " alg_pipeline_tool edit --stdin\n" << " alg_pipeline_tool fix-deps FILE [--in-place]\n"; } @@ -270,7 +275,7 @@ int main(int argc, char* argv[]) { << std::endl; return 1; } - auto result = PipelineCatalog::ToJson(snapshot, biz); + auto result = llm_edgeflow::IoCatalog::ToJson(snapshot, biz); result["profiles"] = ProfilesJson(biz); result["ok"] = biz.empty() || !result["bizs"].empty(); std::cout << result.dump(2) << std::endl; @@ -390,6 +395,70 @@ int main(int argc, char* argv[]) { return report.ok ? 0 : 1; } + if (command == "validate-io") { + if (argc < 4) { + Usage(); + return 2; + } + std::string config_path = argv[2]; + std::string transport; + std::string model_root; + for (int i = 3; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--transport" && i + 1 < argc) { + transport = argv[++i]; + } else if (arg == "--model-root" && i + 1 < argc) { + model_root = argv[++i]; + } else { + Usage(); + return 2; + } + } + if (transport != "cabi" && transport != "operator") { + Usage(); + return 2; + } + + std::unique_ptr plan; + std::string error; + int rc = llm_edgeflow::IoBindingResolver::ResolveFromFile( + config_path, transport, model_root, &plan, &error); + + if (rc != 0 || !plan) { + nlohmann::json err_res = { + {"schema_version", 1}, + {"ok", false}, + {"diagnostics", + nlohmann::json::array({{{"code", "IO_VALIDATION_ERROR"}, + {"path", "/"}, + {"message", error}, + {"severity", "error"}}})}}; + std::cout << err_res.dump(2) << std::endl; + return 1; + } + + nlohmann::json binding_info = { + {"binding_id", plan->binding.binding_id}, + {"biz_name", plan->binding.biz_name}, + {"transport", plan->binding.transport}, + {"input_converter_id", plan->binding.input_converter_id}, + {"output_converter_id", plan->binding.output_converter_id}, + {"input_port_mapping", plan->binding.input_ports}, + {"output_port_mapping", plan->binding.output_ports}, + {"effective_max_batch_size", plan->effective_max_batch_size}, + {"external_input_type", + plan->input_converter ? plan->input_converter->external_type : ""}, + {"external_output_type", + plan->output_converter ? plan->output_converter->external_type : ""}}; + + nlohmann::json result = {{"schema_version", 1}, + {"ok", true}, + {"binding", std::move(binding_info)}, + {"diagnostics", nlohmann::json::array()}}; + std::cout << result.dump(2) << std::endl; + return 0; + } + if (command == "edit") { if (argc != 3 || std::string(argv[2]) != "--stdin") { Usage(); diff --git a/tests/contract/abi/test_adapter_contract_security.cpp b/tests/contract/abi/test_adapter_contract_security.cpp index d82a5381..c882a356 100644 --- a/tests/contract/abi/test_adapter_contract_security.cpp +++ b/tests/contract/abi/test_adapter_contract_security.cpp @@ -10,10 +10,11 @@ #include "adapter/adapter_status.h" #include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_registry.h" #include "adapter/biz_blackboard_keys.h" #include "adapter/biz_results.h" #include "adapter/deployment_model_resolver.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter_registry.h" #include "adapter/shared_algorithm_runtime.h" #include "edgeflow/c_api.h" #include "edgeflow/c_api.hpp" @@ -34,12 +35,14 @@ static std::string GetConfigPath(const std::string& rel_path) { class AdapterContractSecurityTest : public ::testing::Test { protected: void SetUp() override { - BizAdapterRegistry::Instance().ResetConflictForTesting(); + IoBindingRegistry::Instance().ResetConflictForTesting(); + IoConverterRegistry::Instance().ResetConflictForTesting(); Alg_Init(); } void TearDown() override { Alg_DeInit(); - BizAdapterRegistry::Instance().ResetConflictForTesting(); + IoBindingRegistry::Instance().ResetConflictForTesting(); + IoConverterRegistry::Instance().ResetConflictForTesting(); } }; @@ -118,7 +121,7 @@ TEST_F(AdapterContractSecurityTest, std::filesystem::remove_all(directory, error); } } cleanup{directory}; - const auto config = (directory / "pipeline.json").string(); + const auto pipe_path = (directory / "pipeline.json").string(); std::ifstream source(GetConfigPath("configs/pipeline_translate_cpu.json")); ASSERT_TRUE(source.is_open()); nlohmann::json pipeline; @@ -134,9 +137,16 @@ TEST_F(AdapterContractSecurityTest, model_config["model_path"] = "translation-probe.fixture"; model_config["model_config"] = nlohmann::json::object(); model_config["backend_config"] = nlohmann::json::object(); - std::ofstream(config) << pipeline.dump(); - CompanyAlgParamCreate create{config.c_str(), "./models", 0, - ALG_BIZ_TYPE_TRANSLATE}; + std::ofstream(pipe_path) << pipeline.dump(); + + nlohmann::json cabi_cfg = { + {"schema_version", 1}, + {"data", + {{"pipe_path", "pipeline.json"}, {"io_binding", "translate.cabi.v1"}}}}; + const auto config = (directory / "pipeline_cabi.json").string(); + std::ofstream(config) << cabi_cfg.dump(); + + CompanyAlgParamCreate create{config.c_str(), "./models", 0}; void* raw_handle = nullptr; ASSERT_EQ(Alg_Create(&raw_handle, &create), 0); std::unique_ptr handle(raw_handle, Alg_Destroy); @@ -226,26 +236,29 @@ TEST_F(AdapterContractSecurityTest, TEST_F(AdapterContractSecurityTest, TranslationLiteralResultPackingAndCarrierSafety) { - const auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); + const auto* converter = IoConverterRegistry::Instance().FindOutputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(converter, nullptr); const std::string translation(2200, 'x'); AlgContext large; large.Publish(kRawRequestIds, std::vector{123}); large.Publish(kLlmAnswers, TextBatch{{0, 0, translation}}); - EntityResult variable; - void* variable_outputs[] = {&variable}; - int count = 1; - ASSERT_EQ(adapter->PackResultBatch(&large, variable_outputs, &count), 0); - EXPECT_EQ(count, 1); - EXPECT_EQ(variable.request_id, 123U); - EXPECT_EQ(variable.status_code, 0); - EXPECT_EQ(nlohmann::json::parse(variable.entities_json), - nlohmann::json({{"translated", translation}})); + OutputPortBindings bindings( + {{"raw_request_ids", "raw_request_ids"}, {"llm_answers", "llm_answers"}}); + OutputEncodeOptions options; + options.converter_id = "translate.json.cabi.v1"; + AdapterStatus status; + CompanyEntityOutputStruct fixed{}; void* fixed_outputs[] = {&fixed}; - count = 1; - EXPECT_EQ(adapter->Pack(&large, fixed_outputs, &count), + ExternalOutputBatchView fixed_view; + fixed_view.items = fixed_outputs; + fixed_view.count = 1; + fixed_view.capacity = 1; + size_t written = 0; + + EXPECT_EQ(converter->encode_fn(&large, bindings, options, &fixed_view, + &written, &status), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); // Reordered internal results must map back to external request IDs. @@ -254,19 +267,25 @@ TEST_F(AdapterContractSecurityTest, reordered.Publish(kLlmAnswers, TextBatch{{1, 0, "第二句"}, {0, 0, "第一句"}}); CompanyEntityOutputStruct first{}, second{}; void* two_outputs[] = {&first, &second}; - count = 2; - ASSERT_EQ(adapter->Pack(&reordered, two_outputs, &count), 0); - EXPECT_EQ(count, 2); + ExternalOutputBatchView reordered_view; + reordered_view.items = two_outputs; + reordered_view.count = 2; + reordered_view.capacity = 2; + ASSERT_EQ(converter->encode_fn(&reordered, bindings, options, &reordered_view, + &written, &status), + 0); + EXPECT_EQ(written, 2U); EXPECT_EQ(first.request_id, 999U); EXPECT_EQ(second.request_id, 123U); EXPECT_EQ(nlohmann::json::parse(first.entities_json), nlohmann::json({{"translated", "第一句"}})); EXPECT_EQ(nlohmann::json::parse(second.entities_json), nlohmann::json({{"translated", "第二句"}})); - count = 1; - EXPECT_EQ(adapter->Pack(&reordered, two_outputs, &count), + + reordered_view.capacity = 1; + EXPECT_EQ(converter->encode_fn(&reordered, bindings, options, &reordered_view, + &written, &status), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_EQ(count, 2); for (const TextBatch& invalid : std::vector{{}, @@ -277,8 +296,9 @@ TEST_F(AdapterContractSecurityTest, AlgContext ctx; ctx.Publish(kRawRequestIds, std::vector{999, 123}); ctx.Publish(kLlmAnswers, invalid); - count = 2; - EXPECT_EQ(adapter->Pack(&ctx, two_outputs, &count), + reordered_view.capacity = 2; + EXPECT_EQ(converter->encode_fn(&ctx, bindings, options, &reordered_view, + &written, &status), COMPANY_ALG_ERR_INVALID_INPUT); } for (bool publish_ids : {false, true}) { @@ -288,9 +308,10 @@ TEST_F(AdapterContractSecurityTest, } else { missing.Publish(kLlmAnswers, TextBatch{{0, 0, "你好"}}); } - count = 1; - EXPECT_EQ(adapter->PackResultBatch(&missing, variable_outputs, &count), - COMPANY_ALG_ERR_INVALID_INPUT); + reordered_view.capacity = 1; + EXPECT_EQ(converter->encode_fn(&missing, bindings, options, &reordered_view, + &written, &status), + COMPANY_ALG_ERR_BUFFER_TOO_SMALL); } } @@ -396,7 +417,7 @@ TEST_F(AdapterContractSecurityTest, std::unique_ptr runtime; std::string diagnostic; ASSERT_EQ(SharedAlgorithmRuntime::CreateFromPipelineJson( - pipeline_json, 0, model_root.string(), ALG_BIZ_TYPE_DOC_QA, + pipeline_json, 0, model_root.string(), "doc_qa.cabi.v1", &runtime, &diagnostic), COMPANY_ALG_SUCCESS) << diagnostic; @@ -566,9 +587,9 @@ TEST_F(AdapterContractSecurityTest, NestedPointerTreeDepthProtection) { // 4. COPY_IN 内存所有权深度隔离测试 (ADP-002, RECHECK-006) // --------------------------------------------------------------------------- TEST_F(AdapterContractSecurityTest, DirectUnpackMemoryIsolation) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_KEYWORD_MATCH); - ASSERT_NE(adapter, nullptr); + const auto* input_conv = + IoConverterRegistry::Instance().FindInputConverter("text.plain.cabi.v1"); + ASSERT_NE(input_conv, nullptr); // 创建动态可修改的原始缓冲区 char caller_buf[256]; @@ -579,9 +600,18 @@ TEST_F(AdapterContractSecurityTest, DirectUnpackMemoryIsolation) { in_struct.sentence_text = caller_buf; const void* inputs[1] = {&in_struct}; + ExternalInputBatchView in_view; + in_view.items = inputs; + in_view.count = 1; + InputPortBindings in_bindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}); + InputDecodeOptions in_options; + in_options.converter_id = "text.cabi.v1"; + AlgContext ctx; AdapterStatus status; - int unpack_ret = adapter->Unpack(inputs, 1, &ctx, &status); + int unpack_ret = + input_conv->decode_fn(in_view, in_options, in_bindings, &ctx, &status); ASSERT_EQ(unpack_ret, COMPANY_ALG_SUCCESS); // 立即篡改调用方内存 Buffer (例如 memset 覆盖为 'X') @@ -626,34 +656,33 @@ TEST_F(AdapterContractSecurityTest, OutputStringTruncationRejection) { // 6. Pipeline 绑定精确白名单与 Fail-Closed 校验 (RECHECK-002) // --------------------------------------------------------------------------- TEST_F(AdapterContractSecurityTest, PipelineBindingFailClosedAndExactMatch) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_KEYWORD_MATCH); - ASSERT_NE(adapter, nullptr); + const auto* binding = + IoBindingRegistry::Instance().FindBinding("keyword_match.cabi.v1"); + ASSERT_NE(binding, nullptr); // 6.1 精确匹配成功 - EXPECT_TRUE(adapter->ValidatePipelineBinding("keyword_match_v1")); + EXPECT_EQ(binding->biz_name, "keyword_match_v1"); // 6.2 包含子串的伪造名称 / 大小写不匹配 / 空白名称均严格拒绝 (Fail-Closed) - EXPECT_FALSE(adapter->ValidatePipelineBinding("keyword_match_v1_fake")); - EXPECT_FALSE(adapter->ValidatePipelineBinding("my_keyword_match_v1")); - EXPECT_FALSE(adapter->ValidatePipelineBinding("KEYWORD_MATCH_V1")); - EXPECT_FALSE(adapter->ValidatePipelineBinding("")); - EXPECT_FALSE( - adapter->ValidatePipelineBinding("dialogue_compliance_audit_v1")); - - // 6.3 Alg_Create 阶段使用串用配置创建句柄立即失败 (-5) + EXPECT_EQ(IoBindingRegistry::Instance().FindBinding("keyword_match_v1_fake"), + nullptr); + EXPECT_EQ(IoBindingRegistry::Instance().FindBinding("my_keyword_match_v1"), + nullptr); + EXPECT_EQ(IoBindingRegistry::Instance().FindBinding("KEYWORD_MATCH_V1"), + nullptr); + EXPECT_EQ(IoBindingRegistry::Instance().FindBinding(""), nullptr); + + // 6.3 Alg_Create 阶段使用非法配置创建句柄立即失败 (-2) std::string wrong_cfg = GetConfigPath("demo/fixtures/mock/pipeline_dialogue_audit.json"); CompanyAlgParamCreate param; param.config_file_path = wrong_cfg.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; // 业务是 KeywordMatch,但配置是 - // ComplianceAudit void* handle = nullptr; int create_ret = Alg_Create(&handle, ¶m); - EXPECT_EQ(create_ret, -5); + EXPECT_EQ(create_ret, -2); EXPECT_EQ(handle, nullptr); } @@ -661,47 +690,17 @@ TEST_F(AdapterContractSecurityTest, PipelineBindingFailClosedAndExactMatch) { // 7. Registry 拒绝不支持的 Descriptor 策略组合 (RECHECK-003) // --------------------------------------------------------------------------- TEST_F(AdapterContractSecurityTest, RegistryRejectsUnsupportedPolicies) { - class UnsupportedPolicyAdapter : public IBizAdapter { - public: - CompanyAlgBizType BizType() const override { - return static_cast(201); - } - const char* AdapterName() const override { return "UnsupportedPolicy"; } - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - static_cast(201), - "UnsupportedPolicy", - COMPANY_ALG_ABI_VERSION, - "In", - "Out", - 64, - OwnershipPolicy::kBorrowDuringProcess, // 当前未开放策略 - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {BizDefinition{"UnsupportedPolicy", "pipeline_v1"}}}; - return desc; - } - int Unpack(const void** i, int n, AlgContext* c, - AdapterStatus* s) const override { - (void)i; - (void)n; - (void)c; - (void)s; - return 0; - } - int Pack(AlgContext* c, void** o, int* n, AdapterStatus* s) const override { - (void)c; - (void)o; - (void)n; - (void)s; - return 0; - } - }; - - auto bad_adapter = std::make_shared(); - bool reg_ret = BizAdapterRegistry::Instance().RegisterAdapter(bad_adapter); + InputConverterDefinition bad_def; + bad_def.converter_id = "bad.converter.v1"; + bad_def.transport = "unsupported_transport"; + bad_def.decode_fn = [](const ExternalInputBatchView&, + const InputDecodeOptions&, const InputPortBindings&, + AlgContext*, AdapterStatus*) { return 0; }; + + bool reg_ret = + IoConverterRegistry::Instance().RegisterInputConverter(bad_def); EXPECT_FALSE(reg_ret); - EXPECT_TRUE(BizAdapterRegistry::Instance().HasRegistrationConflict()); + EXPECT_TRUE(IoConverterRegistry::Instance().HasConflict()); } // --------------------------------------------------------------------------- @@ -735,12 +734,11 @@ TEST_F(AdapterContractSecurityTest, StructuredStatusAndBoundedStringScan) { // --------------------------------------------------------------------------- TEST_F(AdapterContractSecurityTest, ConcurrentStatelessAdapterExecution) { std::string cfg_path = - GetConfigPath("configs/pipeline_keyword_match_rules.json"); + GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; constexpr int kNumThreads = 8; constexpr int kNumIters = 10; @@ -779,12 +777,12 @@ TEST_F(AdapterContractSecurityTest, ConcurrentStatelessAdapterExecution) { } } -// RFC-0053: Cross-sample carrier error vs biz decode error priority +// RFC-0053 / RFC-0059: Cross-sample carrier error vs biz decode error priority TEST_F(AdapterContractSecurityTest, TranslationCrossSampleCarrierVsBizErrorPriority) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); + const auto* converter = IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(converter, nullptr); // Sample 0 has invalid JSON (biz error), Sample 1 has oversized string // (carrier error) @@ -793,59 +791,78 @@ TEST_F(AdapterContractSecurityTest, CompanyEntityInputStruct in1{102, oversized.c_str()}; const void* inputs[] = {&in0, &in1}; + ExternalInputBatchView view; + view.items = inputs; + view.count = 2; + InputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}); + InputDecodeOptions options; + options.converter_id = "translate.json.cabi.v1"; + AlgContext ctx; AdapterStatus status; - int ret = adapter->Unpack(inputs, 2, &ctx, &status); + int ret = converter->decode_fn(view, options, bindings, &ctx, &status); EXPECT_EQ(ret, COMPANY_ALG_ERR_INVALID_INPUT); // Carrier validation is performed on the entire batch before any decode, // so sample 1's carrier error must be diagnosed rather than sample 0's decode // error. EXPECT_EQ(status.SampleIndex(), 1); EXPECT_EQ(status.FieldPath(), "inputs[i].sentence_text"); - EXPECT_EQ(status.AdapterName(), "EntityExtract"); - // Pure biz decode error retains Translate adapter name + // Pure biz decode error retains translate.json.cabi.v1 converter name CompanyEntityInputStruct in_biz{103, "{\"wrong_field\":123}"}; const void* biz_inputs[] = {&in_biz}; + ExternalInputBatchView biz_view; + biz_view.items = biz_inputs; + biz_view.count = 1; AlgContext biz_ctx; AdapterStatus biz_status; - EXPECT_EQ(adapter->Unpack(biz_inputs, 1, &biz_ctx, &biz_status), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(biz_status.AdapterName(), "Translate"); + EXPECT_EQ( + converter->decode_fn(biz_view, options, bindings, &biz_ctx, &biz_status), + COMPANY_ALG_ERR_INVALID_INPUT); + EXPECT_EQ(biz_status.AdapterName(), "translate.json.cabi.v1"); EXPECT_EQ(biz_status.FieldPath(), "json"); } -// RFC-0053: Return code and AdapterStatus independence +// RFC-0053 / RFC-0059: Return code and AdapterStatus independence TEST_F(AdapterContractSecurityTest, TranslationReturnCodeAndAdapterStatusIndependence) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); + const auto* converter = IoConverterRegistry::Instance().FindOutputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(converter, nullptr); // AlgContext with raw_request_ids but missing answers AlgContext ctx; ctx.Publish(kRawRequestIds, std::vector{1001}); - EntityResult out{}; + CompanyEntityOutputStruct out{}; void* outputs[] = {&out}; - int count = 1; + ExternalOutputBatchView view; + view.items = outputs; + view.count = 1; + view.capacity = 1; + OutputPortBindings bindings( + {{"raw_request_ids", "raw_request_ids"}, {"llm_answers", "llm_answers"}}); + OutputEncodeOptions options; + options.converter_id = "translate.json.cabi.v1"; + + size_t written = 0; AdapterStatus status; - int ret = adapter->PackResultBatch(&ctx, outputs, &count, &status); + int ret = + converter->encode_fn(&ctx, bindings, options, &view, &written, &status); - // Return code is INVALID_INPUT (-3) - EXPECT_EQ(ret, COMPANY_ALG_ERR_INVALID_INPUT); // Underlying reader wrote BUFFER_TOO_SMALL (-4) into AdapterStatus + EXPECT_EQ(ret, COMPANY_ALG_ERR_BUFFER_TOO_SMALL); EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_NE(ret, status.Code()); } -// RFC-0053: Translate serialization failure (invalid UTF-8) priority over -// capacity check +// RFC-0053 / RFC-0059: Translate serialization failure (invalid UTF-8) priority +// over capacity check TEST_F(AdapterContractSecurityTest, TranslationSerializationFailurePriorityOverCapacity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); + const auto* converter = IoConverterRegistry::Instance().FindOutputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(converter, nullptr); // AlgContext with valid raw_req_ids, but answer has invalid UTF-8 byte // sequence @@ -854,69 +871,83 @@ TEST_F(AdapterContractSecurityTest, std::string invalid_utf8 = "prefix\xFF\xFFsuffix"; ctx.Publish(kLlmAnswers, TextBatch{{0, 0, invalid_utf8}}); - // Call Pack with null outputs and 0 count (would trigger BUFFER_TOO_SMALL if - // capacity was checked first) - int count = 0; + ExternalOutputBatchView view; + view.items = nullptr; + view.count = 0; + view.capacity = 0; + OutputPortBindings bindings( + {{"raw_request_ids", "raw_request_ids"}, {"llm_answers", "llm_answers"}}); + OutputEncodeOptions options; + options.converter_id = "translate.json.cabi.v1"; + + size_t written = 0; AdapterStatus status; - // Serialization in PrepareResults precedes capacity validation; - // unhandled dump exception propagates out of Pack to SharedAlgorithmRuntime - // where it maps to COMPANY_ALG_ERR_EXCEPTION (-99) per RFC-0053 §3.2. - EXPECT_THROW(adapter->Pack(&ctx, nullptr, &count, &status), std::exception); - EXPECT_THROW(adapter->PackResultBatch(&ctx, nullptr, &count, &status), - std::exception); + // Serialization in Encode precedes capacity validation; + // unhandled dump exception propagates out of encode_fn + EXPECT_THROW( + converter->encode_fn(&ctx, bindings, options, &view, &written, &status), + std::exception); } -// RFC-0053: Translate null AlgContext legacy diagnostic characterization -TEST_F(AdapterContractSecurityTest, TranslateNullContextLegacyDiagnostics) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); - - // 1. Unpack with null context: must return INVALID_INPUT (-3) with field - // "json" +// RFC-0053 / RFC-0059: Translate null AlgContext diagnostics +TEST_F(AdapterContractSecurityTest, TranslateNullContextDiagnostics) { + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + // 1. Decode with null context: must return INVALID_INPUT (-3) with field + // "context" CompanyEntityInputStruct input{100, "{\"query\":\"test\"}"}; const void* inputs[] = {&input}; + ExternalInputBatchView in_view; + in_view.items = inputs; + in_view.count = 1; + InputPortBindings in_bindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}); + InputDecodeOptions in_options; + in_options.converter_id = "translate.json.cabi.v1"; + AdapterStatus unpack_status; - int unpack_ret = adapter->Unpack(inputs, 1, nullptr, &unpack_status); + int unpack_ret = in_conv->decode_fn(in_view, in_options, in_bindings, nullptr, + &unpack_status); EXPECT_EQ(unpack_ret, COMPANY_ALG_ERR_INVALID_INPUT); EXPECT_EQ(unpack_status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(unpack_status.FieldPath(), "json"); - EXPECT_EQ(unpack_status.AdapterName(), "Translate"); + EXPECT_EQ(unpack_status.FieldPath(), "context"); + EXPECT_EQ(unpack_status.AdapterName(), "translate.json.cabi.v1"); - EXPECT_EQ(adapter->Unpack(inputs, 1, nullptr, nullptr), - COMPANY_ALG_ERR_INVALID_INPUT); + EXPECT_EQ( + in_conv->decode_fn(in_view, in_options, in_bindings, nullptr, nullptr), + COMPANY_ALG_ERR_INVALID_INPUT); - // 2. Pack with null context: must return INVALID_INPUT (-3) with field "json" + // 2. Encode with null context: must return BUFFER_TOO_SMALL (-4) with field + // "context" CompanyEntityOutputStruct output{}; void* outputs[] = {&output}; - int count = 1; + ExternalOutputBatchView out_view; + out_view.items = outputs; + out_view.count = 1; + out_view.capacity = 1; + OutputPortBindings out_bindings( + {{"raw_request_ids", "raw_request_ids"}, {"llm_answers", "llm_answers"}}); + OutputEncodeOptions out_options; + out_options.converter_id = "translate.json.cabi.v1"; + + size_t written = 0; AdapterStatus pack_status; - int pack_ret = adapter->Pack(nullptr, outputs, &count, &pack_status); - EXPECT_EQ(pack_ret, COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(pack_status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(pack_status.FieldPath(), "json"); - EXPECT_EQ(pack_status.AdapterName(), "Translate"); - - EXPECT_EQ(adapter->Pack(nullptr, outputs, &count, nullptr), - COMPANY_ALG_ERR_INVALID_INPUT); - - // 3. PackResultBatch with null context: must return INVALID_INPUT (-3) with - // field "json" - EntityResult owned_output{}; - void* owned_outputs[] = {&owned_output}; - int owned_count = 1; - AdapterStatus owned_pack_status; - int owned_pack_ret = adapter->PackResultBatch( - nullptr, owned_outputs, &owned_count, &owned_pack_status); - EXPECT_EQ(owned_pack_ret, COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(owned_pack_status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(owned_pack_status.FieldPath(), "json"); - EXPECT_EQ(owned_pack_status.AdapterName(), "Translate"); + int pack_ret = out_conv->encode_fn(nullptr, out_bindings, out_options, + &out_view, &written, &pack_status); + EXPECT_EQ(pack_ret, COMPANY_ALG_ERR_BUFFER_TOO_SMALL); + EXPECT_EQ(pack_status.Code(), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); + EXPECT_EQ(pack_status.FieldPath(), "context"); + EXPECT_EQ(pack_status.AdapterName(), "translate.json.cabi.v1"); - EXPECT_EQ( - adapter->PackResultBatch(nullptr, owned_outputs, &owned_count, nullptr), - COMPANY_ALG_ERR_INVALID_INPUT); + EXPECT_EQ(out_conv->encode_fn(nullptr, out_bindings, out_options, &out_view, + &written, nullptr), + COMPANY_ALG_ERR_BUFFER_TOO_SMALL); } } // namespace llm_edgeflow diff --git a/tests/contract/abi/test_c11_abi_compliance.c b/tests/contract/abi/test_c11_abi_compliance.c index a6dc3791..5f08d316 100644 --- a/tests/contract/abi/test_c11_abi_compliance.c +++ b/tests/contract/abi/test_c11_abi_compliance.c @@ -17,17 +17,13 @@ #include "platform_mock/error_codes.h" #include "platform_mock/operator_data_types.h" -#if COMPANY_ALG_ABI_VERSION_MAJOR != 5 +#if COMPANY_ALG_ABI_VERSION_MAJOR != 6 #error "Unexpected public C ABI major" #endif #include "edgeflow/log.h" #include "edgeflow/operator/types.h" #include "edgeflow/version.h" -_Static_assert(sizeof(CompanyAlgBizType) == sizeof(int32_t), - "CompanyAlgBizType must remain a 32-bit C ABI type"); -_Static_assert(ALG_BIZ_TYPE_MAX_GUARD == INT32_MAX, - "CompanyAlgBizType ABI guard must remain INT32_MAX"); _Static_assert(sizeof(CompanyString) == sizeof(int32_t) + sizeof(char*) + (sizeof(char*) == 8 ? 4 : 0), "CompanyString memory layout check"); @@ -42,7 +38,7 @@ _Static_assert(E_ALG_BASE_LOG_LEVEL_VERBOSE == 5, int main(void) { if (strcmp(COMPANY_ALG_PRODUCT_VERSION, "10.0.0") != 0 || - strcmp(COMPANY_ALG_ABI_VERSION, "5.0.0") != 0) { + strcmp(COMPANY_ALG_ABI_VERSION, "6.0.0") != 0) { fprintf(stderr, "[C11 ABI Test] Generated version contract drifted\n"); return 13; } @@ -97,9 +93,9 @@ int main(void) { // Determine config path const char* cfg_candidates[] = { - "configs/pipeline_keyword_match_rules.json", - "../configs/pipeline_keyword_match_rules.json", - "../../configs/pipeline_keyword_match_rules.json"}; + "configs/pipeline_keyword_match_cabi.json", + "../configs/pipeline_keyword_match_cabi.json", + "../../configs/pipeline_keyword_match_cabi.json"}; const char* cfg_path = NULL; for (int i = 0; i < 3; ++i) { FILE* f = fopen(cfg_candidates[i], "r"); @@ -111,16 +107,14 @@ int main(void) { } if (!cfg_path) { - fprintf( - stderr, - "[C11 ABI Test] Could not find pipeline_keyword_match_rules.json\n"); + fprintf(stderr, + "[C11 ABI Test] Could not find pipeline_keyword_match_cabi.json\n"); return 6; } param.config_file_path = cfg_path; param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = NULL; int create_ret = Alg_Create(&handle, ¶m); diff --git a/tests/contract/abi/test_c_abi_safety.cpp b/tests/contract/abi/test_c_abi_safety.cpp index 2183b7b7..7a7fe2ca 100644 --- a/tests/contract/abi/test_c_abi_safety.cpp +++ b/tests/contract/abi/test_c_abi_safety.cpp @@ -2,13 +2,16 @@ #include #include +#include +#include #include #include #include #include -#include "adapter/biz_adapter_registry.h" #include "adapter/biz_blackboard_keys.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter_registry.h" #include "edgeflow/c_api.h" #include "edgeflow/c_api.hpp" @@ -24,12 +27,14 @@ static std::string GetConfigPath(const std::string& rel_path) { class CAbiSafetyTest : public ::testing::Test { protected: void SetUp() override { - llm_edgeflow::BizAdapterRegistry::Instance().ResetConflictForTesting(); + llm_edgeflow::IoBindingRegistry::Instance().ResetConflictForTesting(); + llm_edgeflow::IoConverterRegistry::Instance().ResetConflictForTesting(); Alg_Init(); } void TearDown() override { Alg_DeInit(); - llm_edgeflow::BizAdapterRegistry::Instance().ResetConflictForTesting(); + llm_edgeflow::IoBindingRegistry::Instance().ResetConflictForTesting(); + llm_edgeflow::IoConverterRegistry::Instance().ResetConflictForTesting(); } }; @@ -51,12 +56,11 @@ TEST_F(CAbiSafetyTest, NullPointerSafety) { // 2. 测试句柄快速创建与销毁循环 (50轮生命周期与资源泄露检测) TEST_F(CAbiSafetyTest, HandleLifecycleStressCycles50) { - std::string cfg = GetConfigPath("configs/pipeline_keyword_match_rules.json"); + std::string cfg = GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; for (int cycle = 0; cycle < 50; ++cycle) { void* handle = nullptr; @@ -71,12 +75,11 @@ TEST_F(CAbiSafetyTest, HandleLifecycleStressCycles50) { // 3. 测试通过 C ABI 接口全流程调用与动态控制规则生效 TEST_F(CAbiSafetyTest, EndToEndDynamicControlAndVerification) { - std::string cfg = GetConfigPath("configs/pipeline_keyword_match_rules.json"); + std::string cfg = GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; int ret = Alg_Create(&handle, ¶m); @@ -112,12 +115,11 @@ TEST_F(CAbiSafetyTest, EndToEndDynamicControlAndVerification) { // 4. 测试输出缓冲区容量不足与所需容量回填契约 (ACC-003) TEST_F(CAbiSafetyTest, OutputCapacityInsufficientAndFeedbackContract) { - std::string cfg = GetConfigPath("configs/pipeline_keyword_match_rules.json"); + std::string cfg = GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, ¶m), 0); @@ -148,12 +150,11 @@ TEST_F(CAbiSafetyTest, OutputCapacityInsufficientAndFeedbackContract) { // 5. 测试输入与输出空槽位确定性拦截 (ACC-003) TEST_F(CAbiSafetyTest, NullSlotInBatchInputsOrOutputs) { - std::string cfg = GetConfigPath("configs/pipeline_keyword_match_rules.json"); + std::string cfg = GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, ¶m), 0); @@ -179,32 +180,31 @@ TEST_F(CAbiSafetyTest, NullSlotInBatchInputsOrOutputs) { EXPECT_EQ(Alg_Destroy(handle), 0); } -// 6. 测试 Adapter 注册冲突防护与 Descriptor 机器可读性 (ACC-005) -TEST_F(CAbiSafetyTest, AdapterRegistryConflictDetectionAndDescriptor) { - auto& registry = llm_edgeflow::BizAdapterRegistry::Instance(); - auto doc_adapter = registry.GetAdapter(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(doc_adapter, nullptr); - - // 验证 Descriptor - const auto& desc = doc_adapter->GetDescriptor(); - EXPECT_EQ(desc.biz_type, ALG_BIZ_TYPE_DOC_QA); - EXPECT_EQ(desc.adapter_name, "DocQA"); - EXPECT_EQ(desc.sdk_abi_version, COMPANY_ALG_ABI_VERSION); - EXPECT_GT(desc.max_batch_size, 0); - - // 测试重复 BizType 注册拦截 - bool reg_dup_ret = registry.RegisterAdapter(doc_adapter); - EXPECT_FALSE(reg_dup_ret) << "Duplicate biz_type registration must fail"; +// 6. 测试 IoBinding 注册冲突防护与定义机器可读性 +TEST_F(CAbiSafetyTest, IoBindingRegistryConflictDetectionAndDescriptor) { + auto& registry = llm_edgeflow::IoBindingRegistry::Instance(); + const auto* doc_binding = registry.FindBinding("doc_qa.cabi.v1"); + ASSERT_NE(doc_binding, nullptr); + + EXPECT_EQ(doc_binding->binding_id, "doc_qa.cabi.v1"); + EXPECT_EQ(doc_binding->biz_name, "smart_doc_qa_v1"); + EXPECT_EQ(doc_binding->transport, "cabi"); + EXPECT_GT(doc_binding->max_batch_size, 0); + + // 测试重复 binding 注册拦截 + bool reg_dup_ret = registry.RegisterBinding(*doc_binding); + EXPECT_FALSE(reg_dup_ret) << "Duplicate binding_id registration must fail"; + registry.ResetConflictForTesting(); } // 7. 测试 RuntimeOptions 与设备参数贯通 (ACC-004) TEST_F(CAbiSafetyTest, RuntimeOptionsAndDevicePropagation) { - std::string cfg = GetConfigPath("demo/fixtures/mock/pipeline_doc_qa.json"); + std::string cfg = + GetConfigPath("demo/fixtures/mock/pipeline_doc_qa_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg.c_str(); param.model_root_dir = "./models"; param.device_id = 0; // 显式指定设备 0 - param.biz_type = ALG_BIZ_TYPE_DOC_QA; void* handle0 = nullptr; ASSERT_EQ(Alg_Create(&handle0, ¶m), 0); @@ -216,60 +216,72 @@ TEST_F(CAbiSafetyTest, RuntimeOptionsAndDevicePropagation) { EXPECT_EQ(Alg_Destroy(handle1), 0); } -// 8. 测试 UNKNOWN 业务与未注册业务在 Alg_Create 前置拦截 (REV2-001) -TEST_F(CAbiSafetyTest, UnknownAndUnregisteredBizRejectionInCreate) { - std::string cfg = GetConfigPath("configs/pipeline_keyword_match_rules.json"); +// 8. 测试配置中未知/缺失 binding 在 Alg_Create 前置拦截 +TEST_F(CAbiSafetyTest, UnknownAndUnregisteredBindingRejectionInCreate) { + // 1) 传入不存在的接入配置 CompanyAlgParamCreate param; - param.config_file_path = cfg.c_str(); + param.config_file_path = "non_existent_cabi_config.json"; param.model_root_dir = "./models"; param.device_id = 0; - - // 1) 传入 ALG_BIZ_TYPE_UNKNOWN 必须被 Alg_Create 明确拒绝返回 -5 - param.biz_type = ALG_BIZ_TYPE_UNKNOWN; void* handle = nullptr; int ret = Alg_Create(&handle, ¶m); - EXPECT_EQ(ret, -5); + EXPECT_NE(ret, 0); EXPECT_EQ(handle, nullptr); - // 2) 传入越界/未注册业务枚举 9999 必须被 Alg_Create 明确拒绝返回 -5 - param.biz_type = static_cast(9999); + // 2) 传入缺失 io_binding 的配置 + std::string bad_cfg = "./results/test_missing_binding.json"; + std::filesystem::create_directories("./results"); + { + std::ofstream ofs(bad_cfg); + ofs << R"({"schema_version": 1, "data": {"pipe_path": "pipeline_keyword_match_rules.json"}})"; + } + param.config_file_path = bad_cfg.c_str(); ret = Alg_Create(&handle, ¶m); - EXPECT_EQ(ret, -5); + EXPECT_NE(ret, 0); EXPECT_EQ(handle, nullptr); + + // 3) 传入未知 io_binding + { + std::ofstream ofs(bad_cfg); + ofs << R"({"schema_version": 1, "data": {"pipe_path": "pipeline_keyword_match_rules.json", "io_binding": "unknown.binding.v999"}})"; + } + ret = Alg_Create(&handle, ¶m); + EXPECT_NE(ret, 0); + EXPECT_EQ(handle, nullptr); + std::filesystem::remove(bad_cfg); } -// 9. 测试 Registry 冲突 fail-closed 导致 Alg_Init 失败 (REV2-003) +// 9. 测试 Registry 冲突 fail-closed 导致 Alg_Init 失败 TEST_F(CAbiSafetyTest, FailClosedRegistryConflictAndInitFailure) { - auto& registry = llm_edgeflow::BizAdapterRegistry::Instance(); + auto& registry = llm_edgeflow::IoBindingRegistry::Instance(); registry.ResetConflictForTesting(); // 初始干净状态 Alg_Init 成功 EXPECT_EQ(Alg_Init(), 0); - // 注册冲突(重复注册 DocQA 业务) - auto doc_adapter = registry.GetAdapter(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(doc_adapter, nullptr); - bool reg_ret = registry.RegisterAdapter(doc_adapter); + // 注册冲突(重复注册 DocQA 绑定) + const auto* binding = registry.FindBinding("doc_qa.cabi.v1"); + ASSERT_NE(binding, nullptr); + bool reg_ret = registry.RegisterBinding(*binding); EXPECT_FALSE(reg_ret); - EXPECT_TRUE(registry.HasRegistrationConflict()); + EXPECT_TRUE(registry.HasConflict()); // 注册冲突发生后,Alg_Init 必须 fail-closed 返回 -6 EXPECT_EQ(Alg_Init(), -6); // 测试结束后清理恢复干净状态 registry.ResetConflictForTesting(); - EXPECT_FALSE(registry.HasRegistrationConflict()); + EXPECT_FALSE(registry.HasConflict()); EXPECT_EQ(Alg_Init(), 0); } -// 10. 测试 Adapter Descriptor max_batch_size 契约强制执行 (REV2-005) +// 10. 测试有效批次上限契约强制执行 TEST_F(CAbiSafetyTest, AdapterDescriptorMaxBatchSizeEnforcement) { - std::string cfg = GetConfigPath("configs/pipeline_keyword_match_rules.json"); + std::string cfg = GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, ¶m), 0); @@ -294,12 +306,11 @@ TEST_F(CAbiSafetyTest, AdapterDescriptorMaxBatchSizeEnforcement) { // 11. 同一 handle 的并发 Process 由接入适配层串行化,停流 join 后才允许 Destroy TEST_F(CAbiSafetyTest, SameHandleConcurrentProcessAndQuiescedDestroy) { - std::string cfg = GetConfigPath("configs/pipeline_keyword_match_rules.json"); + std::string cfg = GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, ¶m), 0); @@ -346,9 +357,10 @@ TEST_F(CAbiSafetyTest, SameHandleConcurrentProcessAndQuiescedDestroy) { // 12. RFC-0053: Entity 失败样本在结构化校验失败时,先写 request_id,但 status // 与 entities_json 保留原调用者哨兵值 TEST_F(CAbiSafetyTest, EntityFailureSampleSentinelValues) { - auto adapter = llm_edgeflow::BizAdapterRegistry::Instance().GetAdapter( - ALG_BIZ_TYPE_ENTITY_EXTRACT); - ASSERT_NE(adapter, nullptr); + const auto* out_conv = + llm_edgeflow::IoConverterRegistry::Instance().FindOutputConverter( + "document.structured.cabi.v1"); + ASSERT_NE(out_conv, nullptr); llm_edgeflow::AlgContext ctx; ctx.Publish(llm_edgeflow::kRawRequestIds, std::vector{1001, 2002}); @@ -370,9 +382,24 @@ TEST_F(CAbiSafetyTest, EntityFailureSampleSentinelValues) { std::strcpy(out1.entities_json, "SENTINEL_PAYLOAD"); void* outputs[2] = {&out0, &out1}; - int num_outputs = 2; + llm_edgeflow::ExternalOutputBatchView out_view; + out_view.items = outputs; + out_view.count = 2; + out_view.capacity = 2; + out_view.type_id = out_conv->external_type; + + llm_edgeflow::OutputEncodeOptions options; + options.binding_id = "entity_extract.cabi.v1"; + options.converter_id = out_conv->converter_id; + options.transport = "cabi"; + + llm_edgeflow::OutputPortBindings bindings( + {{"raw_request_ids", "raw_request_ids"}, + {"extracted_entities", "extracted_entities"}}); + size_t written_count = 0; llm_edgeflow::AdapterStatus status; - int ret = adapter->Pack(&ctx, outputs, &num_outputs, &status); + int ret = out_conv->encode_fn(&ctx, bindings, options, &out_view, + &written_count, &status); EXPECT_EQ(ret, COMPANY_ALG_ERR_INVALID_INPUT); EXPECT_EQ(out0.request_id, 1001u); diff --git a/tests/contract/architecture/test_layer_header_views.cmake b/tests/contract/architecture/test_layer_header_views.cmake index df671c71..90d55791 100644 --- a/tests/contract/architecture/test_layer_header_views.cmake +++ b/tests/contract/architecture/test_layer_header_views.cmake @@ -38,8 +38,11 @@ check_header(capability_nodes core/validated_node_plan.h TRUE) check_header(capability_nodes engine/text/utf8.h TRUE) check_header(orchestration core/pipeline.h TRUE) check_header(integration adapter/operator/operator_config_resolver.h TRUE) -check_header(integration adapter/operator_biz_bridge.h TRUE) -check_header(integration adapter/adapter_authoring.h TRUE) +check_header(integration adapter/io_converter.h TRUE) +check_header(integration adapter/io_binding.h TRUE) +check_header(integration adapter/io_converter_registry.h TRUE) +check_header(integration adapter/io_binding_registry.h TRUE) +check_header(integration adapter/converter_authoring.h TRUE) check_header(integration adapter/adapter_result.h TRUE) check_header(integration adapter/adapter_batch.h TRUE) check_header(integration adapter/text_carrier.h TRUE) @@ -53,8 +56,11 @@ 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/io_converter.h FALSE) +check_header(public adapter/io_binding.h FALSE) +check_header(public adapter/io_converter_registry.h FALSE) +check_header(public adapter/io_binding_registry.h FALSE) +check_header(public adapter/converter_authoring.h FALSE) check_header(public adapter/adapter_result.h FALSE) check_header(public adapter/adapter_batch.h FALSE) check_header(public adapter/text_carrier.h FALSE) @@ -63,16 +69,17 @@ check_header(public adapter/operator/operator_config_resolver.h FALSE) check_header(public core/pipeline.h FALSE) check_header(public nodes/node_base.h FALSE) check_header(public engine/tensor.h FALSE) -check_header(extension adapter/biz_adapter_interface.h TRUE) -check_header(extension adapter/operator_biz_bridge.h TRUE) -check_header(extension adapter/adapter_authoring.h TRUE) +check_header(extension adapter/io_converter.h TRUE) +check_header(extension adapter/io_binding.h TRUE) +check_header(extension adapter/io_converter_registry.h TRUE) +check_header(extension adapter/io_binding_registry.h TRUE) +check_header(extension adapter/converter_authoring.h TRUE) check_header(extension adapter/adapter_result.h TRUE) check_header(extension adapter/adapter_batch.h TRUE) check_header(extension adapter/text_carrier.h TRUE) check_header(extension nodes/node_base.h TRUE) check_header(extension engine/model_interface.h TRUE) check_header(extension adapter/shared_algorithm_runtime.h FALSE) -check_header(extension adapter/operator/operator_biz_bridge_registry.h FALSE) check_header(extension adapter/operator/operator_output_pool.h FALSE) foreach(header alg_types.h error_codes.h operator_data_types.h operator_types.h) @@ -84,8 +91,9 @@ foreach(header alg_types.h error_codes.h operator_data_types.h operator_types.h) endforeach() foreach(layer model_execution capability_nodes orchestration) - check_header(${layer} adapter/biz_adapter_interface.h FALSE) - check_header(${layer} adapter/adapter_authoring.h FALSE) + check_header(${layer} adapter/io_converter.h FALSE) + check_header(${layer} adapter/io_binding.h FALSE) + check_header(${layer} adapter/converter_authoring.h FALSE) check_header(${layer} adapter/adapter_result.h FALSE) check_header(${layer} adapter/adapter_batch.h FALSE) check_header(${layer} adapter/text_carrier.h FALSE) diff --git a/tests/contract/catalog/test_catalog_contract_ssot.cpp b/tests/contract/catalog/test_catalog_contract_ssot.cpp index 2353611c..03ab68df 100644 --- a/tests/contract/catalog/test_catalog_contract_ssot.cpp +++ b/tests/contract/catalog/test_catalog_contract_ssot.cpp @@ -7,7 +7,8 @@ #include #include -#include "adapter/biz_adapter_registry.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_catalog.h" #include "core/node_interface.h" #include "core/node_registry.h" #include "core/pipeline_catalog.h" @@ -74,20 +75,14 @@ TEST_F(CatalogContractSsotTest, AllProductionNodesHaveValidDefinitions) { } TEST_F(CatalogContractSsotTest, BizContractsDoNotDependOnDeploymentVariants) { - for (const auto type : {ALG_BIZ_TYPE_ENTITY_EXTRACT, ALG_BIZ_TYPE_DOC_QA}) { - const auto adapter = BizAdapterRegistry::Instance().GetAdapter(type); - ASSERT_NE(adapter, nullptr); - EXPECT_EQ(adapter->GetDescriptor().biz_definitions.size(), 1U); + for (const char* biz : {"entity_extract_v1", "smart_doc_qa_v1"}) { + EXPECT_TRUE(PipelineCatalog::FindBiz(biz).has_value()); } for (const char* name : {"entity_extract_0.6b_v1", "entity_extract_llamacpp_0.6b_v1", "smart_doc_qa_onnx_llamacpp_v1", "smart_doc_qa_rerank_llm_v1"}) { EXPECT_FALSE(PipelineCatalog::FindBiz(name)); - const auto adapter = BizAdapterRegistry::Instance().GetAdapter( - std::string(name).find("entity") == 0 ? ALG_BIZ_TYPE_ENTITY_EXTRACT - : ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(adapter, nullptr); - EXPECT_FALSE(adapter->ValidatePipelineBinding(name)); + EXPECT_EQ(IoBindingRegistry::Instance().FindBinding(name), nullptr); } } @@ -260,6 +255,28 @@ TEST_F(CatalogContractSsotTest, ToJsonSerializationAndFiltering) { EXPECT_TRUE(found_match_node); } +// 5b. 验证 IoCatalog Schema 4 聚合与对外规范性 +TEST_F(CatalogContractSsotTest, IoCatalogSchema4SerializationAndFiltering) { + auto full_catalog = IoCatalog::ToJson(); + EXPECT_EQ(full_catalog["schema_version"], 4); + EXPECT_TRUE(full_catalog["nodes"].is_array()); + EXPECT_TRUE(full_catalog["models"].is_array()); + EXPECT_TRUE(full_catalog["backends"].is_array()); + EXPECT_TRUE(full_catalog["bizs"].is_array()); + EXPECT_TRUE(full_catalog["input_converters"].is_array()); + EXPECT_TRUE(full_catalog["output_converters"].is_array()); + EXPECT_TRUE(full_catalog["io_bindings"].is_array()); + + // 业务过滤查询 + auto km_catalog = IoCatalog::ToJson("keyword_match_v1"); + EXPECT_EQ(km_catalog["schema_version"], 4); + EXPECT_TRUE(km_catalog["bizs"].is_array()); + EXPECT_TRUE(km_catalog["nodes"].is_array()); + EXPECT_TRUE(km_catalog["input_converters"].is_array()); + EXPECT_TRUE(km_catalog["output_converters"].is_array()); + EXPECT_TRUE(km_catalog["io_bindings"].is_array()); +} + // R6: 并发同名注册只有一个成功,另一方失败锁存 TEST_F(CatalogContractSsotTest, ConcurrentSameNameRegistrationSingleWinner) { test_support::RegistryTestAccess::ScopedNodeState scoped; diff --git a/tests/e2e/real_models/test_real_models_e2e.cpp b/tests/e2e/real_models/test_real_models_e2e.cpp index 3d10ac03..9f105492 100644 --- a/tests/e2e/real_models/test_real_models_e2e.cpp +++ b/tests/e2e/real_models/test_real_models_e2e.cpp @@ -124,14 +124,13 @@ TEST_F(RealModelE2ETest, RealModelCAbiEndToEnd) { ASSERT_EQ(Alg_Init(), 0); const std::string cfg_path = - (project_root_ / "configs/pipeline_entity_extract_cpu.json").string(); + (project_root_ / "configs/pipeline_entity_extract_cabi.json").string(); const std::string model_root = model_root_.string(); CompanyAlgParamCreate create_param; create_param.config_file_path = cfg_path.c_str(); create_param.model_root_dir = model_root.c_str(); create_param.device_id = 0; - create_param.biz_type = ALG_BIZ_TYPE_ENTITY_EXTRACT; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, &create_param), 0); 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 b2f37519..50f67d7e 100644 --- a/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_fixture.conf +++ b/tests/fixtures/pipelines/cross_rerank/pipeline_cross_rerank_fixture.conf @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { "pipe_path": "pipeline_cross_rerank_fixture.json", + "io_binding": "cross_rerank.operator.v1", "model_paths": { "rerank_model_v1": "rerank_fixture.onnx" }, 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 300a32b7..959b02c7 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 @@ -1,6 +1,8 @@ { + "schema_version": 1, "data": { "pipe_path": "pipeline_cross_rerank_fixture.json", + "io_binding": "cross_rerank.operator.v1", "model_paths": { "rerank_model_v1": "missing_rerank_fixture.onnx" }, diff --git a/tests/integration/demo/test_demo_runner.cpp b/tests/integration/demo/test_demo_runner.cpp index 3aa6b44c..dd6bb9b7 100644 --- a/tests/integration/demo/test_demo_runner.cpp +++ b/tests/integration/demo/test_demo_runner.cpp @@ -551,69 +551,74 @@ TEST(DemoRunnerTest, RegistryLookupAndConflictDetection) { } } restore{reg.ListDescriptors()}; - // 验证 7 大业务均已静态注册且包含权威的 CompanyAlgBizType + // 验证 7 大业务均已静态注册且包含权威的 expected_binding_id const auto* desc_entity = reg.Find("entity_extract"); ASSERT_NE(desc_entity, nullptr); - EXPECT_EQ(desc_entity->biz_type, ALG_BIZ_TYPE_ENTITY_EXTRACT); - EXPECT_EQ(DemoBizToBizType("entity_extract"), ALG_BIZ_TYPE_ENTITY_EXTRACT); + EXPECT_EQ(desc_entity->expected_binding_id, "entity_extract.operator.v1"); + EXPECT_EQ(DemoBizToExpectedBindingId("entity_extract"), + "entity_extract.operator.v1"); const auto* desc_keyword = reg.Find("keyword_match"); ASSERT_NE(desc_keyword, nullptr); - EXPECT_EQ(desc_keyword->biz_type, ALG_BIZ_TYPE_KEYWORD_MATCH); - EXPECT_EQ(DemoBizToBizType("keyword_match"), ALG_BIZ_TYPE_KEYWORD_MATCH); + EXPECT_EQ(desc_keyword->expected_binding_id, "keyword_match.operator.v1"); + EXPECT_EQ(DemoBizToExpectedBindingId("keyword_match"), + "keyword_match.operator.v1"); const auto* desc_doc_qa = reg.Find("doc_qa"); ASSERT_NE(desc_doc_qa, nullptr); - EXPECT_EQ(desc_doc_qa->biz_type, ALG_BIZ_TYPE_DOC_QA); - EXPECT_EQ(DemoBizToBizType("doc_qa"), ALG_BIZ_TYPE_DOC_QA); + EXPECT_EQ(desc_doc_qa->expected_binding_id, "doc_qa.operator.v1"); + EXPECT_EQ(DemoBizToExpectedBindingId("doc_qa"), "doc_qa.operator.v1"); const auto* desc_audit = reg.Find("dialogue_audit"); ASSERT_NE(desc_audit, nullptr); - EXPECT_EQ(desc_audit->biz_type, ALG_BIZ_TYPE_COMPLIANCE_AUDIT); - EXPECT_EQ(DemoBizToBizType("dialogue_audit"), ALG_BIZ_TYPE_COMPLIANCE_AUDIT); + EXPECT_EQ(desc_audit->expected_binding_id, "compliance_audit.operator.v1"); + EXPECT_EQ(DemoBizToExpectedBindingId("dialogue_audit"), + "compliance_audit.operator.v1"); const auto* desc_ocr = reg.Find("ocr_doc_qa"); ASSERT_NE(desc_ocr, nullptr); - EXPECT_EQ(desc_ocr->biz_type, ALG_BIZ_TYPE_OCR_DOC_QA); - EXPECT_EQ(DemoBizToBizType("ocr_doc_qa"), ALG_BIZ_TYPE_OCR_DOC_QA); + EXPECT_EQ(desc_ocr->expected_binding_id, "ocr_doc_qa.operator.v1"); + EXPECT_EQ(DemoBizToExpectedBindingId("ocr_doc_qa"), "ocr_doc_qa.operator.v1"); const auto* desc_asr = reg.Find("audio_asr"); ASSERT_NE(desc_asr, nullptr); - EXPECT_EQ(desc_asr->biz_type, ALG_BIZ_TYPE_AUDIO_ASR_INTENT); - EXPECT_EQ(DemoBizToBizType("audio_asr"), ALG_BIZ_TYPE_AUDIO_ASR_INTENT); + EXPECT_EQ(desc_asr->expected_binding_id, "audio_asr_intent.operator.v1"); + EXPECT_EQ(DemoBizToExpectedBindingId("audio_asr"), + "audio_asr_intent.operator.v1"); const auto* desc_rerank = reg.Find("cross_rerank"); ASSERT_NE(desc_rerank, nullptr); - EXPECT_EQ(desc_rerank->biz_type, ALG_BIZ_TYPE_CROSS_RERANK); - EXPECT_EQ(DemoBizToBizType("cross_rerank"), ALG_BIZ_TYPE_CROSS_RERANK); + EXPECT_EQ(desc_rerank->expected_binding_id, "cross_rerank.operator.v1"); + EXPECT_EQ(DemoBizToExpectedBindingId("cross_rerank"), + "cross_rerank.operator.v1"); // 尝试重复注册已存在的业务名 -> 应该失败 bool ok = reg.Register({"entity_extract", "Duplicate", [](const DemoOptions&) { return 0; }, - ALG_BIZ_TYPE_ENTITY_EXTRACT}); + "entity_extract.operator.v1"}); EXPECT_FALSE(ok); EXPECT_TRUE(reg.HasConflict()); // 尝试注册非法空业务名 -> 应该失败 ok = reg.Register({"", "Empty", [](const DemoOptions&) { return 0; }, - ALG_BIZ_TYPE_ENTITY_EXTRACT}); + "entity_extract.operator.v1"}); EXPECT_FALSE(ok); // 尝试注册空函数 -> 应该失败 ok = reg.Register( - {"dummy_new", "NullFunc", nullptr, ALG_BIZ_TYPE_ENTITY_EXTRACT}); + {"dummy_new", "NullFunc", nullptr, "entity_extract.operator.v1"}); EXPECT_FALSE(ok); EXPECT_FALSE(reg.Register({"missing_type", "Invalid type", - [](const DemoOptions&) { return 0; }, - ALG_BIZ_TYPE_UNKNOWN})); + [](const DemoOptions&) { return 0; }, ""})); EXPECT_EQ(reg.Find("missing_type"), nullptr); reg.ResetForTesting(); // Registered names have no fallback in the central runner. - EXPECT_EQ(DemoBizToBizType("entity_extract"), ALG_BIZ_TYPE_UNKNOWN); + EXPECT_EQ(DemoBizToExpectedBindingId("entity_extract"), ""); EXPECT_TRUE(reg.Register({"new_domain_alias", "Domain", [](const DemoOptions&) { return 0; }, - ALG_BIZ_TYPE_ENTITY_EXTRACT})); - EXPECT_EQ(DemoBizToBizType("new_domain_alias"), ALG_BIZ_TYPE_ENTITY_EXTRACT); + "entity_extract.operator.v1"})); + EXPECT_EQ(DemoBizToExpectedBindingId("new_domain_alias"), + "entity_extract.operator.v1"); } TEST(DemoRunnerTest, @@ -814,18 +819,18 @@ TEST(DemoRunnerTest, ConfigBizMatchValidation) { // 错误匹配 -> 快速失败 EXPECT_FALSE(ValidateConfigBizMatch("demo/fixtures/mock/pipeline_doc_qa.conf", "entity_extract", &err)); - EXPECT_NE(err.find("Biz mismatch"), std::string::npos); + EXPECT_NE(err.find("Binding mismatch"), std::string::npos); // P1-3: cross_rerank 绝不应该匹配 doc_qa_rerank (即使名字里有 rerank) EXPECT_FALSE(ValidateConfigBizMatch( "demo/fixtures/mock/pipeline_doc_qa_rerank.conf", "cross_rerank", &err)); - EXPECT_NE(err.find("Biz mismatch"), std::string::npos); + EXPECT_NE(err.find("Binding mismatch"), std::string::npos); // P1-2 探针测试: .conf 中 pipe_path 为数字 123 (必须返回 false,不抛异常崩溃) std::string bad_conf_path = "./results/bad_pipe_path.conf"; { std::ofstream ofs(bad_conf_path); - ofs << R"({"data": {"pipe_path": 123}})"; + ofs << R"({"schema_version": 1, "data": {"pipe_path": 123, "io_binding": "keyword_match.operator.v1"}})"; } EXPECT_FALSE(ValidateConfigBizMatch(bad_conf_path, "keyword_match", &err)); EXPECT_NE(err.find("pipe_path"), std::string::npos); @@ -840,19 +845,17 @@ TEST(DemoRunnerTest, ConfigBizMatchValidation) { EXPECT_EQ( ValidateOperatorConfigBinding( root_dir.c_str(), "demo/fixtures/mock/pipeline_entity_extract.conf", - static_cast(ALG_BIZ_TYPE_ENTITY_EXTRACT), err_buf, - sizeof(err_buf)), + "entity_extract.operator.v1", err_buf, sizeof(err_buf)), 0); EXPECT_EQ( ValidateOperatorConfigBinding( root_dir.c_str(), "demo/fixtures/mock/pipeline_entity_extract.conf", - static_cast(ALG_BIZ_TYPE_DOC_QA), err_buf, sizeof(err_buf)), + "doc_qa.operator.v1", err_buf, sizeof(err_buf)), -3); - EXPECT_EQ( - ValidateOperatorConfigBinding( - root_dir.c_str(), "non_existent_conf_file.conf", - static_cast(ALG_BIZ_TYPE_DOC_QA), err_buf, sizeof(err_buf)), - -2); + EXPECT_EQ(ValidateOperatorConfigBinding( + root_dir.c_str(), "non_existent_conf_file.conf", + "doc_qa.operator.v1", err_buf, sizeof(err_buf)), + -2); } // P1-2: 测试显式指定不存在或非法的 Control 文件 Fail-Closed diff --git a/tests/integration/operator/test_operator_api.cpp b/tests/integration/operator/test_operator_api.cpp index 1eb2340e..99b6c50a 100644 --- a/tests/integration/operator/test_operator_api.cpp +++ b/tests/integration/operator/test_operator_api.cpp @@ -13,12 +13,13 @@ #include #include -#include "adapter/biz_adapter_registry.h" #include "adapter/biz_blackboard_keys.h" -#include "adapter/operator/operator_biz_bridge_registry.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter_registry.h" #include "adapter/operator/operator_config_resolver.h" #include "adapter/operator/operator_value_type_registry.h" #include "core/common_contracts.h" +#include "core/pipeline_catalog.h" #include "edgeflow/c_api.h" #include "edgeflow/operator/interface.h" #include "edgeflow/operator/types.h" @@ -883,22 +884,19 @@ TEST_F(OperatorApiTest, ValidateOperatorConfigBindingApi) { // 1. 正常校验 EXPECT_EQ(ValidateOperatorConfigBinding( root_dir.c_str(), "configs/pipeline_keyword_match_rules.conf", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + "keyword_match.operator.v1", err_buf, sizeof(err_buf)), 0); // 2. 业务不匹配 - EXPECT_EQ( - ValidateOperatorConfigBinding( - root_dir.c_str(), "configs/pipeline_keyword_match_rules.conf", - static_cast(ALG_BIZ_TYPE_DOC_QA), err_buf, sizeof(err_buf)), - -3); + EXPECT_EQ(ValidateOperatorConfigBinding( + root_dir.c_str(), "configs/pipeline_keyword_match_rules.conf", + "doc_qa.operator.v1", err_buf, sizeof(err_buf)), + -3); // 3. 相对路径传入绝对路径 / 逃逸 - EXPECT_EQ(ValidateOperatorConfigBinding( - root_dir.c_str(), "/etc/passwd", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + EXPECT_EQ(ValidateOperatorConfigBinding(root_dir.c_str(), "/etc/passwd", + "keyword_match.operator.v1", err_buf, + sizeof(err_buf)), -2); } @@ -1190,8 +1188,10 @@ TEST_F(OperatorApiTest, OutputsConfigValidationFailClosed) { { std::ofstream ofs(conf_path); ofs << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "mem_que": { "type": "keyword_out" } @@ -1209,8 +1209,10 @@ TEST_F(OperatorApiTest, OutputsConfigValidationFailClosed) { { std::ofstream ofs(conf_path); ofs << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json" + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1" } })"; } @@ -1220,8 +1222,10 @@ TEST_F(OperatorApiTest, OutputsConfigValidationFailClosed) { { std::ofstream ofs(conf_path); ofs << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "outputs": { "keyword_out": { "type": "doc_out" @@ -1236,8 +1240,10 @@ TEST_F(OperatorApiTest, OutputsConfigValidationFailClosed) { { std::ofstream ofs(conf_path); ofs << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "outputs": { "keyword_out": { "type": "keyword_out", @@ -1254,8 +1260,10 @@ TEST_F(OperatorApiTest, OutputsConfigValidationFailClosed) { { std::ofstream ofs(conf_path); ofs << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "outputs": { "keyword_out": { "type": "keyword_out", @@ -1289,8 +1297,10 @@ TEST_F(OperatorApiTest, OutputsConfigValidationFailClosed) { { std::ofstream ofs(conf_path); ofs << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "model_path": "models/unused.bin", "outputs": { "keyword_out": { @@ -1308,8 +1318,10 @@ TEST_F(OperatorApiTest, OutputsConfigValidationFailClosed) { { std::ofstream ofs(conf_path); ofs << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "outputs": { "keyword_out": { "type": "keyword_out", @@ -1384,50 +1396,43 @@ TEST_F(OperatorApiTest, PathSandboxStrictBoundaries) { // 1. POSIX 绝对路径拒绝 EXPECT_EQ(ValidateOperatorConfigBinding( root_dir.c_str(), "/etc/pipeline.conf", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + "keyword_match.operator.v1", err_buf, sizeof(err_buf)), -2); // 2. Windows 盘符拒绝 - EXPECT_EQ(ValidateOperatorConfigBinding( - root_dir.c_str(), "C:\\pipeline.conf", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + EXPECT_EQ(ValidateOperatorConfigBinding(root_dir.c_str(), "C:\\pipeline.conf", + "keyword_match.operator.v1", err_buf, + sizeof(err_buf)), -2); // 3. UNC 路径拒绝 EXPECT_EQ(ValidateOperatorConfigBinding( root_dir.c_str(), "\\\\server\\share\\pipeline.conf", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + "keyword_match.operator.v1", err_buf, sizeof(err_buf)), -2); // 4. .. 逃逸拒绝 - EXPECT_EQ(ValidateOperatorConfigBinding( - root_dir.c_str(), "../../etc/passwd", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + EXPECT_EQ(ValidateOperatorConfigBinding(root_dir.c_str(), "../../etc/passwd", + "keyword_match.operator.v1", err_buf, + sizeof(err_buf)), -2); // 5. 目录而非普通文件拒绝 - EXPECT_EQ(ValidateOperatorConfigBinding( - root_dir.c_str(), "configs", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + EXPECT_EQ(ValidateOperatorConfigBinding(root_dir.c_str(), "configs", + "keyword_match.operator.v1", err_buf, + sizeof(err_buf)), -2); // 6. 不存在的文件拒绝 EXPECT_EQ(ValidateOperatorConfigBinding( root_dir.c_str(), "configs/non_existent.conf", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + "keyword_match.operator.v1", err_buf, sizeof(err_buf)), -2); // 7. 路径前缀混淆拒绝 (例如目标根为 root,试图访问 root_extra 目录) EXPECT_EQ(ValidateOperatorConfigBinding( root_dir.c_str(), "../configs_fake/pipeline.conf", - static_cast(ALG_BIZ_TYPE_KEYWORD_MATCH), err_buf, - sizeof(err_buf)), + "keyword_match.operator.v1", err_buf, sizeof(err_buf)), -2); // 8. 对 Create 接口同样严格拦截非普通文件与不存在文件 @@ -1906,8 +1911,10 @@ TEST_F(OperatorApiTest, ModelPathNonExistentFileAllowedWhileEscapeRejected) { root / "configs/pipeline_doc_qa_default.json"); std::ofstream conf(root / "configs/model_paths.conf"); conf << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_doc_qa_default.json", + "pipe_path": "pipeline_doc_qa_default.json", + "io_binding": "doc_qa.operator.v1", "model_paths": { "embed_model_v1": "models/not_deployed_embed.bin", "llm_model_v1": "models/not_deployed_llm.bin" @@ -1949,8 +1956,10 @@ TEST_F(OperatorApiTest, ModelPathNonExistentFileAllowedWhileEscapeRejected) { root / "configs/pipeline_audio_asr_intent.json"); std::ofstream conf(root / "configs/single_model.conf"); conf << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_audio_asr_intent.json", + "pipe_path": "pipeline_audio_asr_intent.json", + "io_binding": "audio_asr_intent.operator.v1", "model_paths": { "asr_model_v1": "deployment/asr_model_will_arrive_later.bin" }, @@ -2083,8 +2092,10 @@ TEST_F(OperatorApiTest, ModelPathNonExistentFileAllowedWhileEscapeRejected) { std::string("configs/outside_pipeline.json")}) { std::ofstream invalid_conf(root / "configs/invalid_pipe.conf"); invalid_conf << nlohmann::json( - {{"data", + {{"schema_version", 1}, + {"data", {{"pipe_path", pipe_path}, + {"io_binding", "keyword_match.operator.v1"}, {"outputs", {{"keyword_out", {{"type", "keyword_out"}}}}}}}}); invalid_conf.close(); EXPECT_EQ(llm_edgeflow::OperatorConfigResolver::Resolve( @@ -2104,8 +2115,10 @@ TEST_F(OperatorApiTest, DotDotPrefixedControlFileNamesStayWithinRoot) { "configs/pipeline_keyword_match_rules.json", root / "..configs/pipeline.json"); std::ofstream(root / "..configs/pipeline.conf") << nlohmann::json( - {{"data", - {{"pipe_path", "..configs/pipeline.json"}, + {{"schema_version", 1}, + {"data", + {{"pipe_path", "pipeline.json"}, + {"io_binding", "keyword_match.operator.v1"}, {"outputs", {{"keyword_out", {{"type", "keyword_out"}}}}}}}}); llm_edgeflow::ResolvedOperatorConfig resolved; @@ -2135,8 +2148,10 @@ TEST_F(OperatorApiTest, VariableResultsUsePoolCapacityAndRollbackOnFailure) { {"config", {{"categories", {{"LONG", {word}}}}}}}}}}; std::ofstream(temp.path() / "pipeline.json") << pipeline; std::ofstream(temp.path() / "pipeline.conf") << nlohmann::json( - {{"data", + {{"schema_version", 1}, + {"data", {{"pipe_path", "pipeline.json"}, + {"io_binding", "keyword_match.operator.v1"}, {"outputs", {{"keyword_out", {{"type", "keyword_out"}, @@ -2199,8 +2214,10 @@ TEST_F(OperatorApiTest, MetadataTypeIdOutOfInt32RangeIsRejected) { { std::ofstream conf(root / "configs/pipe_overflow.conf"); conf << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "outputs": { "keyword_out": { "type": "keyword_out", @@ -2224,8 +2241,10 @@ TEST_F(OperatorApiTest, MetadataTypeIdOutOfInt32RangeIsRejected) { { std::ofstream conf(root / "configs/pipe_underflow.conf"); conf << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "outputs": { "keyword_out": { "type": "keyword_out", @@ -2249,8 +2268,10 @@ TEST_F(OperatorApiTest, MetadataTypeIdOutOfInt32RangeIsRejected) { { std::ofstream conf(root / "configs/pipe_not_integer.conf"); conf << R"({ + "schema_version": 1, "data": { - "pipe_path": "configs/pipeline_keyword_match_rules.json", + "pipe_path": "pipeline_keyword_match_rules.json", + "io_binding": "keyword_match.operator.v1", "outputs": { "keyword_out": { "type": "keyword_out", @@ -2274,59 +2295,6 @@ TEST_F(OperatorApiTest, MetadataTypeIdOutOfInt32RangeIsRejected) { namespace llm_edgeflow::test_support { namespace { -constexpr auto kNestedOutputBiz = static_cast(49001); - -// Reuse the complete keyword C ABI contract. Only the test Operator bridge has -// additional external output carriers; the algorithm and public ABI stay real. -class NestedOutputTestAdapter final : public IBizAdapter { - public: - CompanyAlgBizType BizType() const override { return kNestedOutputBiz; } - const char* AdapterName() const override { return "NestedOutputTest"; } - const char* ResultTypeName() const override { - return KeywordResult::kTypeName; - } - - const AdapterDescriptor& GetDescriptor() const override { - static const AdapterDescriptor descriptor{ - kNestedOutputBiz, - "NestedOutputTest", - COMPANY_ALG_ABI_VERSION, - "CompanyKeywordInputStruct", - "CompanyKeywordOutputStruct", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {{"test_nested_output_v1", - "test_nested_output", - "Test registered nested output allocation", - {RequiredBizInput(kRawRequestIds), RequiredBizInput(kInputSentences)}, - {BizOutput(kRuleMatches)}}}}; - return descriptor; - } - - int Unpack(const void** inputs, int count, AlgContext* context, - AdapterStatus* status) const override { - return KeywordAdapter()->Unpack(inputs, count, context, status); - } - int Pack(AlgContext* context, void** outputs, int* count, - AdapterStatus* status) const override { - return KeywordAdapter()->Pack(context, outputs, count, status); - } - int PackResultBatch(AlgContext* context, void** outputs, int* count, - AdapterStatus* status) const override { - return KeywordAdapter()->PackResultBatch(context, outputs, count, status); - } - - private: - static std::shared_ptr KeywordAdapter() { - return BizAdapterRegistry::Instance().GetAdapter( - ALG_BIZ_TYPE_KEYWORD_MATCH); - } -}; - -REGISTER_BIZ_ADAPTER(NestedOutputTestAdapter); - void RegisterNestedOutputTestTypes() { RegisterOperatorValueType(MakeNestedOutputBinding()); RegisterOperatorOutputAllocator("test_nested_standard", @@ -2337,44 +2305,110 @@ void RegisterNestedOutputTestTypes() { REGISTER_OPERATOR_VALUE_TYPE(RegisterNestedOutputTestTypes); -void RegisterNestedOutputTestBridge() { - OperatorBizBridgeDescriptor bridge; - bridge.biz_type = kNestedOutputBiz; - bridge.adapter_name = "NestedOutputTest"; - bridge.registration_identity = "NestedOutputTestBridge"; - bridge.internal_input_type_name = "CompanyKeywordInputStruct"; - bridge.internal_output_type_name = KeywordResult::kTypeName; - bridge.input_slots = { - {"keyword_in", "keyword_in", IoDirection::kInput, true}}; - bridge.output_slots = {{"main", "test_nested_out", IoDirection::kOutput, true, - "result", ConvertNestedOutput}, - {"audit", "test_nested_out", IoDirection::kOutput, - true, "audit", ConvertNestedOutput}}; - bridge.convert_sample_input = - [](const std::unordered_map& slots, - ProcessLocalShadowStorage& storage, const void** internal, - std::string*) { - const auto* source = static_cast( - slots.at("keyword_in")); - auto* input = storage.AllocateShadowDto(); - input->request_id = source->request_id; - input->sentence_text = storage.StoreString(source->sentence_text); - *internal = input; - return 0; - }; - bridge.create_shadow_output_dto = - [](ProcessLocalShadowStorage& storage) -> void* { - return storage.AllocateShadowDto(); - }; - RegisterOperatorBizBridge(std::move(bridge)); +int EncodeNestedOutput(AlgContext* context, const OutputPortBindings& bindings, + const OutputEncodeOptions& options, + ExternalOutputBatchView* destination, + size_t* written_count, AdapterStatus* status) { + if (written_count) *written_count = 0; + if (!context || !destination) return -1; + const auto* req_ids = context->Read>( + bindings.GetActualKey("raw_request_ids")); + const auto* matches = + context->Read(bindings.GetActualKey("rule_matches")); + if (!req_ids || !matches) return -3; + size_t count = req_ids->size(); + + for (size_t i = 0; i < count; ++i) { + KeywordResult result; + result.request_id = (*req_ids)[i]; + result.is_hit = 0; + for (const auto& m : *matches) { + if (m.req_id == i) { + result.is_hit = m.data.is_hit; + break; + } + } + for (const char* slot_name : {"main", "audit"}) { + void* external = destination->GetSlot(slot_name, i); + const auto* spec = destination->GetPoolSpec(slot_name); + if (!external || !spec) return -4; + std::string error; + int ret = ConvertNestedOutput(&result, external, *spec, &error); + if (ret != 0) { + if (status) { + *status = AdapterStatus(ret, error, slot_name, static_cast(i), + options.converter_id); + } + return ret; + } + } + } + if (written_count) *written_count = count; + return 0; } -REGISTER_OPERATOR_BIZ_BRIDGE(RegisterNestedOutputTestBridge); +const bool g_reg_nested_output_components = []() { + BizDefinition bdef; + bdef.biz_name = "test_nested_output_v1"; + bdef.ingress = { + BizPortDefinition("raw_request_ids", "vector", true, "1:1"), + BizPortDefinition("input_sentences", "TextBatch", true, "1:1")}; + bdef.egress = { + BizPortDefinition("rule_matches", "RuleMatchBatch", true, "1:1")}; + if (!PipelineCatalog::FindBiz(bdef.biz_name)) { + PipelineCatalog::RegisterBizDefinition(bdef); + } + + OutputConverterDefinition odef; + odef.converter_id = "test_nested_output.operator.v1"; + odef.transport = "operator"; + odef.schema_id = "test_nested_output"; + odef.schema_version = 1; + odef.external_type = "test_nested_out"; + odef.max_batch_size = 64; + odef.external_slots = {ExternalSlotDefinition{"main", + "test_nested_out", + PortDirection::kOutput, + true, + "result", + "test_nested_out", + {}, + "result"}, + ExternalSlotDefinition{"audit", + "test_nested_out", + PortDirection::kOutput, + true, + "audit", + "test_nested_out", + {}, + "audit"}}; + odef.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("rule_matches", "RuleMatchBatch", true, "1:1")}; + odef.encode_fn = &EncodeNestedOutput; + IoConverterRegistry::Instance().RegisterOutputConverter(odef); + + IoBindingDefinition bind; + bind.binding_id = "nested_output_test.operator.v1"; + bind.biz_name = "test_nested_output_v1"; + bind.transport = "operator"; + bind.input_converter_id = "keyword.plain.operator.v1"; + bind.output_converter_id = "test_nested_output.operator.v1"; + bind.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}; + bind.output_ports = {{"raw_request_ids", "raw_request_ids"}, + {"rule_matches", "rule_matches"}}; + bind.max_batch_size = 64; + IoBindingRegistry::Instance().RegisterBinding(bind); + return true; +}(); nlohmann::json NestedOutputConfig(bool alternate = false) { return { + {"schema_version", 1}, {"data", {{"pipe_path", "pipeline.json"}, + {"io_binding", "nested_output_test.operator.v1"}, {"outputs", {{"main", {{"type", "test_nested_out"}, @@ -2672,15 +2706,27 @@ TEST_F(OperatorApiTest, SharedCarrierDoesNotMergePayloadSchema) { // Translate adapter rejects plain text because it requires JSON object with // "query" - auto translate_adapter = - llm_edgeflow::BizAdapterRegistry::Instance().GetAdapter( - ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(translate_adapter, nullptr); + const auto* translate_in_conv = + llm_edgeflow::IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(translate_in_conv, nullptr); CompanyEntityInputStruct c_in_plain{50001, plain_text.c_str()}; const void* translate_inputs[] = {&c_in_plain}; llm_edgeflow::AlgContext ctx; llm_edgeflow::AdapterStatus status; - EXPECT_EQ(translate_adapter->Unpack(translate_inputs, 1, &ctx, &status), + llm_edgeflow::ExternalInputBatchView view_plain; + view_plain.items = translate_inputs; + view_plain.count = 1; + view_plain.type_id = translate_in_conv->external_type; + llm_edgeflow::InputPortBindings port_bindings( + {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}); + llm_edgeflow::InputDecodeOptions decode_opts; + decode_opts.converter_id = translate_in_conv->converter_id; + decode_opts.transport = "cabi"; + decode_opts.max_batch_size = 64; + EXPECT_EQ(translate_in_conv->decode_fn(view_plain, decode_opts, port_bindings, + &ctx, &status), COMPANY_ALG_ERR_INVALID_INPUT); // 2. JSON text: Translate accepts and extracts "query" @@ -2688,9 +2734,13 @@ TEST_F(OperatorApiTest, SharedCarrierDoesNotMergePayloadSchema) { CompanyEntityInputStruct c_in_json{50002, json_text.c_str()}; const void* translate_valid_inputs[] = {&c_in_json}; llm_edgeflow::AlgContext valid_ctx; - EXPECT_EQ( - translate_adapter->Unpack(translate_valid_inputs, 1, &valid_ctx, &status), - COMPANY_ALG_SUCCESS); + llm_edgeflow::ExternalInputBatchView view_json; + view_json.items = translate_valid_inputs; + view_json.count = 1; + view_json.type_id = translate_in_conv->external_type; + EXPECT_EQ(translate_in_conv->decode_fn(view_json, decode_opts, port_bindings, + &valid_ctx, &status), + COMPANY_ALG_SUCCESS); const auto* queries = valid_ctx.Read(llm_edgeflow::kInputSentences); ASSERT_NE(queries, nullptr); EXPECT_EQ((*queries)[0].data, "有效翻译查询"); diff --git a/tests/integration/operator/test_operator_golden.cpp b/tests/integration/operator/test_operator_golden.cpp index c78037f3..cf589cb2 100644 --- a/tests/integration/operator/test_operator_golden.cpp +++ b/tests/integration/operator/test_operator_golden.cpp @@ -471,4 +471,50 @@ TEST_F(OperatorGoldenTest, CrossRerankGolden) { std::filesystem::remove_all(temp_dir, ec); } +// Golden Test 8: Translate (Biz 8) +TEST_F(OperatorGoldenTest, TranslateGolden) { + using namespace llm_edgeflow::operator_api; + CreateParam param{}; + param.model_path = "."; + param.cfg_file_name = "demo/fixtures/mock/pipeline_translate.conf"; + param.device_id = 0; + param.compute_platform = ComputePlatform::kCpu; + + void* handle = nullptr; + ASSERT_EQ(ops_.Create(&handle, ¶m), 0); + ASSERT_NE(handle, nullptr); + + std::string json_input = "{\"query\": \"Hello world\"}"; + CompanyString cs_text{static_cast(json_input.size()), + const_cast(json_input.data())}; + + CompanyOperatorEntityInput in{}; + in.request_id = 8001; + in.sentence_text = &cs_text; + + NamedIoBatch inputs(1); + inputs[0]["trans_channel.entity_in"] = MakeBorrowedOperatorInput(&in); + + NamedIoBatch outputs(1); + outputs[0]["trans_channel.entity_out"] = std::shared_ptr(); + + int p_ret = ops_.Process(handle, inputs, outputs); + ASSERT_EQ(p_ret, 0) << "Process error: " + << llm_edgeflow::operator_api::GetOperatorLastError(); + auto out_sp = outputs[0]["trans_channel.entity_out"]; + ASSERT_NE(out_sp, nullptr); + auto* out_dto = static_cast(out_sp.get()); + EXPECT_EQ(out_dto->request_id, 8001u); + EXPECT_EQ(out_dto->status_code, 0); + ASSERT_NE(out_dto->entities_json, nullptr); + EXPECT_GT(out_dto->entities_json->length, 0); + auto res_json = nlohmann::json::parse(std::string( + out_dto->entities_json->data, out_dto->entities_json->length)); + EXPECT_TRUE(res_json.contains("translated")); + + outputs.clear(); + out_sp.reset(); + EXPECT_EQ(ops_.Destroy(handle), 0); +} + } // namespace llm_edgeflow diff --git a/tests/integration/pipeline/test_pipeline_catalog_validator.cpp b/tests/integration/pipeline/test_pipeline_catalog_validator.cpp index 40c5a130..3054ffaa 100644 --- a/tests/integration/pipeline/test_pipeline_catalog_validator.cpp +++ b/tests/integration/pipeline/test_pipeline_catalog_validator.cpp @@ -6,6 +6,7 @@ #include #include +#include "adapter/io_binding_registry.h" #include "adapter/shared_algorithm_runtime.h" #include "core/common_contracts.h" #include "core/node_registry.h" @@ -94,7 +95,8 @@ TEST(PipelineValidatorTest, AllRepositoryPipelinesValidate) { for (const auto& entry : std::filesystem::directory_iterator(configs)) { const auto filename = entry.path().filename().string(); if (!entry.is_regular_file() || entry.path().extension() != ".json" || - filename.rfind("pipeline_", 0) != 0) { + filename.rfind("pipeline_", 0) != 0 || + filename.find("_cabi.json") != std::string::npos) { continue; } ++candidates; @@ -260,12 +262,21 @@ TEST(PipelineValidatorTest, TableDrivenParityMatrix) { // 3. The shared runtime must fail before materialization and preserve the // primary structured diagnostic in its internal C++ error boundary. + std::string biz = config.value("biz_name", ""); + std::string binding_id; + for (const auto& b : IoBindingRegistry::Instance().AllBindings()) { + if (b.biz_name == biz && b.transport == "cabi") { + binding_id = b.binding_id; + break; + } + } + if (binding_id.empty()) { + binding_id = "keyword_match.cabi.v1"; + } std::unique_ptr runtime; std::string runtime_error; int runtime_result = SharedAlgorithmRuntime::CreateFromPipelineJson( - config, 0, "./models", - static_cast(test["biz_type"].get()), &runtime, - &runtime_error); + config, 0, "./models", binding_id, &runtime, &runtime_error); EXPECT_EQ(runtime_result, test["runtime_error_code"].get()); EXPECT_EQ(runtime, nullptr); EXPECT_NE(runtime_error.find(test["primary_code"].get()), diff --git a/tests/integration/runtime/test_all_biz_pipelines.cpp b/tests/integration/runtime/test_all_biz_pipelines.cpp index fe7c3a65..eaba958b 100644 --- a/tests/integration/runtime/test_all_biz_pipelines.cpp +++ b/tests/integration/runtime/test_all_biz_pipelines.cpp @@ -30,12 +30,11 @@ class AllBizPipelinesTest : public ::testing::Test { // VectorSearch -> Prompt -> LLM) TEST_F(AllBizPipelinesTest, DocQaPipelineExecution) { std::string cfg_path = - GetConfigPath("demo/fixtures/mock/pipeline_doc_qa.json"); + GetConfigPath("demo/fixtures/mock/pipeline_doc_qa_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_DOC_QA; void* handle = nullptr; int ret = Alg_Create(&handle, ¶m); @@ -87,12 +86,11 @@ TEST_F(AllBizPipelinesTest, DocQaPipelineExecution) { // 2. 业务 4 (智能对话风控质检 - 3模型6节点级联) 细粒度高危与合规样本双向校验 TEST_F(AllBizPipelinesTest, DialogueComplianceAuditPipeline) { std::string cfg_path = - GetConfigPath("demo/fixtures/mock/pipeline_dialogue_audit.json"); + GetConfigPath("demo/fixtures/mock/pipeline_dialogue_audit_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_COMPLIANCE_AUDIT; void* handle = nullptr; int ret = Alg_Create(&handle, ¶m); diff --git a/tests/integration/runtime/test_concurrency_and_edge_cases.cpp b/tests/integration/runtime/test_concurrency_and_edge_cases.cpp index 448d7c3f..7e81a79b 100644 --- a/tests/integration/runtime/test_concurrency_and_edge_cases.cpp +++ b/tests/integration/runtime/test_concurrency_and_edge_cases.cpp @@ -39,7 +39,7 @@ TEST_F(ConcurrencyAndEdgeCasesTest, MultiThreadedConcurrentStressTest) { workers.reserve(num_threads); std::string cfg_path = - GetConfigPath("configs/pipeline_keyword_match_rules.json"); + GetConfigPath("configs/pipeline_keyword_match_cabi.json"); for (int t = 0; t < num_threads; ++t) { workers.emplace_back([&, t]() { @@ -47,7 +47,6 @@ TEST_F(ConcurrencyAndEdgeCasesTest, MultiThreadedConcurrentStressTest) { param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; for (int iter = 0; iter < iterations_per_thread; ++iter) { void* handle = nullptr; @@ -102,12 +101,11 @@ TEST_F(ConcurrencyAndEdgeCasesTest, EdgeCasesAndFaultTolerance) { // Case A: 畸形与非法 JSON 传入 Alg_Control { std::string cfg_path = - GetConfigPath("configs/pipeline_keyword_match_rules.json"); + GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; int ret = Alg_Create(&handle, ¶m); @@ -136,12 +134,11 @@ TEST_F(ConcurrencyAndEdgeCasesTest, EdgeCasesAndFaultTolerance) { // Case B: 空文本与纯标点符号输入 { std::string cfg_path = - GetConfigPath("configs/pipeline_keyword_match_rules.json"); + GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; Alg_Create(&handle, ¶m); @@ -166,12 +163,11 @@ TEST_F(ConcurrencyAndEdgeCasesTest, EdgeCasesAndFaultTolerance) { // Case C: 音频 0 采样点边界 { std::string cfg_path = - GetConfigPath("demo/fixtures/mock/pipeline_audio_asr_intent.json"); + GetConfigPath("demo/fixtures/mock/pipeline_audio_asr_intent_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_AUDIO_ASR_INTENT; void* handle = nullptr; int ret = Alg_Create(&handle, ¶m); diff --git a/tests/integration/runtime/test_different_io_modalities.cpp b/tests/integration/runtime/test_different_io_modalities.cpp index ee61aa63..ed9ab216 100644 --- a/tests/integration/runtime/test_different_io_modalities.cpp +++ b/tests/integration/runtime/test_different_io_modalities.cpp @@ -34,12 +34,11 @@ class DifferentIoModalitiesTest : public ::testing::Test { // 1. 验证业务 5: 多模态图文票据问答 (Image + Query -> OCR BBox -> LLM JSON) TEST_F(DifferentIoModalitiesTest, OcrDocQa) { std::string cfg_path = - GetConfigPath("demo/fixtures/mock/pipeline_ocr_doc_qa.json"); + GetConfigPath("demo/fixtures/mock/pipeline_ocr_doc_qa_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_OCR_DOC_QA; void* handle = nullptr; int ret = Alg_Create(&handle, ¶m); @@ -74,12 +73,11 @@ TEST_F(DifferentIoModalitiesTest, OcrDocQa) { // NLU Intent/Slots) TEST_F(DifferentIoModalitiesTest, AudioAsrIntent) { std::string cfg_path = - GetConfigPath("demo/fixtures/mock/pipeline_audio_asr_intent.json"); + GetConfigPath("demo/fixtures/mock/pipeline_audio_asr_intent_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_AUDIO_ASR_INTENT; void* handle = nullptr; int ret = Alg_Create(&handle, ¶m); @@ -149,17 +147,25 @@ TEST_F(DifferentIoModalitiesTest, CrossRerankBatch) { auto temp_dir = std::filesystem::temp_directory_path() / ("test_different_io_rerank_" + std::to_string(rand())); std::filesystem::create_directories(temp_dir); - auto temp_cfg_path = temp_dir / "pipeline_cross_rerank.json"; - std::ofstream json_out(temp_cfg_path); + auto temp_pipe_path = temp_dir / "pipeline_cross_rerank.json"; + std::ofstream json_out(temp_pipe_path); json_out << pipe_json.dump(2); json_out.close(); + nlohmann::json deploy_cfg = {{"schema_version", 1}, + {"data", + {{"pipe_path", "pipeline_cross_rerank.json"}, + {"io_binding", "cross_rerank.cabi.v1"}}}}; + auto temp_cfg_path = temp_dir / "pipeline_cross_rerank_cabi.json"; + std::ofstream cfg_out(temp_cfg_path); + cfg_out << deploy_cfg.dump(2); + cfg_out.close(); + std::string temp_cfg_str = temp_cfg_path.string(); CompanyAlgParamCreate param; param.config_file_path = temp_cfg_str.c_str(); param.model_root_dir = ""; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_CROSS_RERANK; void* handle = nullptr; param.device_id = 1; diff --git a/tests/integration/runtime/test_runtime_control_and_hot_swap.cpp b/tests/integration/runtime/test_runtime_control_and_hot_swap.cpp index b11b817c..4bb40168 100644 --- a/tests/integration/runtime/test_runtime_control_and_hot_swap.cpp +++ b/tests/integration/runtime/test_runtime_control_and_hot_swap.cpp @@ -170,12 +170,11 @@ TEST_F(RuntimeControlAndHotSwapTest, // 1. 关键词库运行时动态热更新与立即生效测试 TEST_F(RuntimeControlAndHotSwapTest, KeywordMatcherDynamicHotSwap) { std::string cfg_path = - GetConfigPath("configs/pipeline_keyword_match_rules.json"); + GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, ¶m), 0); @@ -239,12 +238,11 @@ TEST_F(RuntimeControlAndHotSwapTest, KeywordMatcherDynamicHotSwap) { // 2. 同一 handle 的 Process/Control 由 C ABI 层串行化,停流 join 后再销毁 TEST_F(RuntimeControlAndHotSwapTest, ConcurrentProcessAndHotControl) { std::string cfg_path = - GetConfigPath("configs/pipeline_keyword_match_rules.json"); + GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, ¶m), 0); @@ -304,12 +302,11 @@ TEST_F(RuntimeControlAndHotSwapTest, ConcurrentProcessAndHotControl) { // 3. 非法控制指令与边界容错测试 TEST_F(RuntimeControlAndHotSwapTest, InvalidControlCommands) { std::string cfg_path = - GetConfigPath("configs/pipeline_keyword_match_rules.json"); + GetConfigPath("configs/pipeline_keyword_match_cabi.json"); CompanyAlgParamCreate param; param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, ¶m), 0); diff --git a/tests/support/adapter_examples/flat_struct_adapter.h b/tests/support/adapter_examples/flat_struct_adapter.h index b6aefbb7..5caf3cbc 100644 --- a/tests/support/adapter_examples/flat_struct_adapter.h +++ b/tests/support/adapter_examples/flat_struct_adapter.h @@ -6,7 +6,7 @@ #include "adapter/adapter_status.h" #include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_interface.h" +#include "edgeflow/c_api.h" namespace llm_edgeflow { namespace template_examples { @@ -31,33 +31,15 @@ struct TemplateFlatResultDto { }; // 3. 模板适配器实现 -class TemplateFlatStructAdapter : public IBizAdapter { +class TemplateFlatStructAdapter { public: - CompanyAlgBizType BizType() const override { - return static_cast(101); - } - - const char* AdapterName() const override { return "TemplateFlatStruct"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - static_cast(101), - "TemplateFlatStruct", - "2.0.0", - "TemplateFlatInput", - "TemplateFlatOutput", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {BizDefinition{"TemplateFlatStruct", "template_flat_pipeline_v1"}}}; - return desc; - } + const char* AdapterName() const { return "TemplateFlatStruct"; } + size_t MaxBatchSize() const { return 64; } int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { + AdapterStatus* out_status = nullptr) const { int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); + inputs, num_inputs, static_cast(MaxBatchSize()), AdapterName()); if (valid_ret != 0 || !ctx) { return AdapterValidationHelper::ReturnInvalidInput( out_status, "Batch envelope validation failed", "inputs", @@ -100,7 +82,7 @@ class TemplateFlatStructAdapter : public IBizAdapter { } int Pack(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const override { + AdapterStatus* out_status = nullptr) const { if (!ctx) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; const auto* res = diff --git a/tests/support/adapter_examples/nested_array_adapter.h b/tests/support/adapter_examples/nested_array_adapter.h index 1b5438b8..7e90ce7c 100644 --- a/tests/support/adapter_examples/nested_array_adapter.h +++ b/tests/support/adapter_examples/nested_array_adapter.h @@ -6,7 +6,7 @@ #include "adapter/adapter_status.h" #include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_interface.h" +#include "edgeflow/c_api.h" namespace llm_edgeflow { namespace template_examples { @@ -46,34 +46,15 @@ struct TemplateNestedArrayResultDto { std::string summary; }; -class TemplateNestedArrayAdapter : public IBizAdapter { +class TemplateNestedArrayAdapter { public: - CompanyAlgBizType BizType() const override { - return static_cast(103); - } - - const char* AdapterName() const override { return "TemplateNestedArray"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - static_cast(103), - "TemplateNestedArray", - "2.0.0", - "TemplateNestedArrayInput", - "TemplateNestedArrayOutput", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {BizDefinition{"TemplateNestedArray", - "template_nested_array_pipeline_v1"}}}; - return desc; - } + const char* AdapterName() const { return "TemplateNestedArray"; } + size_t MaxBatchSize() const { return 64; } int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { + AdapterStatus* out_status = nullptr) const { int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); + inputs, num_inputs, static_cast(MaxBatchSize()), AdapterName()); if (valid_ret != 0 || !ctx) { return AdapterValidationHelper::ReturnInvalidInput( out_status, "Batch envelope validation failed", "inputs", @@ -140,7 +121,7 @@ class TemplateNestedArrayAdapter : public IBizAdapter { } int Pack(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const override { + AdapterStatus* out_status = nullptr) const { if (!ctx) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; const auto* res = ctx->Read>( diff --git a/tests/support/adapter_examples/nested_pointer_tree_adapter.h b/tests/support/adapter_examples/nested_pointer_tree_adapter.h index b85d5a96..e7311647 100644 --- a/tests/support/adapter_examples/nested_pointer_tree_adapter.h +++ b/tests/support/adapter_examples/nested_pointer_tree_adapter.h @@ -6,7 +6,7 @@ #include "adapter/adapter_status.h" #include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_interface.h" +#include "edgeflow/c_api.h" namespace llm_edgeflow { namespace template_examples { @@ -43,31 +43,10 @@ struct TemplateTreeResultDto { std::string traversal_path; }; -class TemplateNestedPointerTreeAdapter : public IBizAdapter { +class TemplateNestedPointerTreeAdapter { public: - CompanyAlgBizType BizType() const override { - return static_cast(104); - } - - const char* AdapterName() const override { - return "TemplateNestedPointerTree"; - } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - static_cast(104), - "TemplateNestedPointerTree", - "2.0.0", - "TemplateNestedTreeInput", - "TemplateNestedTreeOutput", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {BizDefinition{"TemplateNestedPointerTree", - "template_nested_tree_pipeline_v1"}}}; - return desc; - } + const char* AdapterName() const { return "TemplateNestedPointerTree"; } + size_t MaxBatchSize() const { return 64; } static bool UnpackNodeRecursive(const TemplateTreeNode* node, TemplateTreeNodeDto* out_dto, @@ -123,9 +102,9 @@ class TemplateNestedPointerTreeAdapter : public IBizAdapter { } int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { + AdapterStatus* out_status = nullptr) const { int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); + inputs, num_inputs, static_cast(MaxBatchSize()), AdapterName()); if (valid_ret != 0 || !ctx) { return AdapterValidationHelper::ReturnInvalidInput( out_status, "Batch envelope validation failed", "inputs", @@ -173,7 +152,7 @@ class TemplateNestedPointerTreeAdapter : public IBizAdapter { } int Pack(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const override { + AdapterStatus* out_status = nullptr) const { if (!ctx) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; const auto* res = diff --git a/tests/support/adapter_examples/tagged_union_adapter.h b/tests/support/adapter_examples/tagged_union_adapter.h index a66f9efa..3129bc30 100644 --- a/tests/support/adapter_examples/tagged_union_adapter.h +++ b/tests/support/adapter_examples/tagged_union_adapter.h @@ -6,7 +6,7 @@ #include "adapter/adapter_status.h" #include "adapter/adapter_validation_helper.h" -#include "adapter/biz_adapter_interface.h" +#include "edgeflow/c_api.h" namespace llm_edgeflow { namespace template_examples { @@ -56,34 +56,15 @@ struct TemplateUnionResultDto { std::string verdict; }; -class TemplateTaggedUnionAdapter : public IBizAdapter { +class TemplateTaggedUnionAdapter { public: - CompanyAlgBizType BizType() const override { - return static_cast(102); - } - - const char* AdapterName() const override { return "TemplateTaggedUnion"; } - - const AdapterDescriptor& GetDescriptor() const override { - static AdapterDescriptor desc{ - static_cast(102), - "TemplateTaggedUnion", - "2.0.0", - "TemplateTaggedUnionInput", - "TemplateTaggedUnionOutput", - 64, - OwnershipPolicy::kCopyIn, - ThreadModel::kStatelessThreadSafe, - OutputCardinality::kOneToOne, - {BizDefinition{"TemplateTaggedUnion", - "template_tagged_union_pipeline_v1"}}}; - return desc; - } + const char* AdapterName() const { return "TemplateTaggedUnion"; } + size_t MaxBatchSize() const { return 64; } int Unpack(const void** inputs, int num_inputs, AlgContext* ctx, - AdapterStatus* out_status = nullptr) const override { + AdapterStatus* out_status = nullptr) const { int valid_ret = AdapterValidationHelper::ValidateBatchInputs( - inputs, num_inputs, GetDescriptor().max_batch_size, AdapterName()); + inputs, num_inputs, static_cast(MaxBatchSize()), AdapterName()); if (valid_ret != 0 || !ctx) { return AdapterValidationHelper::ReturnInvalidInput( out_status, "Batch envelope validation failed", "inputs", @@ -149,7 +130,7 @@ class TemplateTaggedUnionAdapter : public IBizAdapter { } int Pack(AlgContext* ctx, void** outputs, int* num_outputs, - AdapterStatus* out_status = nullptr) const override { + AdapterStatus* out_status = nullptr) const { if (!ctx) return COMPANY_ALG_ERR_BUFFER_TOO_SMALL; const auto* res = diff --git a/tests/support/adapter_harness.h b/tests/support/adapter_harness.h index 5cc890c0..606ae7f5 100644 --- a/tests/support/adapter_harness.h +++ b/tests/support/adapter_harness.h @@ -7,7 +7,7 @@ #include #include "adapter/adapter_status.h" -#include "adapter/biz_adapter_interface.h" +#include "adapter/io_converter.h" #include "contracts/traceable_item.h" #include "core/alg_context.h" #include "core/blackboard_key.h" @@ -16,65 +16,83 @@ namespace llm_edgeflow { namespace test { /** - * @brief 测试专用 Adapter 契约夹具 (RFC-0053 Section 6) + * @brief 测试专用 Adapter / Converter 契约夹具 * - * 集中管理 AlgContext、句柄与输出缓冲区生命周期, - * 提供来源扰动生成器和统一的双输出 (C / owned Result) 调用通道。 + * 集中管理 AlgContext、句柄与输入输出视图生命周期, + * 提供来源扰动生成器和统一的 C ABI 解码/编码调用通道。 */ class AdapterHarness { public: - explicit AdapterHarness(std::shared_ptr adapter) - : adapter_(std::move(adapter)) {} + AdapterHarness(const InputConverterDefinition* input_conv, + const OutputConverterDefinition* output_conv, + InputPortBindings in_bindings = {}, + OutputPortBindings out_bindings = {}) + : in_conv_(input_conv), + out_conv_(output_conv), + in_bindings_(std::move(in_bindings)), + out_bindings_(std::move(out_bindings)) {} + + explicit AdapterHarness(const InputConverterDefinition* input_conv, + InputPortBindings in_bindings = {}) + : in_conv_(input_conv), + out_conv_(nullptr), + in_bindings_(std::move(in_bindings)) {} + + explicit AdapterHarness(const OutputConverterDefinition* output_conv, + OutputPortBindings out_bindings = {}) + : in_conv_(nullptr), + out_conv_(output_conv), + out_bindings_(std::move(out_bindings)) {} - IBizAdapter* Adapter() const { return adapter_.get(); } AlgContext& Context() { return ctx_; } const AlgContext& Context() const { return ctx_; } AdapterStatus& Status() { return status_; } const AdapterStatus& Status() const { return status_; } - int Unpack(const std::vector& inputs) { - return adapter_->Unpack(const_cast(inputs.data()), - static_cast(inputs.size()), &ctx_, &status_); + int DecodeCAbi(const std::vector& inputs) { + if (!in_conv_ || !in_conv_->decode_fn) return -1; + ExternalInputBatchView view; + view.items = + inputs.empty() ? nullptr : const_cast(inputs.data()); + view.count = inputs.size(); + view.type_id = in_conv_->external_type; + InputDecodeOptions options; + options.converter_id = in_conv_->converter_id; + options.transport = in_conv_->transport; + return in_conv_->decode_fn(view, options, in_bindings_, &ctx_, &status_); } template - int PackC(std::vector* outputs) { - if (!outputs) return -1; + int EncodeCAbi(std::vector* outputs) { + if (!out_conv_ || !out_conv_->encode_fn || !outputs) return -1; std::vector output_ptrs(outputs->size()); for (size_t i = 0; i < outputs->size(); ++i) { output_ptrs[i] = &(*outputs)[i]; } - int count = static_cast(outputs->size()); - int ret = - adapter_->Pack(&ctx_, outputs->empty() ? nullptr : output_ptrs.data(), - &count, &status_); - if (ret == 0 && count >= 0 && - static_cast(count) <= outputs->size()) { - outputs->resize(count); + ExternalOutputBatchView view; + view.items = outputs->empty() ? nullptr : output_ptrs.data(); + view.count = outputs->size(); + view.capacity = outputs->size(); + view.type_id = out_conv_->external_type; + OutputEncodeOptions options; + options.converter_id = out_conv_->converter_id; + options.transport = out_conv_->transport; + size_t written = 0; + int ret = out_conv_->encode_fn(&ctx_, out_bindings_, options, &view, + &written, &status_); + if (ret == 0 && written <= outputs->size()) { + outputs->resize(written); } return ret; } - template - int PackOwned(std::vector* outputs) { - if (!outputs) return -1; - std::vector output_ptrs(outputs->size()); - for (size_t i = 0; i < outputs->size(); ++i) { - output_ptrs[i] = &(*outputs)[i]; - } - int count = static_cast(outputs->size()); - int ret = adapter_->PackResultBatch( - &ctx_, outputs->empty() ? nullptr : output_ptrs.data(), &count, - &status_); - if (ret == 0 && count >= 0 && - static_cast(count) <= outputs->size()) { - outputs->resize(count); - } - return ret; + template + bool Publish(const BlackboardKey& key, T value) { + return ctx_.Publish(key, std::move(value)); } template - bool Publish(const BlackboardKey& key, T value) { + bool Publish(const std::string& key, T value) { return ctx_.Publish(key, std::move(value)); } @@ -123,7 +141,10 @@ class AdapterHarness { } private: - std::shared_ptr adapter_; + const InputConverterDefinition* in_conv_ = nullptr; + const OutputConverterDefinition* out_conv_ = nullptr; + InputPortBindings in_bindings_; + OutputPortBindings out_bindings_; AlgContext ctx_; AdapterStatus status_; }; diff --git a/tests/support/control_test_utils.h b/tests/support/control_test_utils.h index 872b55b5..cc4d5008 100644 --- a/tests/support/control_test_utils.h +++ b/tests/support/control_test_utils.h @@ -27,13 +27,13 @@ inline void WriteControlTestPipeline(const std::filesystem::path& directory) { {"outputs", {{"matches", "rule_matches"}}}}}, {"config", {{"categories", {{"PREFIX_APPLIED", {"VIP:sample"}}}}}}}}}}; const nlohmann::json conf = { + {"schema_version", 1}, {"data", {{"pipe_path", "pipeline.json"}, + {"io_binding", "keyword_match.operator.v1"}, {"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_pipeline_studio.py b/tests/tooling/test_pipeline_studio.py index 7d7d100c..ef852da3 100644 --- a/tests/tooling/test_pipeline_studio.py +++ b/tests/tooling/test_pipeline_studio.py @@ -312,7 +312,7 @@ def test_saved_pair_runs_the_selected_pipeline_with_explicit_arguments(self): saved = self.service.save_solution(filename, self.keyword, "keyword_match_rules") self.assertEqual(json.loads((self.configs / filename).read_text()), self.keyword) conf = json.loads((self.configs / saved["conf_filename"]).read_text()) - self.assertEqual(conf["data"]["pipe_path"], str((self.configs / filename).relative_to(ROOT))) + self.assertEqual(conf["data"]["pipe_path"], filename) self.assertEqual(conf["data"]["model_paths"], {}) command = shlex.split(saved["command"]) self.assertEqual(command[:3], ["cd", str(ROOT), "&&"]) @@ -463,7 +463,7 @@ def inspect_launch(args, **kwargs): time.sleep(0.05) self.assertEqual(job["status"], "completed", job) self.assertEqual(observed["conf"]["data"]["model_paths"], {"entity_llm": pipeline["models"][0]["model_path"]}) - self.assertTrue(observed["conf"]["data"]["pipe_path"].startswith("build/")) + self.assertEqual(observed["conf"]["data"]["pipe_path"], "pipeline.json") self.assertFalse(observed["directory"].exists()) def test_conflicts_bad_paths_and_mismatches_leave_no_new_files(self): @@ -532,7 +532,7 @@ def command(self, *args, input_pipeline=None): check=False, ) payload = json.loads(process.stdout) - expected_schema = 3 if args[0] in ("catalog", "describe-node") else 1 + expected_schema = 4 if args[0] == "catalog" else (3 if args[0] == "describe-node" else 1) self.assertEqual(payload["schema_version"], expected_schema) return process.returncode, payload @@ -646,6 +646,7 @@ def test_resolve_conf_exposes_model_sources_defaults_and_native_pool_errors(self conf = json.loads(conf_path.read_text()) with tempfile.TemporaryDirectory(prefix="resolve-conf-", dir=ROOT / "build") as directory: changed = Path(directory) / "pipeline.conf" + shutil.copy(conf_path.with_name(conf["data"]["pipe_path"]), directory) conf["data"].pop("model_paths") changed.write_text(json.dumps(conf)) code, direct = self.command("resolve-conf", str(changed.relative_to(ROOT)), "--root", str(ROOT)) @@ -1878,7 +1879,7 @@ def associated_doc_qa(self): conf = json.loads((ROOT / "configs" / "pipeline_doc_qa_cpu.conf").read_text()) pipeline_path = self.configs / "pipeline_associated.json" conf_path = self.configs / "pipeline_associated.conf" - conf["data"]["pipe_path"] = str(pipeline_path.relative_to(ROOT)) + conf["data"]["pipe_path"] = pipeline_path.name for model_id in conf["data"]["model_paths"]: conf["data"]["model_paths"][model_id] = "models/deployed_" + model_id conf["data"]["outputs"]["doc_out"]["capacities"]["answer_text"] = 2047 @@ -1973,7 +1974,7 @@ def test_studio_deployment_associate_and_partial_override_update(self): # Create conf in configs pointing to this pipeline conf_path = self.configs / "pipeline_doc_qa_assoc.conf" doc_qa_conf = json.loads((ROOT / "configs" / "pipeline_doc_qa_cpu.conf").read_text()) - doc_qa_conf["data"]["pipe_path"] = str(pipe_path.relative_to(ROOT)) + doc_qa_conf["data"]["pipe_path"] = pipe_path.name conf_path.write_text(json.dumps(doc_qa_conf, indent=2)) # Associate diff --git a/tests/unit/adapter/test_adapter_purity.cpp b/tests/unit/adapter/test_adapter_purity.cpp index c56e8de9..36bfc396 100644 --- a/tests/unit/adapter/test_adapter_purity.cpp +++ b/tests/unit/adapter/test_adapter_purity.cpp @@ -2,18 +2,27 @@ #include #include +#include #include #include -#include "adapter/adapter_authoring.h" -#include "adapter/adapter_batch.h" -#include "adapter/adapter_result.h" -#include "adapter/biz_adapter_registry.h" +#include "adapter/adapter_status.h" #include "adapter/biz_blackboard_keys.h" -#include "adapter/biz_results.h" +#include "adapter/converter_authoring.h" +#include "adapter/deployment_io_config.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_binding_resolver.h" +#include "adapter/io_converter.h" +#include "adapter/io_converter_registry.h" #include "adapter/shared_algorithm_runtime.h" +#include "contracts/inference_payloads.h" #include "core/alg_context.h" +#include "core/common_contracts.h" +#include "core/pipeline_catalog.h" #include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" +#include "engine/model_interface.h" +#include "engine/model_registry.h" #include "tests/support/adapter_harness.h" namespace llm_edgeflow { @@ -23,24 +32,48 @@ class AdapterPurityTest : public ::testing::Test { void SetUp() override { SharedAlgorithmRuntime::GlobalInit(); } }; -// 1. DocQaAdapter Purity (Biz 1) +struct CustomMultiFieldInput { + uint64_t req_id; + const char* topic; + const char* content; +}; + +DECLARE_EXTERNAL_TYPE_TRAITS(CustomMultiFieldInput, "CustomMultiFieldInput"); + +// ========================================================================= +// 1. All 8 Businesses Converter Purity +// ========================================================================= + +// 1.1 DocQaConverter Purity (Biz 1) TEST_F(AdapterPurityTest, DocQaAdapterPurity) { - auto adapter = BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(adapter, nullptr); + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "doc_query.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "doc_answer.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"raw_docs", "raw_docs"}, + {"raw_queries", "raw_queries"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}, + {"intent_matches", "intent_matches"}, + {"doc_chunk_counts", "doc_chunk_counts"}})); CompanyDocInputStruct in{}; in.request_id = 1001; in.doc_text = "Doc Content"; in.query_text = "Query Question"; - const void* inputs[] = {&in}; - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); - const auto* req_ids = ctx.Read(kRawRequestIds); - const auto* docs = ctx.Read(kRawDocs); - const auto* queries = ctx.Read(kRawQueries); + const auto* req_ids = + harness.Context().Read>("raw_request_ids"); + const auto* docs = harness.Context().Read("raw_docs"); + const auto* queries = harness.Context().Read("raw_queries"); ASSERT_NE(req_ids, nullptr); ASSERT_NE(docs, nullptr); ASSERT_NE(queries, nullptr); @@ -48,515 +81,587 @@ TEST_F(AdapterPurityTest, DocQaAdapterPurity) { EXPECT_EQ((*docs)[0].data, "Doc Content"); EXPECT_EQ((*queries)[0].data, "Query Question"); - // Pack check + // Output encoding TextBatch answers; answers.emplace_back(0, 0, "Model Generated Answer"); - ctx.Publish(kLlmAnswers, std::move(answers)); + harness.Publish("llm_answers", std::move(answers)); RuleMatchBatch intents; intents.emplace_back(0, 0, RuleMatchItem(1, "GENERAL_QA", "query", "{}", 0.95f)); - ctx.Publish(kIntentMatches, std::move(intents)); + harness.Publish("intent_matches", std::move(intents)); Int32Batch chunk_counts; chunk_counts.emplace_back(0, 0, 1); - ctx.Publish(kDocChunkCounts, std::move(chunk_counts)); - - CompanyDocOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), 0); - - EXPECT_EQ(out.request_id, 1001u); - EXPECT_EQ(out.chunk_count, 1); - EXPECT_STREQ(out.intent_name, "GENERAL_QA"); - EXPECT_FLOAT_EQ(out.confidence, 0.95f); - EXPECT_STREQ(out.answer_text, "Model Generated Answer"); + harness.Publish("doc_chunk_counts", std::move(chunk_counts)); + + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); + + EXPECT_EQ(outputs[0].request_id, 1001u); + EXPECT_EQ(outputs[0].chunk_count, 1); + EXPECT_STREQ(outputs[0].intent_name, "GENERAL_QA"); + EXPECT_FLOAT_EQ(outputs[0].confidence, 0.95f); + EXPECT_STREQ(outputs[0].answer_text, "Model Generated Answer"); } -// 2. KeywordMatchAdapter Purity (Biz 2) +// 1.2 KeywordMatchConverter Purity (Biz 2) TEST_F(AdapterPurityTest, KeywordMatchAdapterPurity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_KEYWORD_MATCH); - ASSERT_NE(adapter, nullptr); - - // Unpack check: C Struct -> AlgContext - CompanyKeywordInputStruct input{}; - input.request_id = 12345; - const char* sentence = "测试输入文本"; - input.sentence_text = sentence; - const void* inputs[] = {&input}; + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("text.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "keyword.result.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"rule_matches", "rule_matches"}})); + + CompanyEntityInputStruct in{1002, "Some text"}; + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); + + RuleMatchBatch matches; + matches.emplace_back( + 0, 0, + RuleMatchItem(1, "TEST_CAT", "测试", "{\"intent\":\"TEST_CAT\"}", 0.9f)); + harness.Publish("rule_matches", std::move(matches)); - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); - const auto* req_ids = ctx.Read(kRawRequestIds); - const auto* text_batch = ctx.Read(kInputSentences); - ASSERT_NE(req_ids, nullptr); - ASSERT_NE(text_batch, nullptr); - EXPECT_EQ((*req_ids)[0], 12345u); - EXPECT_EQ((*text_batch)[0].data, sentence); - - // Pack check: AlgContext -> C Struct - RuleMatchBatch match_batch; - RuleMatchItem match_item(1, "TEST_CAT", "测试", "{\"intent\":\"TEST_CAT\"}", - 1.0f); - match_batch.emplace_back(0, 0, std::move(match_item)); - ctx.Publish(kRuleMatches, std::move(match_batch)); - - CompanyKeywordOutputStruct output{}; - void* outputs[] = {&output}; - int num_outputs = 1; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &num_outputs, &status), 0); - - EXPECT_EQ(output.request_id, 12345u); - EXPECT_EQ(output.is_hit, 1); - EXPECT_STREQ(output.match_result_json, "{\"intent\":\"TEST_CAT\"}"); + EXPECT_EQ(outputs[0].request_id, 1002u); + EXPECT_EQ(outputs[0].is_hit, 1); + EXPECT_STREQ(outputs[0].match_result_json, "{\"intent\":\"TEST_CAT\"}"); } -// 3. EntityExtractAdapter Purity (Biz 3) +// 1.3 EntityExtractConverter Purity (Biz 3) TEST_F(AdapterPurityTest, EntityExtractAdapterPurity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_ENTITY_EXTRACT); - ASSERT_NE(adapter, nullptr); - - CompanyEntityInputStruct in{}; - in.request_id = 3001; - in.sentence_text = "张三就职于阿里巴巴"; - const void* inputs[] = {&in}; - - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("text.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "document.structured.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"extracted_entities", "extracted_entities"}})); + + CompanyEntityInputStruct in{1003, "Entity text"}; + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); StructuredDocumentBatch entities; - entities.emplace_back(0, 0, JsonDocumentItem("[\"张三\",\"阿里巴巴\"]")); - ctx.Publish(kExtractedEntities, std::move(entities)); + entities.emplace_back( + 0, 0, JsonDocumentItem("[\"E1\"]", true, JsonParseStatus::kOk)); + harness.Publish("extracted_entities", std::move(entities)); - CompanyEntityOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), 0); + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); - EXPECT_EQ(out.request_id, 3001u); - EXPECT_STREQ(out.entities_json, "[\"张三\",\"阿里巴巴\"]"); + EXPECT_EQ(outputs[0].request_id, 1003u); + EXPECT_EQ(outputs[0].status_code, 0); + EXPECT_STREQ(outputs[0].entities_json, "[\"E1\"]"); } -// 4. ComplianceAuditAdapter Purity (Biz 4) +// 1.4 ComplianceAuditConverter Purity (Biz 4) TEST_F(AdapterPurityTest, ComplianceAuditAdapterPurity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_COMPLIANCE_AUDIT); - ASSERT_NE(adapter, nullptr); - - CompanyAuditInputStruct in{}; - in.request_id = 8888; - in.user_text = "客户投诉退款问题"; - in.channel_name = "VIP_HOTLINE"; - const void* inputs[] = {&in}; - - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("audit.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "audit_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"user_texts", "user_texts"}, + {"channel_names", "channel_names"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"structured_verdicts", "structured_verdicts"}, + {"matched_policies", "matched_policies"}})); + + CompanyAuditInputStruct in{1004, "audit sentence", "channel_vip"}; + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); - // Pack structured verdict from AlgContext directly StructuredDocumentBatch verdicts; - nlohmann::json structured_obj = {{"risk_level", "HIGH_RISK"}, - {"risk_score", 0.92f}}; verdicts.emplace_back( 0, 0, - JsonDocumentItem("{\"risk_level\":\"HIGH_RISK\",\"risk_score\":0.92}", - true, JsonParseStatus::kOk, "", structured_obj)); - ctx.Publish(kStructuredVerdicts, std::move(verdicts)); + JsonDocumentItem("{\"risk_level\":\"SAFE\",\"risk_score\":0.1}", true, + JsonParseStatus::kOk, "", + {{"risk_level", "SAFE"}, {"risk_score", 0.1f}})); + harness.Publish("structured_verdicts", std::move(verdicts)); RankedTextBatch policies; - policies.emplace_back(0, 0, - RankedCandidate("Clause 9.1 Refund Policy", 0.95f, 1)); - ctx.Publish(kMatchedPolicy, std::move(policies)); - - CompanyAuditOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), 0); - - EXPECT_EQ(out.request_id, 8888u); - EXPECT_STREQ(out.risk_level, "HIGH_RISK"); - EXPECT_FLOAT_EQ(out.risk_score, 0.92f); - EXPECT_STREQ(out.matched_policy_clause, "Clause 9.1 Refund Policy"); + policies.emplace_back(0, 0, RankedCandidate("Clause 1", 1.0f, 1, 0)); + harness.Publish("matched_policies", std::move(policies)); + + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); + + EXPECT_EQ(outputs[0].request_id, 1004u); + EXPECT_FLOAT_EQ(outputs[0].risk_score, 0.1f); + EXPECT_STREQ(outputs[0].risk_level, "SAFE"); + EXPECT_STREQ(outputs[0].matched_policy_clause, "Clause 1"); } -// 5. OcrDocQaAdapter Purity (Biz 5) +// 1.5 OcrDocQaConverter Purity (Biz 5) TEST_F(AdapterPurityTest, OcrDocQaAdapterPurity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_OCR_DOC_QA); - ASSERT_NE(adapter, nullptr); - - CompanyOcrDocInputStruct in{}; - in.request_id = 5001; - in.image_path = "./data/invoice.png"; - in.query_prompt = "提取发票总额"; - const void* inputs[] = {&in}; + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "image_query.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "invoice_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"image_paths", "image_paths"}, + {"user_queries", "user_queries"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"extracted_invoice_json", "extracted_invoice_json"}, + {"ocr_docs", "ocr_docs"}})); + + CompanyOcrDocInputStruct in{1005, "/path/invoice.jpg", "Total amount?"}; + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); + StructuredDocumentBatch invoices; + invoices.emplace_back( + 0, 0, JsonDocumentItem("{\"total\":99.9}", true, JsonParseStatus::kOk)); + harness.Publish("extracted_invoice_json", std::move(invoices)); OcrDocumentBatch ocr_docs; - OcrDocumentItem doc_item; - doc_item.boxes.push_back({10, 20, 100, 30, "总计 500 元", 0.99f}); - ocr_docs.emplace_back(0, 0, std::move(doc_item)); - ctx.Publish(kOcrDocs, std::move(ocr_docs)); + OcrDocumentItem ocr_item; + ocr_item.boxes.push_back({0, 0, 10, 10, "Total", 0.99f}); + ocr_docs.emplace_back(0, 0, std::move(ocr_item)); + harness.Publish("ocr_docs", std::move(ocr_docs)); - StructuredDocumentBatch invoices; - invoices.emplace_back(0, 0, JsonDocumentItem("{\"total\":500}")); - ctx.Publish(kExtractedInvoiceJson, std::move(invoices)); - - CompanyOcrDocOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), 0); + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); - EXPECT_EQ(out.request_id, 5001u); - EXPECT_EQ(out.detected_box_count, 1); - EXPECT_STREQ(out.extracted_invoice_json, "{\"total\":500}"); + EXPECT_EQ(outputs[0].request_id, 1005u); + EXPECT_EQ(outputs[0].detected_box_count, 1U); + EXPECT_STREQ(outputs[0].extracted_invoice_json, "{\"total\":99.9}"); } -// 6. AudioAsrIntentAdapter Purity (Biz 6) +// 1.6 AudioAsrIntentConverter Purity (Biz 6) TEST_F(AdapterPurityTest, AudioAsrIntentAdapterPurity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_AUDIO_ASR_INTENT); - ASSERT_NE(adapter, nullptr); - - std::vector pcm(160, 0.1f); - CompanyAudioInputStruct in{}; - in.request_id = 6001; - in.pcm_buffer = pcm.data(); - in.pcm_length = static_cast(pcm.size()); - in.sample_rate = 16000; - const void* inputs[] = {&in}; - - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("audio.pcm.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "audio_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"audio_inputs", "audio_inputs"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"transcripts", "transcripts"}, + {"intent_slots", "intent_slots"}})); + + std::vector pcm(1600, 0.05f); + CompanyAudioInputStruct in{1006, pcm.data(), static_cast(pcm.size()), + 16000}; + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); TextBatch transcripts; - transcripts.emplace_back(0, 0, "导航到清华科技园"); - ctx.Publish(kTranscripts, std::move(transcripts)); - - RuleMatchBatch slots; - slots.emplace_back(0, 0, - RuleMatchItem(1, "NAVIGATION", "导航到", - "{\"intent\":\"NAVIGATION\",\"slots\":{" - "\"destination\":\"清华科技园\"}}", - 1.0f)); - ctx.Publish(kIntentSlots, std::move(slots)); - - CompanyAudioOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), 0); - - EXPECT_EQ(out.request_id, 6001u); - EXPECT_STREQ(out.transcribed_text, "导航到清华科技园"); - EXPECT_NE(std::string(out.intent_slot_json).find("清华科技园"), - std::string::npos); + transcripts.emplace_back(0, 0, "turn left"); + harness.Publish("transcripts", std::move(transcripts)); + + RuleMatchBatch intent_slots; + intent_slots.emplace_back( + 0, 0, RuleMatchItem(1, "NAV", "", "{\"intent\":\"NAV\"}", 0.99f)); + harness.Publish("intent_slots", std::move(intent_slots)); + + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); + + EXPECT_EQ(outputs[0].request_id, 1006u); + EXPECT_STREQ(outputs[0].transcribed_text, "turn left"); + EXPECT_STREQ(outputs[0].intent_slot_json, "{\"intent\":\"NAV\"}"); } -// 7. CrossRerankAdapter Purity (Biz 7) +// 1.7 CrossRerankConverter Purity (Biz 7) TEST_F(AdapterPurityTest, CrossRerankAdapterPurity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_CROSS_RERANK); - ASSERT_NE(adapter, nullptr); - - const char* query = "EdgeFlow 架构"; - const char* p0 = "LLM-EdgeFlow 核心组件"; - const char* p1 = "不相关段落"; + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "rerank.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "rerank_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"rerank_queries", "rerank_queries"}, + {"rerank_candidates", "rerank_candidates"}, + {"rerank_pairs", "rerank_pairs"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"ranked_results", "ranked_results"}})); + + const char* passages[] = {"cand0", "cand1"}; CompanyRerankBatchInputStruct in{}; - in.request_id = 7001; - in.query_text = query; - in.candidate_passages[0] = p0; - in.candidate_passages[1] = p1; + in.request_id = 1007; + in.query_text = "query"; in.candidate_count = 2; - const void* inputs[] = {&in}; + in.candidate_passages[0] = passages[0]; + in.candidate_passages[1] = passages[1]; - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); - - RankedTextBatch results; - results.emplace_back(0, 0, RankedCandidate(p0, 0.98f, 1, 0)); - results.emplace_back(0, 1, RankedCandidate(p1, 0.12f, 2, 1)); - ctx.Publish(kRankedResults, std::move(results)); - - CompanyRerankBatchOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), 0); - - EXPECT_EQ(out.request_id, 7001u); - EXPECT_EQ(out.count, 2); - EXPECT_FLOAT_EQ(out.scores[0], 0.98f); - EXPECT_EQ(out.sorted_indices[0], 0); + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); + + RankedTextBatch ranked; + ranked.emplace_back(0, 0, RankedCandidate("cand1", 0.85f, 1, 1)); + ranked.emplace_back(0, 1, RankedCandidate("cand0", 0.45f, 2, 0)); + harness.Publish("ranked_results", std::move(ranked)); + + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); + + EXPECT_EQ(outputs[0].request_id, 1007u); + EXPECT_EQ(outputs[0].count, 2); + EXPECT_FLOAT_EQ(outputs[0].scores[0], 0.85f); + EXPECT_EQ(outputs[0].sorted_indices[0], 1); + EXPECT_FLOAT_EQ(outputs[0].scores[1], 0.45f); + EXPECT_EQ(outputs[0].sorted_indices[1], 0); +} + +// 1.8 TranslateConverter Purity (Biz 8) +TEST_F(AdapterPurityTest, TranslateAdapterPurity) { + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}})); + + CompanyEntityInputStruct in{1008, "{\"query\":\"Hello\"}"}; + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); + + TextBatch answers; + answers.emplace_back(0, 0, "Bonjour"); + harness.Publish("llm_answers", std::move(answers)); + + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); + + EXPECT_EQ(outputs[0].request_id, 1008u); + EXPECT_EQ(outputs[0].status_code, 0); + auto parsed = nlohmann::json::parse(outputs[0].entities_json); + EXPECT_EQ(parsed["translated"], "Bonjour"); } -// 8. Negative Tests: Fail-Closed Purity Assertions (Zero Fabrication) +// ========================================================================= +// 2. Contract Invariants and Security Edge Cases +// ========================================================================= + TEST_F(AdapterPurityTest, DocQaAdapter_FailClosedWhenMissingOutputs) { - auto adapter = BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(adapter, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "doc_answer.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); - AlgContext ctx; - AdapterStatus status; - CompanyDocOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; + OutputPortBindings out_bindings({{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}, + {"intent_matches", "intent_matches"}, + {"doc_chunk_counts", "doc_chunk_counts"}}); // Case 1: missing llm_answers - EXPECT_NE(adapter->Pack(&ctx, outputs, &num_out, &status), 0); + { + test::AdapterHarness harness(out_conv, out_bindings); + harness.Publish("raw_request_ids", std::vector{1001}); + std::vector outputs(1); + EXPECT_NE(harness.EncodeCAbi(&outputs), 0); + } // Case 2: has llm_answers but missing intent_matches -> MUST fail-closed - TextBatch answers; - answers.emplace_back(0, 0, "Some answer"); - ctx.Publish(kLlmAnswers, std::move(answers)); - EXPECT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), - COMPANY_ALG_ERR_INVALID_INPUT); + { + test::AdapterHarness harness(out_conv, out_bindings); + harness.Publish("raw_request_ids", std::vector{1001}); + TextBatch answers; + answers.emplace_back(0, 0, "Some answer"); + harness.Publish("llm_answers", std::move(answers)); + std::vector outputs(1); + EXPECT_EQ(harness.EncodeCAbi(&outputs), COMPANY_ALG_ERR_INVALID_INPUT); + } - // Case 3: has intent_matches but missing explicit per-request chunk counts - // -> MUST fail-closed (the adapter may not derive business data). - RuleMatchBatch intents; - intents.emplace_back(0, 0, RuleMatchItem(1, "GENERAL_QA", "", "{}", 0.9f)); - ctx.Publish(kIntentMatches, std::move(intents)); - EXPECT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), - COMPANY_ALG_ERR_INVALID_INPUT); + // Case 3: has intent_matches but missing explicit chunk counts -> MUST + // fail-closed + { + test::AdapterHarness harness(out_conv, out_bindings); + harness.Publish("raw_request_ids", std::vector{1001}); + TextBatch answers; + answers.emplace_back(0, 0, "Some answer"); + harness.Publish("llm_answers", std::move(answers)); + RuleMatchBatch intents; + intents.emplace_back(0, 0, RuleMatchItem(1, "QA", "", "{}", 0.9f)); + harness.Publish("intent_matches", std::move(intents)); + std::vector outputs(1); + EXPECT_EQ(harness.EncodeCAbi(&outputs), COMPANY_ALG_ERR_INVALID_INPUT); + } - // Case 4: all business outputs exist but input request provenance is absent. - Int32Batch chunk_counts; - chunk_counts.emplace_back(0, 0, 1); - ctx.Publish(kDocChunkCounts, std::move(chunk_counts)); - EXPECT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), - COMPANY_ALG_ERR_INVALID_INPUT); + // Case 4: all outputs exist but raw_request_ids is absent + { + test::AdapterHarness harness(out_conv, out_bindings); + TextBatch answers; + answers.emplace_back(0, 0, "Some answer"); + harness.Publish("llm_answers", std::move(answers)); + RuleMatchBatch intents; + intents.emplace_back(0, 0, RuleMatchItem(1, "QA", "", "{}", 0.9f)); + harness.Publish("intent_matches", std::move(intents)); + Int32Batch chunk_counts; + chunk_counts.emplace_back(0, 0, 1); + harness.Publish("doc_chunk_counts", std::move(chunk_counts)); + std::vector outputs(1); + EXPECT_NE(harness.EncodeCAbi(&outputs), 0); + } } TEST_F(AdapterPurityTest, ComplianceAuditAdapter_FailClosedWhenMissingStructuredFields) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_COMPLIANCE_AUDIT); - ASSERT_NE(adapter, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "audit_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); - AlgContext ctx; - AdapterStatus status; - CompanyAuditOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; + OutputPortBindings out_bindings( + {{"raw_request_ids", "raw_request_ids"}, + {"structured_verdicts", "structured_verdicts"}, + {"matched_policies", "matched_policies"}}); - // Case 1: missing structured_verdicts - EXPECT_NE(adapter->Pack(&ctx, outputs, &num_out, &status), 0); + test::AdapterHarness harness(out_conv, out_bindings); + harness.Publish("raw_request_ids", std::vector{1001}); - // Case 2: structured_verdicts missing required field 'risk_level' -> MUST - // fail-closed + // structured_verdicts missing required field 'risk_level' -> MUST fail-closed StructuredDocumentBatch verdicts; nlohmann::json incomplete_obj = {{"only_verdict", "合规"}}; verdicts.emplace_back( 0, 0, JsonDocumentItem("{}", true, JsonParseStatus::kOk, "", incomplete_obj)); - ctx.Publish(kStructuredVerdicts, std::move(verdicts)); + harness.Publish("structured_verdicts", std::move(verdicts)); RankedTextBatch policies; policies.emplace_back(0, 0, RankedCandidate("Clause", 1.0f, 1)); - ctx.Publish(kMatchedPolicy, std::move(policies)); + harness.Publish("matched_policies", std::move(policies)); - EXPECT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), - COMPANY_ALG_ERR_INVALID_INPUT); + std::vector outputs(1); + EXPECT_EQ(harness.EncodeCAbi(&outputs), COMPANY_ALG_ERR_INVALID_INPUT); } -} // namespace llm_edgeflow - -namespace llm_edgeflow { TEST_F(AdapterPurityTest, AuditJoinsRankOneByRequestAndRejectsFallback) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_COMPLIANCE_AUDIT); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "audit_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + OutputPortBindings out_bindings( + {{"raw_request_ids", "raw_request_ids"}, + {"structured_verdicts", "structured_verdicts"}, + {"matched_policies", "matched_policies"}}); + for (const auto parse_status : {JsonParseStatus::kOk, JsonParseStatus::kFailed, JsonParseStatus::kFallbackApplied}) { - AlgContext ctx; - ctx.Publish(kRawRequestIds, std::vector{100, 200}); + test::AdapterHarness harness(out_conv, out_bindings); + harness.Publish("raw_request_ids", std::vector{100, 200}); + StructuredDocumentBatch verdicts; for (uint32_t id : {1u, 0u}) { verdicts.emplace_back( id, 0, JsonDocumentItem("{}", true, parse_status, "", - {{"risk_level", "SAFE"}, {"risk_score", 0.1}})); + {{"risk_level", "SAFE"}, {"risk_score", 0.1f}})); } - ctx.Publish(kStructuredVerdicts, std::move(verdicts)); - ctx.Publish(kMatchedPolicy, RankedTextBatch{{0, 0, {"req0 first", 1, 1}}, - {0, 1, {"req0 second", 0.5, 2}}, - {1, 0, {"req1 first", 1, 1}}}); - CompanyAuditOutputStruct out[2]{}; - void* outputs[] = {&out[0], &out[1]}; - int count = 2; - AdapterStatus status; - const int ret = adapter->Pack(&ctx, outputs, &count, &status); + harness.Publish("structured_verdicts", std::move(verdicts)); + harness.Publish("matched_policies", + RankedTextBatch{{0, 0, {"req0 first", 1.0f, 1, 1}}, + {0, 1, {"req0 second", 0.5f, 2, 2}}, + {1, 0, {"req1 first", 1.0f, 1, 1}}}); + + std::vector outputs(2); + const int ret = harness.EncodeCAbi(&outputs); if (parse_status == JsonParseStatus::kOk) { - ASSERT_EQ(ret, 0) << status.ToString(); - EXPECT_EQ(out[1].request_id, 200u); - EXPECT_STREQ(out[1].matched_policy_clause, "req1 first"); + ASSERT_EQ(ret, 0) << harness.Status().ToString(); + EXPECT_EQ(outputs[1].request_id, 200u); + EXPECT_STREQ(outputs[1].matched_policy_clause, "req1 first"); } else { EXPECT_NE(ret, 0); } } } + TEST_F(AdapterPurityTest, OneToOneResultsRejectDuplicateAndOutOfRangeIds) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_KEYWORD_MATCH); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "keyword.result.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + OutputPortBindings out_bindings({{"raw_request_ids", "raw_request_ids"}, + {"rule_matches", "rule_matches"}}); + for (const auto& ids : {std::vector{0, 0}, std::vector{0, 2}}) { - AlgContext ctx; - ctx.Publish(kRawRequestIds, std::vector{100, 200}); + test::AdapterHarness harness(out_conv, out_bindings); + harness.Publish("raw_request_ids", std::vector{100, 200}); RuleMatchBatch matches; for (auto id : ids) matches.emplace_back(id, 0, RuleMatchItem{}); - ctx.Publish(kRuleMatches, std::move(matches)); - CompanyKeywordOutputStruct out[2]{}; - void* outputs[] = {&out[0], &out[1]}; - int count = 2; - EXPECT_EQ(adapter->Pack(&ctx, outputs, &count), - COMPANY_ALG_ERR_INVALID_INPUT); + harness.Publish("rule_matches", std::move(matches)); + + std::vector outputs(2); + EXPECT_EQ(harness.EncodeCAbi(&outputs), COMPANY_ALG_ERR_INVALID_INPUT); } } TEST_F(AdapterPurityTest, ComplianceAuditAdapter_RejectsOversizedChannelName) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_COMPLIANCE_AUDIT); - ASSERT_NE(adapter, nullptr); + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("audit.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + + InputPortBindings in_bindings({{"raw_request_ids", "raw_request_ids"}, + {"user_texts", "user_texts"}, + {"channel_names", "channel_names"}}); const std::string valid_channel(256, 'c'); const std::string oversized_channel(257, 'c'); // Valid length <= 256 { - CompanyAuditInputStruct in{}; - in.request_id = 5001; - in.user_text = "test query"; - in.channel_name = valid_channel.c_str(); - const void* inputs[] = {&in}; - AlgContext ctx; - AdapterStatus status; - EXPECT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), COMPANY_ALG_SUCCESS); + test::AdapterHarness harness(in_conv, in_bindings); + CompanyAuditInputStruct in{5001, "test query", valid_channel.c_str()}; + EXPECT_EQ(harness.DecodeCAbi({&in}), COMPANY_ALG_SUCCESS); } // Oversized length > 256 { - CompanyAuditInputStruct in{}; - in.request_id = 5002; - in.user_text = "test query"; - in.channel_name = oversized_channel.c_str(); - const void* inputs[] = {&in}; - AlgContext ctx; - AdapterStatus status; - EXPECT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), - COMPANY_ALG_ERR_INVALID_INPUT); + test::AdapterHarness harness(in_conv, in_bindings); + CompanyAuditInputStruct in{5002, "test query", oversized_channel.c_str()}; + EXPECT_EQ(harness.DecodeCAbi({&in}), COMPANY_ALG_ERR_INVALID_INPUT); } } -} // namespace llm_edgeflow -namespace llm_edgeflow { TEST_F(AdapterPurityTest, VariableDocResultPreservesLongAnswerAndCAbiLimit) { - auto adapter = BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_DOC_QA); - AlgContext ctx; - const std::string answer(5000, 'a'); - ctx.Publish(kRawRequestIds, std::vector{10}); - ctx.Publish(kLlmAnswers, TextBatch{{0, 0, answer}}); - ctx.Publish(kIntentMatches, RuleMatchBatch{{0, 0, RuleMatchItem{}}}); - ctx.Publish(kDocChunkCounts, Int32Batch{{0, 0, 1}}); - CompanyDocOutputStruct fixed{}; - void* fixed_outputs[] = {&fixed}; - int count = 1; - EXPECT_EQ(adapter->Pack(&ctx, fixed_outputs, &count), - COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - DocResult variable; - void* variable_outputs[] = {&variable}; - count = 1; - ASSERT_EQ(adapter->PackResultBatch(&ctx, variable_outputs, &count), 0); - EXPECT_EQ(variable.answer_text, answer); - EXPECT_EQ(variable.request_id, 10u); -} - -// RFC-0053: TranslateAdapter Purity -TEST_F(AdapterPurityTest, TranslateAdapterPurity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); - - CompanyEntityInputStruct in{}; - in.request_id = 9901; - in.sentence_text = "{\"query\":\"测试翻译句子\"}"; - const void* inputs[] = {&in}; + const std::string long_answer(5000, 'a'); AlgContext ctx; + ctx.Publish("raw_request_ids", std::vector{10}); + ctx.Publish("llm_answers", TextBatch{{0, 0, long_answer}}); + ctx.Publish("intent_matches", + RuleMatchBatch{{0, 0, RuleMatchItem(1, "QA", "", "{}", 0.9f)}}); + ctx.Publish("doc_chunk_counts", Int32Batch{{0, 0, 1}}); + + // 1. C ABI fixed buffer: sizeof(answer_text) is 1024, must return + // BUFFER_TOO_SMALL + const auto* cabi_conv = IoConverterRegistry::Instance().FindOutputConverter( + "doc_answer.plain.cabi.v1"); + ASSERT_NE(cabi_conv, nullptr); + + CompanyDocOutputStruct cabi_out{}; + void* cabi_ptrs[] = {&cabi_out}; + ExternalOutputBatchView cabi_dest; + cabi_dest.items = cabi_ptrs; + cabi_dest.count = 1; + cabi_dest.capacity = 1; + + OutputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}, + {"intent_matches", "intent_matches"}, + {"doc_chunk_counts", "doc_chunk_counts"}}); + OutputEncodeOptions options; + options.converter_id = cabi_conv->converter_id; + size_t written = 0; AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); - - const auto* req_ids = ctx.Read(kRawRequestIds); - const auto* sentences = ctx.Read(kInputSentences); - ASSERT_NE(req_ids, nullptr); - ASSERT_NE(sentences, nullptr); - EXPECT_EQ((*req_ids)[0], 9901u); - EXPECT_EQ((*sentences)[0].data, "测试翻译句子"); - - TextBatch answers; - answers.emplace_back(0, 0, "Translated Sentence"); - ctx.Publish(kLlmAnswers, std::move(answers)); - - CompanyEntityOutputStruct out{}; - void* outputs[] = {&out}; - int num_out = 1; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &num_out, &status), 0); - - EXPECT_EQ(out.request_id, 9901u); - EXPECT_EQ(out.status_code, 0); - EXPECT_EQ(nlohmann::json::parse(out.entities_json), - nlohmann::json({{"translated", "Translated Sentence"}})); + int ret = cabi_conv->encode_fn(&ctx, bindings, options, &cabi_dest, &written, + &status); + EXPECT_EQ(ret, COMPANY_ALG_ERR_BUFFER_TOO_SMALL); + + // 2. Operator variable buffer: capacity = 6000, must succeed and preserve + // full answer + const auto* op_conv = IoConverterRegistry::Instance().FindOutputConverter( + "doc_answer.plain.operator.v1"); + ASSERT_NE(op_conv, nullptr); + + CompanyOperatorDocOutput op_out{}; + std::vector ans_buf(6000); + CompanyString cs_ans{0, ans_buf.data()}; + op_out.answer_text = &cs_ans; + + std::vector intent_buf(128); + CompanyString cs_intent{0, intent_buf.data()}; + op_out.intent_name = &cs_intent; + + ExternalOutputBatchView op_dest; + op_dest.leased_slots["doc_out"].push_back(&op_out); + op_dest.slot_capacities["doc_out"]["answer_text"] = 6000; + op_dest.slot_capacities["doc_out"]["intent_name"] = 128; + op_dest.count = 1; + + options.converter_id = op_conv->converter_id; + ret = + op_conv->encode_fn(&ctx, bindings, options, &op_dest, &written, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + EXPECT_EQ(written, 1U); + EXPECT_EQ(op_out.request_id, 10U); + EXPECT_EQ(std::string(op_out.answer_text->data), long_answer); } -// RFC-0053: Copy-In purity (mutating input buffer does not mutate context) TEST_F(AdapterPurityTest, InputBatchSkeleton_CopyInPurity) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + + test::AdapterHarness harness( + in_conv, InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}})); std::string buffer = "{\"query\":\"original query\"}"; - CompanyEntityInputStruct in{}; - in.request_id = 5555; - in.sentence_text = buffer.c_str(); - const void* inputs[] = {&in}; + CompanyEntityInputStruct in{5555, buffer.c_str()}; - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 1, &ctx, &status), 0); + ASSERT_EQ(harness.DecodeCAbi({&in}), 0); // Overwrite external buffer buffer[11] = 'X'; buffer[12] = 'X'; - const auto* sentences = ctx.Read(kInputSentences); + const auto* sentences = harness.Context().Read("input_sentences"); ASSERT_NE(sentences, nullptr); EXPECT_EQ((*sentences)[0].data, "original query"); } -// RFC-0053: External duplicate request IDs allowed (internal req_id is index) TEST_F(AdapterPurityTest, InputBatchSkeleton_ExternalDuplicateIdsAllowed) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + in_conv, out_conv, + InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}), + OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}})); CompanyEntityInputStruct in0{1234, "{\"query\":\"q0\"}"}; CompanyEntityInputStruct in1{1234, "{\"query\":\"q1\"}"}; - const void* inputs[] = {&in0, &in1}; - AlgContext ctx; - AdapterStatus status; - ASSERT_EQ(adapter->Unpack(inputs, 2, &ctx, &status), 0); + ASSERT_EQ(harness.DecodeCAbi({&in0, &in1}), 0); - const auto* req_ids = ctx.Read(kRawRequestIds); - const auto* sentences = ctx.Read(kInputSentences); + const auto* req_ids = + harness.Context().Read>("raw_request_ids"); + const auto* sentences = harness.Context().Read("input_sentences"); ASSERT_NE(req_ids, nullptr); ASSERT_NE(sentences, nullptr); EXPECT_EQ((*req_ids)[0], 1234u); @@ -565,94 +670,59 @@ TEST_F(AdapterPurityTest, InputBatchSkeleton_ExternalDuplicateIdsAllowed) { EXPECT_EQ((*sentences)[1].req_id, 1u); TextBatch answers{{0, 0, "ans0"}, {1, 0, "ans1"}}; - ctx.Publish(kLlmAnswers, std::move(answers)); - - CompanyEntityOutputStruct out0{}, out1{}; - void* outputs[] = {&out0, &out1}; - int count = 2; - ASSERT_EQ(adapter->Pack(&ctx, outputs, &count, &status), 0); - EXPECT_EQ(out0.request_id, 1234u); - EXPECT_EQ(out1.request_id, 1234u); + harness.Publish("llm_answers", std::move(answers)); + + std::vector outputs(2); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); + EXPECT_EQ(outputs[0].request_id, 1234u); + EXPECT_EQ(outputs[1].request_id, 1234u); } -// RFC-0053: All samples validated before publish (fail-closed, no partial -// publication) TEST_F(AdapterPurityTest, InputBatchSkeleton_AllSamplesValidatedBeforePublish) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + + test::AdapterHarness harness( + in_conv, InputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}})); - // Sample 0 is valid, sample 1 has invalid JSON CompanyEntityInputStruct in0{1, "{\"query\":\"valid\"}"}; CompanyEntityInputStruct in1{2, "invalid json"}; - const void* inputs[] = {&in0, &in1}; - AlgContext ctx; - AdapterStatus status; - EXPECT_EQ(adapter->Unpack(inputs, 2, &ctx, &status), - COMPANY_ALG_ERR_INVALID_INPUT); + EXPECT_EQ(harness.DecodeCAbi({&in0, &in1}), COMPANY_ALG_ERR_INVALID_INPUT); // AlgContext must be completely unpopulated - EXPECT_EQ(ctx.Read(kRawRequestIds), nullptr); - EXPECT_EQ(ctx.Read(kInputSentences), nullptr); -} - -// RFC-0053: Subsequent key conflict partial publication behavior (existing -// AlgContext semantics) -TEST_F(AdapterPurityTest, - InputBatchSkeleton_SubsequentKeyConflictPartialPublish) { - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_TRANSLATE); - ASSERT_NE(adapter, nullptr); - - AlgContext ctx; - // Pre-publish kInputSentences to cause conflict on the second publish - TextBatch existing_sentences{{0, 0, "pre-existing"}}; - ctx.Publish(kInputSentences, std::move(existing_sentences)); - - CompanyEntityInputStruct in0{101, "{\"query\":\"test query\"}"}; - const void* inputs[] = {&in0}; - AdapterStatus status; - int ret = adapter->Unpack(inputs, 1, &ctx, &status); - - // Unpack fails due to conflict on kInputSentences - EXPECT_EQ(ret, COMPANY_ALG_ERR_INVALID_INPUT); - - // Partial publish behavior: kRawRequestIds was published before the conflict - // and remains in ctx - const auto* raw_ids = ctx.Read(kRawRequestIds); - ASSERT_NE(raw_ids, nullptr); - EXPECT_EQ(raw_ids->size(), 1u); - EXPECT_EQ((*raw_ids)[0], 101u); - - // kInputSentences retained its pre-existing value - const auto* sentences = ctx.Read(kInputSentences); - ASSERT_NE(sentences, nullptr); - EXPECT_EQ((*sentences)[0].data, "pre-existing"); + EXPECT_EQ(harness.Context().Read>("raw_request_ids"), + nullptr); + EXPECT_EQ(harness.Context().Read("input_sentences"), nullptr); } -// RFC-0053: Multi-way results out-of-order alignment, owned packing, and -// perturbation testing TEST_F(AdapterPurityTest, DocQaAdapter_MultiWayResultsReorderedAndPerturbed) { - auto adapter = BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(adapter, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "doc_answer.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + test::AdapterHarness harness( + out_conv, OutputPortBindings({{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}, + {"intent_matches", "intent_matches"}, + {"doc_chunk_counts", "doc_chunk_counts"}})); - test::AdapterHarness harness(adapter); - harness.Publish(kRawRequestIds, std::vector{1001, 2002}); + harness.Publish("raw_request_ids", std::vector{1001, 2002}); - // Reordered answers: index 1 before index 0 + // Perturbed order: index 1 published before index 0 TextBatch answers{{1, 0, "Answer 1"}, {0, 0, "Answer 0"}}; RuleMatchBatch intents{{0, 0, RuleMatchItem(1, "INTENT_0", "", "{}", 0.9f)}, {1, 0, RuleMatchItem(2, "INTENT_1", "", "{}", 0.8f)}}; Int32Batch chunks{{0, 0, 3}, {1, 0, 5}}; - harness.Publish(kLlmAnswers, answers); - harness.Publish(kIntentMatches, intents); - harness.Publish(kDocChunkCounts, chunks); + harness.Publish("llm_answers", std::move(answers)); + harness.Publish("intent_matches", std::move(intents)); + harness.Publish("doc_chunk_counts", std::move(chunks)); - // 1. Pack C array outputs std::vector outputs(2); - ASSERT_EQ(harness.PackC(&outputs), 0); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); EXPECT_EQ(outputs[0].request_id, 1001u); EXPECT_STREQ(outputs[0].answer_text, "Answer 0"); EXPECT_STREQ(outputs[0].intent_name, "INTENT_0"); @@ -662,601 +732,419 @@ TEST_F(AdapterPurityTest, DocQaAdapter_MultiWayResultsReorderedAndPerturbed) { EXPECT_STREQ(outputs[1].answer_text, "Answer 1"); EXPECT_STREQ(outputs[1].intent_name, "INTENT_1"); EXPECT_EQ(outputs[1].chunk_count, 5); +} - // 2. Pack owned Result outputs (dual representation behavior) - std::vector owned_outputs(2); - ASSERT_EQ(harness.PackOwned(&owned_outputs), 0); - EXPECT_EQ(owned_outputs[0].request_id, 1001u); - EXPECT_EQ(owned_outputs[0].answer_text, "Answer 0"); - EXPECT_EQ(owned_outputs[0].intent_name, "INTENT_0"); - EXPECT_EQ(owned_outputs[0].chunk_count, 3); - - EXPECT_EQ(owned_outputs[1].request_id, 2002u); - EXPECT_EQ(owned_outputs[1].answer_text, "Answer 1"); - EXPECT_EQ(owned_outputs[1].intent_name, "INTENT_1"); - EXPECT_EQ(owned_outputs[1].chunk_count, 5); - - // 3. Harness Perturbations: out-of-range req_id, invalid sub_id, duplicate - // req_id, missing item - for (const auto anomaly : - {test::AdapterHarness::ProvenanceAnomaly::kOutOfRangeReqId, - test::AdapterHarness::ProvenanceAnomaly::kInvalidSubId, - test::AdapterHarness::ProvenanceAnomaly::kDuplicateReqId, - test::AdapterHarness::ProvenanceAnomaly::kMissingReqId}) { - test::AdapterHarness h(adapter); - h.Publish(kRawRequestIds, std::vector{1001, 2002}); - auto perturbed_answers = test::AdapterHarness::PerturbBatch( - std::vector{"Answer 0", "Answer 1"}, anomaly); - h.Publish(kLlmAnswers, perturbed_answers); - h.Publish(kIntentMatches, intents); - h.Publish(kDocChunkCounts, chunks); - std::vector out(2); - EXPECT_EQ(h.PackC(&out), COMPANY_ALG_ERR_INVALID_INPUT); +// ========================================================================= +// 3. Section 13.1 Independent Reuse Proofs +// ========================================================================= + +// Proof 1: Input Converters Match Biz Declared Host Types & Prove Reuse via +// Test Binding +TEST_F(AdapterPurityTest, ReuseProof_1_InputConverterReusedAcrossBindings) { + const auto* entity_binding = + IoBindingRegistry::Instance().FindBinding("entity_extract.cabi.v1"); + ASSERT_NE(entity_binding, nullptr); + const auto* keyword_binding = + IoBindingRegistry::Instance().FindBinding("keyword_match.cabi.v1"); + ASSERT_NE(keyword_binding, nullptr); + + EXPECT_EQ(entity_binding->input_converter_id, "text.plain.cabi.v1"); + EXPECT_EQ(keyword_binding->input_converter_id, "keyword.plain.cabi.v1"); + + const auto* entity_conv = + IoConverterRegistry::Instance().FindInputConverter("text.plain.cabi.v1"); + ASSERT_NE(entity_conv, nullptr); + EXPECT_EQ(entity_conv->external_type, "CompanyEntityInputStruct"); + + const auto* keyword_conv = IoConverterRegistry::Instance().FindInputConverter( + "keyword.plain.cabi.v1"); + ASSERT_NE(keyword_conv, nullptr); + EXPECT_EQ(keyword_conv->external_type, "CompanyKeywordInputStruct"); + + // Decode input with entity binding: constructs CompanyEntityInputStruct + { + test::AdapterHarness harness( + entity_conv, InputPortBindings(entity_binding->input_ports)); + CompanyEntityInputStruct in{8001, "entity sentence"}; + EXPECT_EQ(harness.DecodeCAbi({&in}), 0); + const auto* sentences = + harness.Context().Read("input_sentences"); + ASSERT_NE(sentences, nullptr); + EXPECT_EQ((*sentences)[0].data, "entity sentence"); } -} -// RFC-0053: RequestResults Multi-Way Alignment Direct Unit Coverage -TEST(RequestResultsTest, MultiWayAlignmentAndAccessors) { - std::vector raw_ids = {100, 200}; - TextBatch answers{{0, 0, "ans0"}, {1, 0, "ans1"}}; - RuleMatchBatch intents{{0, 0, RuleMatchItem(1, "INTENT", "")}, - {1, 0, RuleMatchItem(2, "OTHER", "")}}; - Int32Batch chunks{{0, 0, 7}, {1, 0, 14}}; - - std::vector p = {&answers[0], &answers[1]}; - std::vector s0 = {&intents[0], - &intents[1]}; - std::vector s1 = {&chunks[0], &chunks[1]}; - - // With raw_request_ids - RequestResults results( - &raw_ids, std::move(p), std::make_tuple(std::move(s0), std::move(s1))); - - EXPECT_EQ(results.Size(), 2u); - EXPECT_EQ(results.RequestId(0), 100u); - EXPECT_EQ(results.RequestId(1), 200u); - EXPECT_EQ(results.Primary(0).data, "ans0"); - EXPECT_EQ(results.Primary(1).data, "ans1"); - EXPECT_EQ(results.Secondary<0>(0).data.category, "INTENT"); - EXPECT_EQ(results.Secondary<0>(1).data.category, "OTHER"); - EXPECT_EQ(results.Secondary<1>(0).data, 7); - EXPECT_EQ(results.Secondary<1>(1).data, 14); - - // Without raw_request_ids (falls back to primary req_id) - std::vector p_fallback = {&answers[0], - &answers[1]}; - RequestResults fallback_res(nullptr, std::move(p_fallback), {}); - EXPECT_EQ(fallback_res.RequestId(0), 0u); - EXPECT_EQ(fallback_res.RequestId(1), 1u); -} + // Decode input with keyword binding: constructs CompanyKeywordInputStruct + { + test::AdapterHarness harness( + keyword_conv, InputPortBindings(keyword_binding->input_ports)); + CompanyKeywordInputStruct in{8002, "keyword sentence"}; + EXPECT_EQ(harness.DecodeCAbi({&in}), 0); + const auto* sentences = + harness.Context().Read("input_sentences"); + ASSERT_NE(sentences, nullptr); + EXPECT_EQ((*sentences)[0].data, "keyword sentence"); + } -// RFC-0053: AdapterResult Direct Unit Coverage -TEST(AdapterResultTest, TypedAndVoidMethods) { - // 1. AdapterResult Ok - auto ok_res = AdapterResult::Ok("hello_edgeflow"); - EXPECT_TRUE(ok_res.IsOk()); - EXPECT_TRUE(static_cast(ok_res)); - EXPECT_EQ(ok_res.ReturnCode(), COMPANY_ALG_SUCCESS); - EXPECT_EQ(ok_res.Value(), "hello_edgeflow"); - EXPECT_EQ(*ok_res, "hello_edgeflow"); - EXPECT_EQ(ok_res->size(), 14u); - EXPECT_EQ(ok_res.ValueOr("default"), "hello_edgeflow"); - EXPECT_EQ(ok_res.TakeValue(), "hello_edgeflow"); - - // 2. AdapterResult InvalidInput - auto err_res = AdapterResult::InvalidInput( - "custom error", "req.field", 2, "TestAdapter"); - EXPECT_FALSE(err_res.IsOk()); - EXPECT_FALSE(static_cast(err_res)); - EXPECT_EQ(err_res.ReturnCode(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(err_res.Status().Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(err_res.Status().FieldPath(), "req.field"); - EXPECT_EQ(err_res.Status().SampleIndex(), 2); - EXPECT_EQ(err_res.Status().AdapterName(), "TestAdapter"); - EXPECT_EQ(err_res.ValueOr("fallback"), "fallback"); - - // 3. AdapterResult BufferTooSmall - auto buf_res = AdapterResult::BufferTooSmall("buffer short", "out.buf", - 0, "TestAdapter"); - EXPECT_FALSE(buf_res.IsOk()); - EXPECT_EQ(buf_res.ReturnCode(), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_EQ(buf_res.Status().Code(), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_EQ(buf_res.ValueOr(42), 42); - - // 4. AdapterResult - auto void_ok = AdapterResult::Ok(); - EXPECT_TRUE(void_ok.IsOk()); - EXPECT_EQ(void_ok.ReturnCode(), COMPANY_ALG_SUCCESS); - - auto void_err = AdapterResult::InvalidInput("void error"); - EXPECT_FALSE(void_err.IsOk()); - EXPECT_EQ(void_err.ReturnCode(), COMPANY_ALG_ERR_INVALID_INPUT); - - auto void_buf = AdapterResult::BufferTooSmall("void buf error"); - EXPECT_FALSE(void_buf.IsOk()); - EXPECT_EQ(void_buf.ReturnCode(), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); + // 跨业务复用证明:在测试专用绑定中复用 text.plain.cabi.v1 + { + IoBindingDefinition test_reuse_binding; + test_reuse_binding.binding_id = "test_purity_reuse.cabi.v1"; + test_reuse_binding.biz_name = "entity_extract_v1"; + test_reuse_binding.transport = "cabi"; + test_reuse_binding.input_converter_id = "text.plain.cabi.v1"; + test_reuse_binding.output_converter_id = "document.structured.cabi.v1"; + test_reuse_binding.input_ports = entity_binding->input_ports; + test_reuse_binding.output_ports = entity_binding->output_ports; + test_reuse_binding.max_batch_size = 64; + IoBindingRegistry::Instance().RegisterBinding(test_reuse_binding); + + const auto* b_test = + IoBindingRegistry::Instance().FindBinding("test_purity_reuse.cabi.v1"); + ASSERT_NE(b_test, nullptr); + EXPECT_EQ(b_test->input_converter_id, entity_binding->input_converter_id); + } } -// RFC-0053: ReadMultiWayResults Parameterized Reader Direct Coverage -TEST(ReadMultiWayResultsTest, ReadsAndAlignsMultiWayResults) { - AlgContext ctx; - ctx.Publish(kRawRequestIds, std::vector{3001, 3002}); - // Reordered answers to verify alignment - TextBatch answers{{1, 0, "ans_1"}, {0, 0, "ans_0"}}; - RuleMatchBatch intents{{0, 0, RuleMatchItem(1, "INTENT_A", "", "{}", 0.95f)}, - {1, 0, RuleMatchItem(2, "INTENT_B", "", "{}", 0.85f)}}; - Int32Batch chunks{{0, 0, 4}, {1, 0, 8}}; - - ctx.Publish(kLlmAnswers, std::move(answers)); - ctx.Publish(kIntentMatches, std::move(intents)); - ctx.Publish(kDocChunkCounts, std::move(chunks)); - - const ResultBindingSpec primary_spec( - kLlmAnswers, "answers", "answers", true, "Missing answers", - COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - const ResultBindingSpec> raw_req_ids_spec( - kRawRequestIds, "raw_request_ids", "raw_request_ids", true, - "raw_request_ids mismatch", COMPANY_ALG_ERR_INVALID_INPUT); - const ResultBindingSpec intent_spec( - kIntentMatches, "intent_matches", "intent_matches", true, - "intent_matches mismatch", COMPANY_ALG_ERR_INVALID_INPUT); - const ResultBindingSpec chunk_spec( - kDocChunkCounts, "doc_chunk_counts", "chunk_counts", true, - "doc_chunk_counts mismatch", COMPANY_ALG_ERR_INVALID_INPUT); - - std::vector outs(2); - void* output_ptrs[2] = {&outs[0], &outs[1]}; - int count = 2; - AdapterStatus status; - RequestResults results; - int ret = ReadMultiWayResults(&ctx, output_ptrs, &count, "TestDocQA", &status, - &results, primary_spec, raw_req_ids_spec, - intent_spec, chunk_spec); - ASSERT_EQ(ret, COMPANY_ALG_SUCCESS); - ASSERT_EQ(results.Size(), 2u); - - EXPECT_EQ(results.RequestId(0), 3001u); - EXPECT_EQ(results.Primary(0).data, "ans_0"); - EXPECT_EQ(results.Secondary<0>(0).data.category, "INTENT_A"); - EXPECT_FLOAT_EQ(results.Secondary<0>(0).data.score, 0.95f); - EXPECT_EQ(results.Secondary<1>(0).data, 4); - - EXPECT_EQ(results.RequestId(1), 3002u); - EXPECT_EQ(results.Primary(1).data, "ans_1"); - EXPECT_EQ(results.Secondary<0>(1).data.category, "INTENT_B"); - EXPECT_FLOAT_EQ(results.Secondary<0>(1).data.score, 0.85f); - EXPECT_EQ(results.Secondary<1>(1).data, 8); - - // Core reader without outputs/num_outputs - RequestResults core_results; - EXPECT_EQ(ReadMultiWayResults(&ctx, "TestDocQA", &status, &core_results, - primary_spec, raw_req_ids_spec, intent_spec, - chunk_spec), - COMPANY_ALG_SUCCESS); - EXPECT_EQ(core_results.Size(), 2u); - - // Null num_outputs returns BUFFER_TOO_SMALL safely without crashing - EXPECT_EQ(ReadMultiWayResults(&ctx, output_ptrs, nullptr, "TestDocQA", - &status, &results, primary_spec, - raw_req_ids_spec, intent_spec, chunk_spec), - COMPANY_ALG_ERR_BUFFER_TOO_SMALL); -} +// Proof 2: Output Converter Reused Across Pipelines +TEST_F(AdapterPurityTest, ReuseProof_2_OutputConverterReusedAcrossPipelines) { + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "document.structured.cabi.v1"); + ASSERT_NE(out_conv, nullptr); -// 验证变长模板推导:0 路次要结果与 3 路次要结果对齐 -TEST(ReadMultiWayResultsTest, VariadicSecondarySlotsDeductionAndAlignment) { - AlgContext ctx; - ctx.Publish(kRawRequestIds, std::vector{2001, 2002}); - ctx.Publish(kLlmAnswers, TextBatch{{0, 0, "ans_0"}, {1, 0, "ans_1"}}); - ctx.Publish(kIntentMatches, - RuleMatchBatch{{0, 0, RuleMatchItem(1, "A", "", "{}", 0.9f)}, - {1, 0, RuleMatchItem(2, "B", "", "{}", 0.8f)}}); - ctx.Publish(kDocChunkCounts, Int32Batch{{0, 0, 3}, {1, 0, 5}}); - ctx.Publish(kDocChunks, TextBatch{{0, 0, "chunk_0"}, {1, 0, "chunk_1"}}); - - const ResultBindingSpec primary_spec(kLlmAnswers, "answers"); - const ResultBindingSpec> raw_req_ids_spec( - kRawRequestIds, "raw_request_ids"); - const ResultBindingSpec intent_spec(kIntentMatches, - "intent_matches"); - const ResultBindingSpec chunk_spec(kDocChunkCounts, - "doc_chunk_counts"); - const ResultBindingSpec doc_spec(kDocChunks, "doc_chunks"); + OutputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"extracted_entities", "extracted_entities"}}); - AdapterStatus status; + // Context A: Entity Extraction pipeline output + { + test::AdapterHarness harness(out_conv, bindings); + harness.Publish("raw_request_ids", std::vector{9001}); + StructuredDocumentBatch batch; + batch.emplace_back( + 0, 0, + JsonDocumentItem("[\"PERSON: Alice\"]", true, JsonParseStatus::kOk)); + harness.Publish("extracted_entities", std::move(batch)); + + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); + EXPECT_EQ(outputs[0].request_id, 9001U); + EXPECT_STREQ(outputs[0].entities_json, "[\"PERSON: Alice\"]"); + } - // 1. 0 路次要槽位(仅 primary + raw_req_ids) - RequestResults zero_results; - int ret0 = ReadMultiWayResults(&ctx, "TestAdapter", &status, &zero_results, - primary_spec, raw_req_ids_spec); - ASSERT_EQ(ret0, COMPANY_ALG_SUCCESS); - ASSERT_EQ(zero_results.Size(), 2u); - EXPECT_EQ(zero_results.RequestId(0), 2001u); - EXPECT_EQ(zero_results.Primary(0).data, "ans_0"); - EXPECT_EQ(zero_results.RequestId(1), 2002u); - EXPECT_EQ(zero_results.Primary(1).data, "ans_1"); - - // 2. 3 路次要槽位(3+ variadic secondary specs) - RequestResults - three_results; - int ret3 = ReadMultiWayResults(&ctx, "TestAdapter", &status, &three_results, - primary_spec, raw_req_ids_spec, intent_spec, - chunk_spec, doc_spec); - ASSERT_EQ(ret3, COMPANY_ALG_SUCCESS); - ASSERT_EQ(three_results.Size(), 2u); - EXPECT_EQ(three_results.RequestId(0), 2001u); - EXPECT_EQ(three_results.Primary(0).data, "ans_0"); - EXPECT_EQ(three_results.Secondary<0>(0).data.category, "A"); - EXPECT_EQ(three_results.Secondary<1>(0).data, 3); - EXPECT_EQ(three_results.Secondary<2>(0).data, "chunk_0"); - - EXPECT_EQ(three_results.RequestId(1), 2002u); - EXPECT_EQ(three_results.Primary(1).data, "ans_1"); - EXPECT_EQ(three_results.Secondary<0>(1).data.category, "B"); - EXPECT_EQ(three_results.Secondary<1>(1).data, 5); - EXPECT_EQ(three_results.Secondary<2>(1).data, "chunk_1"); + // Context B: Generic structured JSON pipeline output producing same schema + { + test::AdapterHarness harness(out_conv, bindings); + harness.Publish("raw_request_ids", std::vector{9002}); + StructuredDocumentBatch batch; + batch.emplace_back( + 0, 0, + JsonDocumentItem("{\"summary\":\"ok\"}", true, JsonParseStatus::kOk)); + harness.Publish("extracted_entities", std::move(batch)); + + std::vector outputs(1); + ASSERT_EQ(harness.EncodeCAbi(&outputs), 0); + EXPECT_EQ(outputs[0].request_id, 9002U); + EXPECT_STREQ(outputs[0].entities_json, "{\"summary\":\"ok\"}"); + } } -// RFC-0053: ReadMultiWayResults Error Mappings and Diagnostics -TEST(ReadMultiWayResultsTest, ErrorMappingsAndDiagnostics) { - AlgContext ctx; - AdapterStatus status; - RequestResults results; - - // 1. Missing primary with custom error code (-7) - const ResultBindingSpec custom_primary_spec( - kLlmAnswers, "custom_field", "custom_field", true, "Custom missing msg", - -7); - const ResultBindingSpec> raw_spec( - kRawRequestIds, "raw_req_ids", "raw_req_ids", true); - - EXPECT_EQ(ReadMultiWayResults(&ctx, "TestAdapter", &status, &results, - custom_primary_spec, raw_spec), - -7); - EXPECT_EQ(status.Code(), -7); - EXPECT_EQ(status.FieldPath(), "custom_field"); - EXPECT_EQ(status.Message(), "Custom missing msg"); - - // 2. Count mismatch in secondary batch - ctx.Publish(kLlmAnswers, TextBatch{{0, 0, "ans0"}, {1, 0, "ans1"}}); - ctx.Publish(kRawRequestIds, std::vector{1, 2}); - ctx.Publish(kDocChunkCounts, Int32Batch{{0, 0, 1}}); // only 1 item != 2 - - const ResultBindingSpec ok_primary_spec(kLlmAnswers, "answers"); - const ResultBindingSpec secondary_spec( - kDocChunkCounts, "chunk_counts", "chunk_counts", true, "count mismatch", - COMPANY_ALG_ERR_INVALID_INPUT); - - RequestResults mismatch_results; - EXPECT_EQ(ReadMultiWayResults(&ctx, "TestAdapter", &status, &mismatch_results, - ok_primary_spec, raw_spec, secondary_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "chunk_counts"); - EXPECT_EQ(status.Message(), "count mismatch"); +// Proof 3: Multiple External Input Formats Driving Same Pipeline +TEST_F(AdapterPurityTest, + ReuseProof_3_MultipleExternalInputFormatsForSamePipeline) { + // Register custom input converter that converts CustomMultiFieldInput to + // TextBatch + InputConverterDefinition custom_in_def; + custom_in_def.converter_id = "test.multi_field.cabi.v1"; + custom_in_def.transport = "cabi"; + custom_in_def.schema_id = "multi_field.request"; + custom_in_def.schema_version = 1; + custom_in_def.external_type = "CustomMultiFieldInput"; + custom_in_def.external_slots = {ExternalSlotDefinition( + "inputs", "CustomMultiFieldInput", PortDirection::kInput, true)}; + custom_in_def.max_batch_size = 64; + custom_in_def.logical_ports = { + NodePortDefinition("raw_request_ids", "vector", true, "1:1"), + NodePortDefinition("texts", "TextBatch", true, "1:1")}; + custom_in_def.decode_fn = [](const ExternalInputBatchView& src, + const InputDecodeOptions&, + const InputPortBindings& bindings, + AlgContext* ctx, AdapterStatus*) -> int { + std::vector ids; + TextBatch texts; + for (size_t i = 0; i < src.count; ++i) { + const auto* item = src.GetCAbi(i); + if (!item) return -3; + ids.push_back(item->req_id); + std::string combined = + std::string(item->topic) + ": " + std::string(item->content); + texts.emplace_back(static_cast(i), 0, combined); + } + ctx->Publish(bindings.GetActualKey("raw_request_ids"), ids); + ctx->Publish(bindings.GetActualKey("texts"), texts); + return 0; + }; + + EXPECT_TRUE( + IoConverterRegistry::Instance().RegisterInputConverter(custom_in_def)); + + // Format A: CompanyEntityInputStruct via text.plain.cabi.v1 + AlgContext ctx_a; + { + const auto* in_a = IoConverterRegistry::Instance().FindInputConverter( + "text.plain.cabi.v1"); + ASSERT_NE(in_a, nullptr); + CompanyEntityInputStruct req_a{777, "AI: Revolution in robotics"}; + const void* items[] = {&req_a}; + ExternalInputBatchView view; + view.items = items; + view.count = 1; + view.type_id = "CompanyEntityInputStruct"; + InputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}); + InputDecodeOptions opts; + opts.converter_id = in_a->converter_id; + AdapterStatus st; + ASSERT_EQ(in_a->decode_fn(view, opts, bindings, &ctx_a, &st), 0); + } + + // Format B: CustomMultiFieldInput via test.multi_field.cabi.v1 + AlgContext ctx_b; + { + const auto* in_b = IoConverterRegistry::Instance().FindInputConverter( + "test.multi_field.cabi.v1"); + ASSERT_NE(in_b, nullptr); + CustomMultiFieldInput req_b{777, "AI", "Revolution in robotics"}; + const void* items[] = {&req_b}; + ExternalInputBatchView view; + view.items = items; + view.count = 1; + view.type_id = "CustomMultiFieldInput"; + InputPortBindings bindings( + {{"raw_request_ids", "raw_request_ids"}, {"texts", "input_sentences"}}); + InputDecodeOptions opts; + opts.converter_id = in_b->converter_id; + AdapterStatus st; + ASSERT_EQ(in_b->decode_fn(view, opts, bindings, &ctx_b, &st), 0); + } + + // Both produce identical internal TextBatch on "input_sentences" + const auto* texts_a = ctx_a.Read("input_sentences"); + const auto* texts_b = ctx_b.Read("input_sentences"); + ASSERT_NE(texts_a, nullptr); + ASSERT_NE(texts_b, nullptr); + EXPECT_EQ((*texts_a)[0].data, (*texts_b)[0].data); + EXPECT_EQ((*texts_a)[0].data, "AI: Revolution in robotics"); } -// RFC-0053 §2.2: ReadMultiWayResults Rejects Non-Required Bindings -TEST(ReadMultiWayResultsTest, RejectsNonRequiredBindingsWithDiagnostic) { +// Proof 4: Independently Switch Output Formats for Same Pipeline +TEST_F(AdapterPurityTest, ReuseProof_4_IndependentlySwitchOutputFormat) { + // Prepare common AlgContext with both structured document and keyword results AlgContext ctx; - ctx.Publish(kLlmAnswers, TextBatch{{0, 0, "ans0"}}); - ctx.Publish(kRawRequestIds, std::vector{1001}); - ctx.Publish(kDocChunkCounts, Int32Batch{{0, 0, 1}}); + ctx.Publish("raw_request_ids", std::vector{5001}); - AdapterStatus status; - RequestResults results; - - // 1. Non-required primary spec - const ResultBindingSpec opt_primary_spec(kLlmAnswers, "answers", - "answers", /*req=*/false); - const ResultBindingSpec> req_raw_spec( - kRawRequestIds, "raw_request_ids", "raw_request_ids", /*req=*/true); - const ResultBindingSpec req_secondary_spec( - kDocChunkCounts, "chunk_counts", "chunk_counts", /*req=*/true); - - EXPECT_EQ( - ReadMultiWayResults(&ctx, "TestAdapter", &status, &results, - opt_primary_spec, req_raw_spec, req_secondary_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "answers"); - EXPECT_EQ(status.Message(), - "Optional bindings not supported in current phase"); - EXPECT_EQ(status.AdapterName(), "TestAdapter"); - - // 2. Non-required raw_req_ids spec - const ResultBindingSpec req_primary_spec(kLlmAnswers, "answers", - "answers", /*req=*/true); - const ResultBindingSpec> opt_raw_spec( - kRawRequestIds, "raw_request_ids", "raw_request_ids", /*req=*/false); - - EXPECT_EQ( - ReadMultiWayResults(&ctx, "TestAdapter", &status, &results, - req_primary_spec, opt_raw_spec, req_secondary_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "raw_request_ids"); - EXPECT_EQ(status.Message(), - "Optional bindings not supported in current phase"); - - // 3. Non-required secondary spec - const ResultBindingSpec opt_secondary_spec( - kDocChunkCounts, "chunk_counts", "chunk_counts", /*req=*/false); - - EXPECT_EQ( - ReadMultiWayResults(&ctx, "TestAdapter", &status, &results, - req_primary_spec, req_raw_spec, opt_secondary_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "chunk_counts"); - EXPECT_EQ(status.Message(), - "Optional bindings not supported in current phase"); - - // 4. Overload with outputs and num_outputs also rejects non-required bindings - CompanyDocOutputStruct out; - void* output_ptrs[1] = {&out}; - int count = 1; - - EXPECT_EQ(ReadMultiWayResults(&ctx, output_ptrs, &count, "TestAdapter", - &status, &results, opt_primary_spec, - req_raw_spec, req_secondary_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "answers"); - EXPECT_EQ(status.Message(), - "Optional bindings not supported in current phase"); - - EXPECT_EQ(ReadMultiWayResults(&ctx, output_ptrs, &count, "TestAdapter", - &status, &results, req_primary_spec, - req_raw_spec, opt_secondary_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "chunk_counts"); - EXPECT_EQ(status.Message(), - "Optional bindings not supported in current phase"); - - EXPECT_EQ(ReadMultiWayResults(&ctx, output_ptrs, &count, "TestAdapter", - &status, &results, req_primary_spec, - opt_raw_spec, req_secondary_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "raw_request_ids"); - EXPECT_EQ(status.Message(), - "Optional bindings not supported in current phase"); - - // 5. Multiple secondary specs short-circuiting to first non-required binding - const ResultBindingSpec req_intent_spec( - kIntentMatches, "intent_matches", "intent_matches", /*req=*/true); - const ResultBindingSpec opt_intent_spec( - kIntentMatches, "intent_matches", "intent_matches", /*req=*/false); - RequestResults multi_sec_results; - - // Second secondary spec is optional -> rejected with second secondary - // field_name - EXPECT_EQ( - ReadMultiWayResults(&ctx, "TestAdapter", &status, &multi_sec_results, - req_primary_spec, req_raw_spec, req_secondary_spec, - opt_intent_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "intent_matches"); - EXPECT_EQ(status.Message(), - "Optional bindings not supported in current phase"); - - // Both secondary specs are optional -> short-circuits to first non-required - // secondary - EXPECT_EQ( - ReadMultiWayResults(&ctx, "TestAdapter", &status, &multi_sec_results, - req_primary_spec, req_raw_spec, opt_secondary_spec, - opt_intent_spec), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "chunk_counts"); - EXPECT_EQ(status.Message(), - "Optional bindings not supported in current phase"); + StructuredDocumentBatch docs; + docs.emplace_back( + 0, 0, JsonDocumentItem("[\"item_1\"]", true, JsonParseStatus::kOk)); + ctx.Publish("extracted_entities", std::move(docs)); + + RuleMatchBatch rules; + rules.emplace_back( + 0, 0, RuleMatchItem(1, "URGENT", "急", "{\"flag\":\"urgent\"}", 0.99f)); + ctx.Publish("rule_matches", std::move(rules)); + + // Binding Output A: document.structured.cabi.v1 -> CompanyEntityOutputStruct + { + const auto* out_a = IoConverterRegistry::Instance().FindOutputConverter( + "document.structured.cabi.v1"); + ASSERT_NE(out_a, nullptr); + CompanyEntityOutputStruct out{}; + void* items[] = {&out}; + ExternalOutputBatchView dest; + dest.items = items; + dest.count = 1; + dest.capacity = 1; + dest.type_id = out_a->external_type; + OutputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"extracted_entities", "extracted_entities"}}); + OutputEncodeOptions opts; + opts.converter_id = out_a->converter_id; + size_t written = 0; + AdapterStatus st; + ASSERT_EQ(out_a->encode_fn(&ctx, bindings, opts, &dest, &written, &st), 0); + EXPECT_EQ(out.request_id, 5001U); + EXPECT_STREQ(out.entities_json, "[\"item_1\"]"); + } + + // Binding Output B: keyword.result.cabi.v1 -> CompanyKeywordOutputStruct + { + const auto* out_b = IoConverterRegistry::Instance().FindOutputConverter( + "keyword.result.cabi.v1"); + ASSERT_NE(out_b, nullptr); + CompanyKeywordOutputStruct out{}; + void* items[] = {&out}; + ExternalOutputBatchView dest; + dest.items = items; + dest.count = 1; + dest.capacity = 1; + dest.type_id = out_b->external_type; + OutputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"rule_matches", "rule_matches"}}); + OutputEncodeOptions opts; + opts.converter_id = out_b->converter_id; + size_t written = 0; + AdapterStatus st; + ASSERT_EQ(out_b->encode_fn(&ctx, bindings, opts, &dest, &written, &st), 0); + EXPECT_EQ(out.request_id, 5001U); + EXPECT_EQ(out.is_hit, 1); + EXPECT_STREQ(out.match_result_json, "{\"flag\":\"urgent\"}"); + } } -// RFC-0053: OneToOneTextAdapter Custom Null Context Hooks -TEST(OneToOneTextAdapterTest, CustomNullContextHooks) { - struct CustomHookSpecProvider { - static const OneToOneTextAdapterSpec& GetSpec() { - static const OneToOneTextAdapterSpec spec = [] { - OneToOneTextAdapterSpec s; - s.biz_type = static_cast(999); - s.adapter_name = "CustomHook"; - s.unpack_null_ctx_hook = [](const char* name, - AdapterStatus* out_status) -> int { - if (out_status) { - *out_status = AdapterStatus(-123, "Custom unpack null ctx", - "custom_unpack", -1, name); - } - return -123; - }; - s.pack_null_ctx_hook = [](const char* name, - AdapterStatus* out_status) -> int { - if (out_status) { - *out_status = AdapterStatus(-456, "Custom pack null ctx", - "custom_pack", -1, name); - } - return -456; - }; - return s; - }(); - return spec; - } - }; +// Proof 5: Same Carrier with Different Schemas +TEST_F(AdapterPurityTest, ReuseProof_5_SameCarrierDifferentSchema) { + const auto* plain_conv = + IoConverterRegistry::Instance().FindInputConverter("text.plain.cabi.v1"); + ASSERT_NE(plain_conv, nullptr); + const auto* json_conv = IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(json_conv, nullptr); + + // Payload 1: Pure plain text "Hello plain text" + CompanyEntityInputStruct plain_req{101, "Hello plain text"}; + const void* plain_items[] = {&plain_req}; + ExternalInputBatchView plain_view; + plain_view.items = plain_items; + plain_view.count = 1; + plain_view.type_id = "CompanyEntityInputStruct"; + + InputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}); + InputDecodeOptions opts; + + // text.plain.cabi.v1 accepts it as plain text + { + AlgContext ctx; + AdapterStatus st; + opts.converter_id = plain_conv->converter_id; + EXPECT_EQ(plain_conv->decode_fn(plain_view, opts, bindings, &ctx, &st), 0); + const auto* s = ctx.Read("input_sentences"); + ASSERT_NE(s, nullptr); + EXPECT_EQ((*s)[0].data, "Hello plain text"); + } - OneToOneTextAdapter adapter; - AdapterStatus status; + // translate.json.cabi.v1 rejects it because it is not JSON + { + AlgContext ctx; + AdapterStatus st; + opts.converter_id = json_conv->converter_id; + EXPECT_EQ(json_conv->decode_fn(plain_view, opts, bindings, &ctx, &st), + COMPANY_ALG_ERR_INVALID_INPUT); + EXPECT_EQ(st.FieldPath(), "json"); + } - CompanyEntityInputStruct input{1, "test"}; - const void* inputs[] = {&input}; - EXPECT_EQ(adapter.Unpack(inputs, 1, nullptr, &status), -123); - EXPECT_EQ(status.Code(), -123); - EXPECT_EQ(status.FieldPath(), "custom_unpack"); - EXPECT_EQ(status.Message(), "Custom unpack null ctx"); - - CompanyEntityOutputStruct output{}; - void* outputs[] = {&output}; - int count = 1; - EXPECT_EQ(adapter.Pack(nullptr, outputs, &count, &status), -456); - EXPECT_EQ(status.Code(), -456); - EXPECT_EQ(status.FieldPath(), "custom_pack"); - EXPECT_EQ(status.Message(), "Custom pack null ctx"); + // Payload 2: JSON formatted string "{\"query\": \"Hello JSON\"}" + CompanyEntityInputStruct json_req{102, "{\"query\": \"Hello JSON\"}"}; + const void* json_items[] = {&json_req}; + ExternalInputBatchView json_view; + json_view.items = json_items; + json_view.count = 1; + json_view.type_id = "CompanyEntityInputStruct"; + + // translate.json.cabi.v1 succeeds and extracts "query" + { + AlgContext ctx; + AdapterStatus st; + opts.converter_id = json_conv->converter_id; + EXPECT_EQ(json_conv->decode_fn(json_view, opts, bindings, &ctx, &st), 0); + const auto* s = ctx.Read("input_sentences"); + ASSERT_NE(s, nullptr); + EXPECT_EQ((*s)[0].data, "Hello JSON"); + } } -// RFC-0053: DocQaAdapter Characterization and Diagnostics -TEST_F(AdapterPurityTest, DocQaAdapterDiagnosticsCharacterization) { - auto adapter = BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(adapter, nullptr); +// Proof 6: Negative Combinations Rejected +TEST_F(AdapterPurityTest, ReuseProof_6_NegativeCombinations) { + // 1. Unknown or unregistered io_binding + DeploymentIoConfig bad_binding_cfg; + bad_binding_cfg.io_binding = "non_existent.binding.v999"; + bad_binding_cfg.pipe_path = "pipeline_keyword_match_rules.json"; + std::unique_ptr plan; + std::string error; + int ret = IoBindingResolver::ResolveFromConfig(bad_binding_cfg, "cabi", + "./models", &plan, &error); + EXPECT_EQ(ret, -2); + EXPECT_NE(error.find("Unknown or unregistered io_binding"), + std::string::npos); - CompanyDocOutputStruct out{}; - void* outputs[] = {&out}; - int count = 1; - AdapterStatus status; + // 2. Transport mismatch: CABI requested, but operator binding specified + DeploymentIoConfig mismatch_cfg; + mismatch_cfg.io_binding = "keyword_match.operator.v1"; + mismatch_cfg.pipe_path = "pipeline_keyword_match_rules.json"; + ret = IoBindingResolver::ResolveFromConfig(mismatch_cfg, "cabi", "./models", + &plan, &error); + EXPECT_EQ(ret, -2); + EXPECT_NE(error.find("Binding transport mismatch"), std::string::npos); + + // 3. DeploymentIoConfig schema validation rejects invalid version + nlohmann::json invalid_version_json = { + {"schema_version", 999}, + {"data", + {{"pipe_path", "test.json"}, {"io_binding", "keyword_match.cabi.v1"}}}}; + DeploymentIoConfig parsed_cfg; + EXPECT_FALSE(DeploymentIoConfig::Parse(invalid_version_json, ".", "cabi", + &parsed_cfg, &error)); + EXPECT_NE(error.find("schema_version"), std::string::npos); + + // 4. CABI config rejects outputs block + nlohmann::json cabi_with_outputs_json = { + {"schema_version", 1}, + {"data", + {{"pipe_path", "test.json"}, + {"io_binding", "keyword_match.cabi.v1"}, + {"outputs", {{"main", {{"type", "test"}}}}}}}}; + EXPECT_FALSE(DeploymentIoConfig::Parse(cabi_with_outputs_json, ".", "cabi", + &parsed_cfg, &error)); + EXPECT_NE(error.find("outputs"), std::string::npos); + + // 5. Operator config with unknown output slot rejected by parity check + DeploymentIoConfig unknown_out_cfg; + unknown_out_cfg.io_binding = "keyword_match.operator.v1"; + unknown_out_cfg.pipe_path = "pipeline_keyword_match_rules.json"; + unknown_out_cfg.outputs = {{"unknown_slot", {{"type", "String"}}}}; + ret = IoBindingResolver::ResolveFromConfig(unknown_out_cfg, "operator", + "./models", &plan, &error); + EXPECT_EQ(ret, -2); + EXPECT_NE(error.find("Unknown configured output slot: unknown_slot"), + std::string::npos); - // 1. Null AlgContext: returns BUFFER_TOO_SMALL (-4) with field "ctx" - EXPECT_EQ(adapter->Pack(nullptr, outputs, &count, &status), - COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_EQ(status.FieldPath(), "ctx"); - - // 2. Empty AlgContext (missing answers): returns BUFFER_TOO_SMALL (-4) with - // field "llm_answers" and legacy message "llm_answers not found in - // AlgContext" - AlgContext empty_ctx; - EXPECT_EQ(adapter->Pack(&empty_ctx, outputs, &count, &status), - COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_EQ(status.FieldPath(), "llm_answers"); - EXPECT_EQ(status.Message(), "llm_answers not found in AlgContext"); - - // 3. Null num_outputs pointer with valid context: returns BUFFER_TOO_SMALL - // (-4) safely without crashing - AlgContext valid_ctx; - valid_ctx.Publish(kRawRequestIds, std::vector{1001}); - valid_ctx.Publish(kLlmAnswers, TextBatch{{0, 0, "Doc answer"}}); - valid_ctx.Publish( - kIntentMatches, - RuleMatchBatch{{0, 0, RuleMatchItem(0, "QA", "", "{}", 1.0f)}}); - valid_ctx.Publish(kDocChunkCounts, Int32Batch{{0, 0, 5}}); - - EXPECT_EQ(adapter->Pack(&valid_ctx, outputs, nullptr, &status), - COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - - // 4. Missing intent_matches: returns INVALID_INPUT (-3) with field - // "intent_matches" - AlgContext no_intent; - no_intent.Publish(kRawRequestIds, std::vector{1001}); - no_intent.Publish(kLlmAnswers, TextBatch{{0, 0, "Doc answer"}}); - no_intent.Publish(kDocChunkCounts, Int32Batch{{0, 0, 5}}); - count = 1; - EXPECT_EQ(adapter->Pack(&no_intent, outputs, &count, &status), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "intent_matches"); - - // 5. Missing doc_chunk_counts: returns INVALID_INPUT (-3) with field - // "doc_chunk_counts" - AlgContext no_chunks; - no_chunks.Publish(kRawRequestIds, std::vector{1001}); - no_chunks.Publish(kLlmAnswers, TextBatch{{0, 0, "Doc answer"}}); - no_chunks.Publish( - kIntentMatches, - RuleMatchBatch{{0, 0, RuleMatchItem(0, "QA", "", "{}", 1.0f)}}); - count = 1; - EXPECT_EQ(adapter->Pack(&no_chunks, outputs, &count, &status), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "doc_chunk_counts"); - - // 6. Missing raw_request_ids: returns INVALID_INPUT (-3) with field - // "raw_request_ids" - AlgContext no_ids; - no_ids.Publish(kLlmAnswers, TextBatch{{0, 0, "Doc answer"}}); - no_ids.Publish( - kIntentMatches, - RuleMatchBatch{{0, 0, RuleMatchItem(0, "QA", "", "{}", 1.0f)}}); - no_ids.Publish(kDocChunkCounts, Int32Batch{{0, 0, 5}}); - count = 1; - EXPECT_EQ(adapter->Pack(&no_ids, outputs, &count, &status), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "raw_request_ids"); - - // 7. Successful Pack - count = 1; - EXPECT_EQ(adapter->Pack(&valid_ctx, outputs, &count, &status), - COMPANY_ALG_SUCCESS); - EXPECT_EQ(count, 1); - EXPECT_EQ(out.request_id, 1001u); - EXPECT_FLOAT_EQ(out.confidence, 1.0f); - EXPECT_EQ(out.chunk_count, 5); - EXPECT_STREQ(out.intent_name, "QA"); - EXPECT_STREQ(out.answer_text, "Doc answer"); - - // 8. Invalid provenance on answers: IndexResults fails with field "answers" - // (index_name), distinguishing it from missing "llm_answers" - AlgContext bad_answers_ctx; - bad_answers_ctx.Publish(kRawRequestIds, std::vector{1001}); - bad_answers_ctx.Publish(kLlmAnswers, - TextBatch{{5, 0, "Doc answer"}}); // req_id 5 >= 1 - bad_answers_ctx.Publish( - kIntentMatches, - RuleMatchBatch{{0, 0, RuleMatchItem(0, "QA", "", "{}", 1.0f)}}); - bad_answers_ctx.Publish(kDocChunkCounts, Int32Batch{{0, 0, 5}}); - count = 1; - EXPECT_EQ(adapter->Pack(&bad_answers_ctx, outputs, &count, &status), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "answers"); - EXPECT_EQ(status.Message(), - "Missing, duplicate or invalid result provenance"); - - // 9. Invalid provenance on doc_chunk_counts: IndexResults fails with field - // "chunk_counts" (index_name), distinguishing it from missing - // "doc_chunk_counts" - AlgContext bad_chunks_ctx; - bad_chunks_ctx.Publish(kRawRequestIds, std::vector{1001}); - bad_chunks_ctx.Publish(kLlmAnswers, TextBatch{{0, 0, "Doc answer"}}); - bad_chunks_ctx.Publish( - kIntentMatches, - RuleMatchBatch{{0, 0, RuleMatchItem(0, "QA", "", "{}", 1.0f)}}); - bad_chunks_ctx.Publish(kDocChunkCounts, - Int32Batch{{5, 0, 5}}); // req_id 5 >= 1 - count = 1; - EXPECT_EQ(adapter->Pack(&bad_chunks_ctx, outputs, &count, &status), - COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.Code(), COMPANY_ALG_ERR_INVALID_INPUT); - EXPECT_EQ(status.FieldPath(), "chunk_counts"); - EXPECT_EQ(status.Message(), - "Missing, duplicate or invalid result provenance"); + // 6. Unknown model_id in model_paths rejected + DeploymentIoConfig unknown_mid_cfg; + unknown_mid_cfg.io_binding = "keyword_match.cabi.v1"; + unknown_mid_cfg.pipe_path = "configs/pipeline_keyword_match_rules.json"; + unknown_mid_cfg.resolved_pipe_path = + "configs/pipeline_keyword_match_rules.json"; + unknown_mid_cfg.model_paths = {{"non_existent_model", "dummy_path"}}; + ret = IoBindingResolver::ResolveFromConfig(unknown_mid_cfg, "cabi", + "./models", &plan, &error); + EXPECT_EQ(ret, -2); + EXPECT_NE( + error.find("Unknown model_id 'non_existent_model' in 'model_paths'"), + std::string::npos); } -// RFC-0053: AdapterHarness Capacity Pre-Query -TEST_F(AdapterPurityTest, AdapterHarnessCapacityPreQuery) { - auto adapter = BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(adapter, nullptr); - - test::AdapterHarness harness(adapter); - harness.Publish(kRawRequestIds, std::vector{10, 20, 30}); - harness.Publish(kLlmAnswers, - TextBatch{{0, 0, "a0"}, {1, 0, "a1"}, {2, 0, "a2"}}); - harness.Publish(kIntentMatches, RuleMatchBatch{{0, 0, RuleMatchItem{}}, - {1, 0, RuleMatchItem{}}, - {2, 0, RuleMatchItem{}}}); - harness.Publish(kDocChunkCounts, Int32Batch{{0, 0, 1}, {1, 0, 2}, {2, 0, 3}}); - - // Query capacity with empty outputs vector - std::vector empty_c_outs; - int ret_c = harness.PackC(&empty_c_outs); - EXPECT_EQ(ret_c, COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_TRUE(empty_c_outs.empty()); - - std::vector empty_owned_outs; - int ret_owned = harness.PackOwned(&empty_owned_outs); - EXPECT_EQ(ret_owned, COMPANY_ALG_ERR_BUFFER_TOO_SMALL); - EXPECT_TRUE(empty_owned_outs.empty()); +// Proof 7: Validation Before Initialization (probe model not loaded on invalid +// binding) +TEST_F(AdapterPurityTest, ReuseProof_7_ValidationBeforeInitialization) { + // Attempt to create algorithm instance with non-existent or invalid binding + // configuration + CompanyAlgParamCreate param{}; + param.config_file_path = "non_existent_path.json"; + param.model_root_dir = "./models"; + param.device_id = 0; + + void* handle = nullptr; + int ret = Alg_Create(&handle, ¶m); + EXPECT_NE(ret, COMPANY_ALG_SUCCESS); + EXPECT_EQ(handle, nullptr); } } // namespace llm_edgeflow diff --git a/tests/unit/adapter/test_complex_converters.cpp b/tests/unit/adapter/test_complex_converters.cpp new file mode 100644 index 00000000..f611e10b --- /dev/null +++ b/tests/unit/adapter/test_complex_converters.cpp @@ -0,0 +1,394 @@ +#include + +#include +#include +#include + +#include "adapter/adapter_status.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter.h" +#include "adapter/io_converter_registry.h" +#include "contracts/inference_payloads.h" +#include "core/alg_context.h" +#include "core/common_contracts.h" +#include "core/pipeline_catalog.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { + +class ComplexConvertersTest : public ::testing::Test {}; + +// ==================== DocQA ==================== +TEST_F(ComplexConvertersTest, DocQaCAbiInputAndOutput) { + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "doc_query.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "doc_answer.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + CompanyDocInputStruct doc_in{2001, "Sample document text", "What is sample?"}; + const void* in_items[] = {&doc_in}; + ExternalInputBatchView in_view; + in_view.items = in_items; + in_view.count = 1; + + InputPortBindings in_bindings({{"raw_request_ids", "raw_request_ids"}, + {"raw_docs", "raw_docs"}, + {"raw_queries", "raw_queries"}}); + InputDecodeOptions in_options; + in_options.converter_id = in_conv->converter_id; + + AlgContext ctx; + AdapterStatus status; + int ret = in_conv->decode_fn(in_view, in_options, in_bindings, &ctx, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + + // Populate answer context + TextBatch answers; + answers.emplace_back(0, 0, "This is the answer."); + ctx.Publish("llm_answers", std::move(answers)); + + RuleMatchBatch intents; + RuleMatchItem match; + match.category = "general_faq"; + match.score = 0.95f; + match.status_code = 0; + intents.emplace_back(0, 0, match); + ctx.Publish("intent_matches", std::move(intents)); + + Int32Batch chunk_counts; + chunk_counts.emplace_back(0, 0, 3); + ctx.Publish("doc_chunk_counts", std::move(chunk_counts)); + + CompanyDocOutputStruct doc_out{}; + void* out_items[] = {&doc_out}; + ExternalOutputBatchView out_view; + out_view.items = out_items; + out_view.count = 1; + out_view.capacity = 1; + + OutputPortBindings out_bindings({{"raw_request_ids", "raw_request_ids"}, + {"llm_answers", "llm_answers"}, + {"intent_matches", "intent_matches"}, + {"doc_chunk_counts", "doc_chunk_counts"}}); + OutputEncodeOptions out_options; + out_options.converter_id = out_conv->converter_id; + + size_t written = 0; + ret = out_conv->encode_fn(&ctx, out_bindings, out_options, &out_view, + &written, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + EXPECT_EQ(written, 1U); + EXPECT_EQ(doc_out.request_id, 2001U); + EXPECT_STREQ(doc_out.intent_name, "general_faq"); + EXPECT_STREQ(doc_out.answer_text, "This is the answer."); + EXPECT_FLOAT_EQ(doc_out.confidence, 0.95f); + EXPECT_EQ(doc_out.chunk_count, 3); +} + +// ==================== CrossRerank ==================== +TEST_F(ComplexConvertersTest, CrossRerankCAbiInputAndOutput) { + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "rerank.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "rerank_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + const char* passages[] = {"passage zero", "passage one", "passage two"}; + CompanyRerankBatchInputStruct rerank_in{}; + rerank_in.request_id = 3001; + rerank_in.query_text = "what is passage"; + rerank_in.candidate_count = 3; + rerank_in.candidate_passages[0] = passages[0]; + rerank_in.candidate_passages[1] = passages[1]; + rerank_in.candidate_passages[2] = passages[2]; + + const void* in_items[] = {&rerank_in}; + ExternalInputBatchView in_view; + in_view.items = in_items; + in_view.count = 1; + + InputPortBindings in_bindings({{"raw_request_ids", "raw_request_ids"}, + {"rerank_queries", "rerank_queries"}, + {"rerank_candidates", "rerank_candidates"}, + {"rerank_pairs", "rerank_pairs"}}); + InputDecodeOptions in_options; + in_options.converter_id = in_conv->converter_id; + + AlgContext ctx; + AdapterStatus status; + int ret = in_conv->decode_fn(in_view, in_options, in_bindings, &ctx, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + + // Ranked results + RankedTextBatch ranked; + ranked.emplace_back(0, 0, RankedCandidate("passage one", 0.9f, 1, 1)); + ranked.emplace_back(0, 1, RankedCandidate("passage zero", 0.5f, 2, 0)); + ranked.emplace_back(0, 2, RankedCandidate("passage two", 0.1f, 3, 2)); + ctx.Publish("ranked_results", std::move(ranked)); + + CompanyRerankBatchOutputStruct rerank_out{}; + void* out_items[] = {&rerank_out}; + ExternalOutputBatchView out_view; + out_view.items = out_items; + out_view.count = 1; + out_view.capacity = 1; + + OutputPortBindings out_bindings({{"raw_request_ids", "raw_request_ids"}, + {"ranked_results", "ranked_results"}}); + OutputEncodeOptions out_options; + out_options.converter_id = out_conv->converter_id; + + size_t written = 0; + ret = out_conv->encode_fn(&ctx, out_bindings, out_options, &out_view, + &written, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + EXPECT_EQ(written, 1U); + EXPECT_EQ(rerank_out.request_id, 3001U); + EXPECT_EQ(rerank_out.count, 3); + EXPECT_FLOAT_EQ(rerank_out.scores[0], 0.9f); + EXPECT_EQ(rerank_out.sorted_indices[0], 1); + EXPECT_FLOAT_EQ(rerank_out.scores[1], 0.5f); + EXPECT_EQ(rerank_out.sorted_indices[1], 0); +} + +// ==================== ComplianceAudit ==================== +TEST_F(ComplexConvertersTest, ComplianceAuditCAbiInputAndOutput) { + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("audit.plain.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "audit_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + CompanyAuditInputStruct audit_in{4001, "some dialogue text", "channel_vip"}; + const void* in_items[] = {&audit_in}; + ExternalInputBatchView in_view; + in_view.items = in_items; + in_view.count = 1; + + InputPortBindings in_bindings({{"raw_request_ids", "raw_request_ids"}, + {"user_texts", "user_texts"}, + {"channel_names", "channel_names"}}); + InputDecodeOptions in_options; + in_options.converter_id = in_conv->converter_id; + + AlgContext ctx; + AdapterStatus status; + int ret = in_conv->decode_fn(in_view, in_options, in_bindings, &ctx, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + + // Verdict document + StructuredDocumentBatch verdicts; + JsonDocumentItem doc_item; + doc_item.is_valid = true; + doc_item.parse_status = JsonParseStatus::kOk; + doc_item.json_payload = "{\"risk_level\":\"SAFE\",\"risk_score\":0.05}"; + doc_item.structured_data = {{"risk_level", "SAFE"}, {"risk_score", 0.05f}}; + verdicts.emplace_back(0, 0, doc_item); + ctx.Publish("structured_verdicts", std::move(verdicts)); + + RankedTextBatch policies; + policies.emplace_back(0, 0, + RankedCandidate("Article 42.1 Policy", 1.0f, 1, 0)); + ctx.Publish("matched_policies", std::move(policies)); + + CompanyAuditOutputStruct audit_out{}; + void* out_items[] = {&audit_out}; + ExternalOutputBatchView out_view; + out_view.items = out_items; + out_view.count = 1; + out_view.capacity = 1; + + OutputPortBindings out_bindings( + {{"raw_request_ids", "raw_request_ids"}, + {"structured_verdicts", "structured_verdicts"}, + {"matched_policies", "matched_policies"}}); + OutputEncodeOptions out_options; + out_options.converter_id = out_conv->converter_id; + + size_t written = 0; + ret = out_conv->encode_fn(&ctx, out_bindings, out_options, &out_view, + &written, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + EXPECT_EQ(written, 1U); + EXPECT_EQ(audit_out.request_id, 4001U); + EXPECT_FLOAT_EQ(audit_out.risk_score, 0.05f); + EXPECT_STREQ(audit_out.risk_level, "SAFE"); + EXPECT_STREQ(audit_out.matched_policy_clause, "Article 42.1 Policy"); +} + +// ==================== AudioAsrIntent ==================== +TEST_F(ComplexConvertersTest, AudioAsrIntentCAbiInputAndOutput) { + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("audio.pcm.cabi.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "audio_result.plain.cabi.v1"); + ASSERT_NE(out_conv, nullptr); + + std::vector pcm(1600, 0.1f); + CompanyAudioInputStruct audio_in{5001, pcm.data(), + static_cast(pcm.size()), 16000}; + const void* in_items[] = {&audio_in}; + ExternalInputBatchView in_view; + in_view.items = in_items; + in_view.count = 1; + + InputPortBindings in_bindings({{"raw_request_ids", "raw_request_ids"}, + {"audio_inputs", "audio_inputs"}}); + InputDecodeOptions in_options; + in_options.converter_id = in_conv->converter_id; + + AlgContext ctx; + AdapterStatus status; + int ret = in_conv->decode_fn(in_view, in_options, in_bindings, &ctx, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + + TextBatch transcripts; + transcripts.emplace_back(0, 0, "open the front door"); + ctx.Publish("transcripts", std::move(transcripts)); + + RuleMatchBatch slots; + RuleMatchItem m; + m.status_code = 0; + m.match_result_json = + "{\"intent\":\"open_door\",\"slot\":{\"target\":\"front\"}}"; + slots.emplace_back(0, 0, m); + ctx.Publish("intent_slots", std::move(slots)); + + CompanyAudioOutputStruct audio_out{}; + void* out_items[] = {&audio_out}; + ExternalOutputBatchView out_view; + out_view.items = out_items; + out_view.count = 1; + out_view.capacity = 1; + + OutputPortBindings out_bindings({{"raw_request_ids", "raw_request_ids"}, + {"transcripts", "transcripts"}, + {"intent_slots", "intent_slots"}}); + OutputEncodeOptions out_options; + out_options.converter_id = out_conv->converter_id; + + size_t written = 0; + ret = out_conv->encode_fn(&ctx, out_bindings, out_options, &out_view, + &written, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + EXPECT_EQ(written, 1U); + EXPECT_EQ(audio_out.request_id, 5001U); + EXPECT_STREQ(audio_out.transcribed_text, "open the front door"); + EXPECT_STREQ(audio_out.intent_slot_json, + "{\"intent\":\"open_door\",\"slot\":{\"target\":\"front\"}}"); +} + +// ==================== OcrDocQa ==================== +TEST_F(ComplexConvertersTest, OcrDocQaOperatorInputAndOutput) { + const auto* in_conv = IoConverterRegistry::Instance().FindInputConverter( + "image_query.plain.operator.v1"); + ASSERT_NE(in_conv, nullptr); + const auto* out_conv = IoConverterRegistry::Instance().FindOutputConverter( + "invoice_result.plain.operator.v1"); + ASSERT_NE(out_conv, nullptr); + + // Setup operator input: frame and string + std::string uri_str = "/path/to/invoice.jpg"; + CompanyString uri{static_cast(uri_str.size()), + const_cast(uri_str.data())}; + CompanyFrame frame{}; + frame.request_id = 6001; + frame.image_uri = &uri; + + std::string q_str = "Total amount?"; + CompanyString query{static_cast(q_str.size()), + const_cast(q_str.data())}; + + ExternalInputBatchView in_view; + auto frame_holder = std::shared_ptr(&frame, [](void*) {}); + auto query_holder = std::shared_ptr(&query, [](void*) {}); + in_view.slots["frame"].push_back(frame_holder); + in_view.slots["string"].push_back(query_holder); + in_view.count = 1; + + InputPortBindings in_bindings({{"raw_request_ids", "raw_request_ids"}, + {"image_paths", "image_paths"}, + {"user_queries", "user_queries"}}); + InputDecodeOptions in_options; + in_options.converter_id = in_conv->converter_id; + + AlgContext ctx; + AdapterStatus status; + int ret = in_conv->decode_fn(in_view, in_options, in_bindings, &ctx, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + + // Setup invoice output in AlgContext + StructuredDocumentBatch invoices; + JsonDocumentItem doc_item; + doc_item.is_valid = true; + doc_item.parse_status = JsonParseStatus::kOk; + doc_item.json_payload = "{\"total\":123.45}"; + invoices.emplace_back(0, 0, doc_item); + ctx.Publish("extracted_invoice_json", std::move(invoices)); + + OcrDocumentBatch ocr_docs; + OcrDocumentItem ocr_doc; + ocr_doc.boxes.push_back({0, 0, 10, 10, "Invoice", 0.99f}); + ocr_docs.emplace_back(0, 0, ocr_doc); + ctx.Publish("ocr_docs", std::move(ocr_docs)); + + // Destination operator od_out + CompanyOdOutput od_out{}; + std::vector buf(256); + CompanyString res_str{0, buf.data()}; + od_out.result_json = &res_str; + + ExternalOutputBatchView out_view; + out_view.leased_slots["od_out"].push_back(&od_out); + out_view.slot_capacities["od_out"]["result_json"] = 256; + out_view.count = 1; + + OutputPortBindings out_bindings( + {{"raw_request_ids", "raw_request_ids"}, + {"extracted_invoice_json", "extracted_invoice_json"}, + {"ocr_docs", "ocr_docs"}}); + OutputEncodeOptions out_options; + out_options.converter_id = out_conv->converter_id; + + size_t written = 0; + ret = out_conv->encode_fn(&ctx, out_bindings, out_options, &out_view, + &written, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + EXPECT_EQ(written, 1U); + EXPECT_EQ(od_out.request_id, 6001U); + EXPECT_EQ(od_out.detected_box_count, 1); + EXPECT_STREQ(od_out.result_json->data, "{\"total\":123.45}"); +} + +// ==================== All 8 Businesses Bound ==================== +TEST_F(ComplexConvertersTest, AllEightBusinessesRegistered) { + const std::vector expected_biz = { + "translate_v1", + "entity_extract_v1", + "keyword_match_v1", + "smart_doc_qa_v1", + "dense_cross_rerank_scoring", + "dialogue_compliance_audit_v1", + "speech_audio_asr_intent_slot", + "multimodal_ocr_invoice_qa", + }; + + for (const auto& biz : expected_biz) { + const auto* desc = IoBindingRegistry::Instance().FindExposure(biz); + ASSERT_NE(desc, nullptr) << "Missing biz exposure: " << biz; + EXPECT_GE(desc->required_transports.size(), 2U) + << "Biz missing transports: " << biz; + + auto biz_def = PipelineCatalog::FindBiz(biz); + ASSERT_TRUE(biz_def.has_value()) << "Missing biz in catalog: " << biz; + } +} + +} // namespace llm_edgeflow diff --git a/tests/unit/adapter/test_io_binding_registry.cpp b/tests/unit/adapter/test_io_binding_registry.cpp new file mode 100644 index 00000000..2b75ed9f --- /dev/null +++ b/tests/unit/adapter/test_io_binding_registry.cpp @@ -0,0 +1,417 @@ +#include + +#include +#include +#include + +#include "adapter/converter_authoring.h" +#include "adapter/deployment_io_config.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_binding_resolver.h" +#include "adapter/io_converter_registry.h" +#include "adapter/shared_algorithm_runtime.h" +#include "core/pipeline_catalog.h" + +namespace llm_edgeflow { +namespace { + +namespace fs = std::filesystem; + +int DummyDecode(const ExternalInputBatchView&, const InputDecodeOptions&, + const InputPortBindings&, AlgContext*, AdapterStatus*) { + return 0; +} + +int DummyEncode(AlgContext*, const OutputPortBindings&, + const OutputEncodeOptions&, ExternalOutputBatchView*, + size_t* written_count, AdapterStatus*) { + if (written_count) *written_count = 1; + return 0; +} + +} // namespace + +class IoBindingRegistryTest : public ::testing::Test { + protected: + void SetUp() override { + IoConverterRegistry::Instance().ClearForTesting(); + IoBindingRegistry::Instance().ClearForTesting(); + + // 注册基础转换器供测试 + InputConverterDefinition in_def; + in_def.converter_id = "test.in.cabi"; + in_def.transport = "cabi"; + in_def.schema_id = "in_schema"; + in_def.schema_version = 1; + in_def.external_type = "int"; + in_def.external_slots = { + ExternalSlotDefinition("inputs", "int", PortDirection::kInput, true)}; + in_def.max_batch_size = 64; + in_def.logical_ports = { + NodePortDefinition("texts", "TextBatch", true, "1:1")}; + in_def.decode_fn = &DummyDecode; + IoConverterRegistry::Instance().RegisterInputConverter(in_def); + + OutputConverterDefinition out_def; + out_def.converter_id = "test.out.cabi"; + out_def.transport = "cabi"; + out_def.schema_id = "out_schema"; + out_def.schema_version = 1; + out_def.external_type = "int"; + out_def.external_slots = { + ExternalSlotDefinition("answers", "int", PortDirection::kOutput, true)}; + out_def.max_batch_size = 64; + out_def.logical_ports = { + NodePortDefinition("answers", "TextBatch", true, "1:1")}; + out_def.encode_fn = &DummyEncode; + IoConverterRegistry::Instance().RegisterOutputConverter(out_def); + + // 注册业务契约 + BizDefinition biz; + biz.biz_name = "test_biz_v1"; + biz.ingress = {BizPortDefinition("input_sentences", "TextBatch", true)}; + biz.egress = {BizPortDefinition("llm_answers", "TextBatch", true)}; + PipelineCatalog::RegisterBizDefinition(biz); + } + + void TearDown() override { + IoConverterRegistry::Instance().ClearForTesting(); + IoBindingRegistry::Instance().ClearForTesting(); + } +}; + +TEST_F(IoBindingRegistryTest, RegisterAndAuditValidBinding) { + auto& reg = IoBindingRegistry::Instance(); + + IoBindingDefinition binding; + binding.binding_id = "test_biz.cabi.v1"; + binding.biz_name = "test_biz_v1"; + binding.transport = "cabi"; + binding.input_converter_id = "test.in.cabi"; + binding.output_converter_id = "test.out.cabi"; + binding.input_ports = {{"texts", "input_sentences"}}; + binding.output_ports = {{"answers", "llm_answers"}}; + + EXPECT_TRUE(reg.RegisterBinding(binding)); + + BizExposureDefinition exposure; + exposure.biz_name = "test_biz_v1"; + exposure.max_batch_size = 32; + exposure.required_transports = {"cabi"}; + EXPECT_TRUE(reg.RegisterExposure(exposure)); + + std::vector audit_errors; + EXPECT_TRUE(reg.Audit(&audit_errors)); + EXPECT_TRUE(audit_errors.empty()); +} + +TEST_F(IoBindingRegistryTest, AuditRejectsUnregisteredConvertersAndBiz) { + auto& reg = IoBindingRegistry::Instance(); + + // 1. 引用不存在的 biz_name + IoBindingDefinition bad_biz; + bad_biz.binding_id = "bad_biz.binding"; + bad_biz.biz_name = "non_existent_biz"; + bad_biz.transport = "cabi"; + bad_biz.input_converter_id = "test.in.cabi"; + bad_biz.output_converter_id = "test.out.cabi"; + reg.RegisterBinding(bad_biz); + + std::vector errors; + EXPECT_FALSE(reg.Audit(&errors)); + bool found_unregistered_biz = false; + for (const auto& e : errors) { + if (e.find("unregistered biz_name") != std::string::npos) { + found_unregistered_biz = true; + } + } + EXPECT_TRUE(found_unregistered_biz); + + reg.ClearForTesting(); + + // 2. 引用不存在的转换器 + IoBindingDefinition bad_conv; + bad_conv.binding_id = "bad_conv.binding"; + bad_conv.biz_name = "test_biz_v1"; + bad_conv.transport = "cabi"; + bad_conv.input_converter_id = "non_existent_input"; + bad_conv.output_converter_id = "test.out.cabi"; + reg.RegisterBinding(bad_conv); + + errors.clear(); + EXPECT_FALSE(reg.Audit(&errors)); + bool found_unregistered_conv = false; + for (const auto& e : errors) { + if (e.find("unregistered input_converter") != std::string::npos) { + found_unregistered_conv = true; + } + } + EXPECT_TRUE(found_unregistered_conv); +} + +TEST_F(IoBindingRegistryTest, AuditRejectsMissingProductionExposure) { + auto& reg = IoBindingRegistry::Instance(); + + // 暴露要求 operator,但未注册 operator 绑定 + BizExposureDefinition exposure; + exposure.biz_name = "test_biz_v1"; + exposure.required_transports = {"operator"}; + reg.RegisterExposure(exposure); + + std::vector errors; + EXPECT_FALSE(reg.Audit(&errors)); + bool found_missing_exp = false; + for (const auto& e : errors) { + if (e.find("lacks valid binding for required transport: operator") != + std::string::npos) { + found_missing_exp = true; + } + } + EXPECT_TRUE(found_missing_exp); +} + +TEST_F(IoBindingRegistryTest, UnselectedIllegalBindingFailsAudit) { + auto& reg = IoBindingRegistry::Instance(); + + // 1. 注册合法绑定与曝光 + IoBindingDefinition valid_binding; + valid_binding.binding_id = "test_biz.cabi.v1"; + valid_binding.biz_name = "test_biz_v1"; + valid_binding.transport = "cabi"; + valid_binding.input_converter_id = "test.in.cabi"; + valid_binding.output_converter_id = "test.out.cabi"; + valid_binding.input_ports = {{"texts", "input_sentences"}}; + valid_binding.output_ports = {{"answers", "llm_answers"}}; + EXPECT_TRUE(reg.RegisterBinding(valid_binding)); + + BizExposureDefinition exposure; + exposure.biz_name = "test_biz_v1"; + exposure.max_batch_size = 32; + exposure.required_transports = {"cabi"}; + EXPECT_TRUE(reg.RegisterExposure(exposure)); + + // 单独 audit 合法绑定应当通过 + std::vector errors; + EXPECT_TRUE(reg.Audit(&errors)); + EXPECT_TRUE(errors.empty()); + + // 2. 注册未被选择使用的非法绑定 (缺失必需输入映射) + IoBindingDefinition illegal_binding; + illegal_binding.binding_id = "unselected_bad.cabi.v1"; + illegal_binding.biz_name = "test_biz_v1"; + illegal_binding.transport = "cabi"; + illegal_binding.input_converter_id = "test.in.cabi"; + illegal_binding.output_converter_id = "test.out.cabi"; + illegal_binding.input_ports = {}; // 缺失必需 logical port texts + illegal_binding.output_ports = {{"answers", "llm_answers"}}; + EXPECT_TRUE(reg.RegisterBinding(illegal_binding)); + + // 全量审计必须被这个未被选中的非法绑定阻断 + errors.clear(); + EXPECT_FALSE(reg.Audit(&errors)); + bool found_missing_port_mapping = false; + for (const auto& e : errors) { + if (e.find( + "missing required input converter logical port mapping: texts") != + std::string::npos) { + found_missing_port_mapping = true; + } + } + EXPECT_TRUE(found_missing_port_mapping); + + // 验证 SharedAlgorithmRuntime::GlobalInit() 也会因为 Audit 失败而返回冲突错误 + // (-6) + EXPECT_EQ(SharedAlgorithmRuntime::GlobalInit(), + COMPANY_ALG_ERR_REGISTRY_CONFLICT); +} + +TEST_F(IoBindingRegistryTest, DeploymentIoConfigValidation) { + // 1. 合法 schema 1 C ABI 配置 + nlohmann::json valid_cfg = { + {"schema_version", 1}, + {"data", + {{"pipe_path", "test.json"}, {"io_binding", "test_biz.cabi.v1"}}}}; + + // 写入临时测试 pipeline 文件 + std::string tmp_dir = "/tmp/edgeflow_test_config_" + std::to_string(getpid()); + fs::create_directories(tmp_dir); + std::string pipe_path = tmp_dir + "/test.json"; + { + std::ofstream ofs(pipe_path); + ofs << "{}"; + } + + DeploymentIoConfig parsed; + std::string err; + EXPECT_TRUE( + DeploymentIoConfig::Parse(valid_cfg, tmp_dir, "cabi", &parsed, &err)); + EXPECT_EQ(parsed.pipe_path, "test.json"); + EXPECT_EQ(parsed.io_binding, "test_biz.cabi.v1"); + + // 2. 拒绝未知 schema_version + nlohmann::json bad_ver = valid_cfg; + bad_ver["schema_version"] = 2; + EXPECT_FALSE( + DeploymentIoConfig::Parse(bad_ver, tmp_dir, "cabi", &parsed, &err)); + + // 3. 拒绝顶层未知字段 + nlohmann::json bad_field = valid_cfg; + bad_field["extra_field"] = "foo"; + EXPECT_FALSE( + DeploymentIoConfig::Parse(bad_field, tmp_dir, "cabi", &parsed, &err)); + + // 4. C ABI 拒绝 outputs + nlohmann::json cabi_with_outputs = valid_cfg; + cabi_with_outputs["data"]["outputs"] = nlohmann::json::object(); + EXPECT_FALSE(DeploymentIoConfig::Parse(cabi_with_outputs, tmp_dir, "cabi", + &parsed, &err)); + + // 5. 路径逃逸拒绝 + nlohmann::json escape_cfg = valid_cfg; + escape_cfg["data"]["pipe_path"] = "../../../etc/passwd"; + EXPECT_FALSE( + DeploymentIoConfig::Parse(escape_cfg, tmp_dir, "cabi", &parsed, &err)); + + fs::remove_all(tmp_dir); +} + +TEST_F(IoBindingRegistryTest, StrictConfigDirectoryIsolationAndCwdInvariance) { + const std::string root_dir = + "/tmp/edgeflow_test_isolation_" + std::to_string(getpid()); + fs::remove_all(root_dir); + + const fs::path base_dir = fs::path(root_dir) / "service_configs"; + const fs::path outside_dir = fs::path(root_dir) / "outside"; + const fs::path sibling_dir = fs::path(root_dir) / "sibling"; + const fs::path sub_dir = base_dir / "subdir"; + + fs::create_directories(base_dir); + fs::create_directories(outside_dir); + fs::create_directories(sibling_dir); + fs::create_directories(sub_dir); + + // 准备各个目标文件 + { + std::ofstream(base_dir / "pipeline.json") << "{}"; + std::ofstream(sub_dir / "sub_pipeline.json") << "{}"; + std::ofstream(outside_dir / "outside_pipeline.json") << "{}"; + std::ofstream(sibling_dir / "sibling_pipeline.json") << "{}"; + } + + // 创建指向根外文件的符号链接 + std::error_code ec; + fs::create_symlink(outside_dir / "outside_pipeline.json", + base_dir / "symlink_escape.json", ec); + ASSERT_FALSE(ec) << ec.message(); + + DeploymentIoConfig parsed; + std::string err; + + auto make_conf = [](const std::string& pipe) { + nlohmann::json cfg = { + {"schema_version", 1}, + {"data", {{"pipe_path", pipe}, {"io_binding", "test_biz.cabi.v1"}}}}; + return cfg; + }; + + // 1. 同级文件 -> 成功 + EXPECT_TRUE(DeploymentIoConfig::Parse( + make_conf("pipeline.json"), base_dir.string(), "cabi", &parsed, &err)); + EXPECT_EQ(parsed.resolved_pipe_path, + fs::canonical(base_dir / "pipeline.json").string()); + + // 2. 子目录文件 -> 成功 + EXPECT_TRUE(DeploymentIoConfig::Parse(make_conf("subdir/sub_pipeline.json"), + base_dir.string(), "cabi", &parsed, + &err)); + EXPECT_EQ(parsed.resolved_pipe_path, + fs::canonical(sub_dir / "sub_pipeline.json").string()); + + // 3. 父目录逃逸 (../outside/outside_pipeline.json) -> 严格拒绝 + EXPECT_FALSE( + DeploymentIoConfig::Parse(make_conf("../outside/outside_pipeline.json"), + base_dir.string(), "cabi", &parsed, &err)); + EXPECT_NE(err.find("escapes config directory"), std::string::npos); + + // 4. 兄弟目录逃逸 (../sibling/sibling_pipeline.json) -> 严格拒绝 + EXPECT_FALSE( + DeploymentIoConfig::Parse(make_conf("../sibling/sibling_pipeline.json"), + base_dir.string(), "cabi", &parsed, &err)); + EXPECT_NE(err.find("escapes config directory"), std::string::npos); + + // 5. 符号链接逃逸 (位于 base_dir 内但指向根外) -> 严格拒绝 + EXPECT_FALSE(DeploymentIoConfig::Parse(make_conf("symlink_escape.json"), + base_dir.string(), "cabi", &parsed, + &err)); + EXPECT_NE(err.find("escapes config directory"), std::string::npos); + + // 6. 切换工作目录不改变解析结果 (Cwd Invariance) + const fs::path conf_file = base_dir / "deploy.conf"; + { + std::ofstream ofs(conf_file); + ofs << make_conf("pipeline.json").dump(); + } + + const fs::path orig_cwd = fs::current_path(); + // 切换工作目录到 outside_dir + fs::current_path(outside_dir, ec); + ASSERT_FALSE(ec); + + DeploymentIoConfig cwd_parsed; + std::string cwd_err; + bool read_ok = DeploymentIoConfig::ReadFromFile(conf_file.string(), "cabi", + &cwd_parsed, &cwd_err); + + // 恢复原工作目录 + fs::current_path(orig_cwd, ec); + ASSERT_FALSE(ec); + + EXPECT_TRUE(read_ok) << cwd_err; + EXPECT_EQ(cwd_parsed.resolved_pipe_path, + fs::canonical(base_dir / "pipeline.json").string()); + + fs::remove_all(root_dir); +} + +TEST_F(IoBindingRegistryTest, FailClosedAuditRejectsInvalidUnselectedBinding) { + auto& reg = IoBindingRegistry::Instance(); + + // 注册合法暴露与绑定 + IoBindingDefinition valid_binding; + valid_binding.binding_id = "test_biz.cabi.v1"; + valid_binding.biz_name = "test_biz_v1"; + valid_binding.transport = "cabi"; + valid_binding.input_converter_id = "test.in.cabi"; + valid_binding.output_converter_id = "test.out.cabi"; + valid_binding.input_ports = {{"texts", "input_sentences"}}; + valid_binding.output_ports = {{"answers", "llm_answers"}}; + EXPECT_TRUE(reg.RegisterBinding(valid_binding)); + + BizExposureDefinition exposure; + exposure.biz_name = "test_biz_v1"; + exposure.max_batch_size = 32; + exposure.required_transports = {"cabi"}; + EXPECT_TRUE(reg.RegisterExposure(exposure)); + + // 注册一个未被任何曝光引用的非法绑定 (输入端口缺少必需端口) + IoBindingDefinition unselected_bad_binding; + unselected_bad_binding.binding_id = "unselected_bad.cabi.v1"; + unselected_bad_binding.biz_name = "test_biz_v1"; + unselected_bad_binding.transport = "cabi"; + unselected_bad_binding.input_converter_id = "test.in.cabi"; + unselected_bad_binding.output_converter_id = "test.out.cabi"; + // 故意遗漏必需输入映射 texts + unselected_bad_binding.input_ports = {}; + unselected_bad_binding.output_ports = {{"answers", "llm_answers"}}; + EXPECT_TRUE(reg.RegisterBinding(unselected_bad_binding)); + + // 全量 Audit 必须对所有已注册绑定实行 Fail-Closed 检查 + std::vector audit_errors; + EXPECT_FALSE(reg.Audit(&audit_errors)); + EXPECT_FALSE(audit_errors.empty()); + + // GlobalInit 必须失败并返回 -6 (COMPANY_ALG_ERR_REGISTRY_CONFLICT) + EXPECT_EQ(SharedAlgorithmRuntime::GlobalInit(), -6); +} + +} // namespace llm_edgeflow diff --git a/tests/unit/adapter/test_io_converters.cpp b/tests/unit/adapter/test_io_converters.cpp new file mode 100644 index 00000000..ad18cd13 --- /dev/null +++ b/tests/unit/adapter/test_io_converters.cpp @@ -0,0 +1,204 @@ +#include + +#include "adapter/converter_authoring.h" +#include "adapter/io_converter.h" +#include "adapter/io_converter_registry.h" +#include "contracts/inference_payloads.h" + +namespace llm_edgeflow { +namespace { + +int DummyDecode(const ExternalInputBatchView&, const InputDecodeOptions&, + const InputPortBindings&, AlgContext*, AdapterStatus*) { + return 0; +} + +int DummyEncode(AlgContext*, const OutputPortBindings&, + const OutputEncodeOptions&, ExternalOutputBatchView*, + size_t* written_count, AdapterStatus*) { + if (written_count) *written_count = 1; + return 0; +} + +} // namespace + +TEST(IoConverterTest, ViewAccessorsAndPortBindings) { + // 1. ExternalInputBatchView C ABI 与 Slot 访问 + ExternalInputBatchView in_view; + int sample_int = 42; + const void* items[] = {&sample_int}; + in_view.items = items; + in_view.count = 1; + in_view.type_id = "int"; + + EXPECT_EQ(in_view.GetCAbi(0), &sample_int); + EXPECT_EQ(in_view.GetCAbi(1), nullptr); + EXPECT_EQ(in_view.GetCAbi(0), nullptr); + EXPECT_EQ(in_view.At(0), &sample_int); + + auto shared_sample = std::make_shared(100); + in_view.slots["slot_a"] = {shared_sample}; + in_view.slot_types["slot_a"] = "int"; + EXPECT_EQ(in_view.GetSlot("slot_a", 0), shared_sample.get()); + EXPECT_EQ(in_view.GetSlot("slot_a", 0), nullptr); + EXPECT_EQ(in_view.GetSlot("slot_a", 1), nullptr); + EXPECT_EQ(in_view.GetSlot("unknown", 0), nullptr); + EXPECT_EQ(in_view.At(0, "slot_a"), shared_sample.get()); + + // 2. ExternalOutputBatchView 访问 + ExternalOutputBatchView out_view; + int out_sample = 0; + void* out_items[] = {&out_sample}; + out_view.items = out_items; + out_view.count = 1; + out_view.type_id = "int"; + EXPECT_EQ(out_view.GetCAbi(0), &out_sample); + EXPECT_EQ(out_view.GetCAbi(0), nullptr); + + out_view.leased_slots["out_slot"] = {&out_sample}; + out_view.slot_types["out_slot"] = "int"; + out_view.slot_capacities["out_slot"]["field_1"] = 1024; + EXPECT_EQ(out_view.GetSlot("out_slot", 0), &out_sample); + EXPECT_EQ(out_view.GetSlot("out_slot", 0), nullptr); + EXPECT_EQ(out_view.GetSlotCapacity("out_slot", "field_1"), 1024U); + EXPECT_EQ(out_view.GetSlotCapacity("out_slot", "unknown", 42), 42U); + + // 3. PortBindings + InputPortBindings in_bindings({{"texts", "input_sentences"}}); + auto key = in_bindings.Key("texts"); + EXPECT_STREQ(key.name, "input_sentences"); + EXPECT_EQ(in_bindings.GetActualKey("texts"), "input_sentences"); + EXPECT_TRUE(in_bindings.HasKey("texts")); + EXPECT_EQ(in_bindings.GetActualKey("unknown"), ""); + EXPECT_FALSE(in_bindings.HasKey("unknown")); + + OutputPortBindings out_bindings({{"answers", "llm_answers"}}); + auto out_key = out_bindings.Key("answers"); + EXPECT_STREQ(out_key.name, "llm_answers"); + EXPECT_TRUE(out_bindings.HasKey("answers")); + EXPECT_EQ(out_bindings.GetActualKey("unknown"), ""); + EXPECT_FALSE(out_bindings.HasKey("unknown")); +} + +TEST(IoConverterTest, RegisterAndFindInputConverter) { + auto& reg = IoConverterRegistry::Instance(); + + InputConverterDefinition def; + def.converter_id = "test.input.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "test_input"; + def.schema_version = 1; + def.external_type = "int"; + def.external_slots = { + ExternalSlotDefinition("inputs", "int", PortDirection::kInput, true)}; + def.max_batch_size = 64; + def.logical_ports = {NodePortDefinition("texts", "TextBatch", true, "1:1")}; + def.decode_fn = &DummyDecode; + + EXPECT_TRUE(reg.RegisterInputConverter(def)); + + const auto* found = reg.FindInputConverter("test.input.cabi.v1"); + ASSERT_NE(found, nullptr); + EXPECT_EQ(found->converter_id, "test.input.cabi.v1"); + EXPECT_EQ(found->transport, "cabi"); + EXPECT_EQ(found->logical_ports.size(), 1U); + + // 重复注册拒绝并记录冲突 + EXPECT_FALSE(reg.RegisterInputConverter(def)); + EXPECT_TRUE(reg.HasConflict()); +} + +TEST(IoConverterTest, RegisterAndFindOutputConverter) { + auto& reg = IoConverterRegistry::Instance(); + + OutputConverterDefinition def; + def.converter_id = "test.output.cabi.v1"; + def.transport = "cabi"; + def.schema_id = "test_output"; + def.schema_version = 1; + def.external_type = "int"; + def.external_slots = { + ExternalSlotDefinition("answers", "int", PortDirection::kOutput, true)}; + def.max_batch_size = 64; + def.logical_ports = {NodePortDefinition("answers", "TextBatch", true, "1:1")}; + def.encode_fn = &DummyEncode; + + EXPECT_TRUE(reg.RegisterOutputConverter(def)); + + const auto* found = reg.FindOutputConverter("test.output.cabi.v1"); + ASSERT_NE(found, nullptr); + EXPECT_EQ(found->converter_id, "test.output.cabi.v1"); + EXPECT_EQ(found->transport, "cabi"); +} + +TEST(IoConverterTest, RejectsInvalidDefinitions) { + InputConverterDefinition bad_in; + bad_in.converter_id = ""; + bad_in.decode_fn = &DummyDecode; + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + bad_in.converter_id = "bad.in.transport"; + bad_in.transport = "invalid_transport"; + bad_in.schema_id = "test"; + bad_in.schema_version = 1; + bad_in.external_type = "int"; + bad_in.external_slots = { + ExternalSlotDefinition("inputs", "int", PortDirection::kInput, true)}; + bad_in.max_batch_size = 64; + bad_in.logical_ports = { + NodePortDefinition("texts", "TextBatch", true, "1:1")}; + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + bad_in.transport = "cabi"; + bad_in.decode_fn = nullptr; + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + // 缺少 schema_id + bad_in.decode_fn = &DummyDecode; + bad_in.schema_id = ""; + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + // schema_version < 1 + bad_in.schema_id = "test"; + bad_in.schema_version = 0; + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + // 缺少 external_type + bad_in.schema_version = 1; + bad_in.external_type = ""; + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + // 缺少 external_slots + bad_in.external_type = "int"; + bad_in.external_slots.clear(); + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + // 缺少 logical_ports + bad_in.external_slots = { + ExternalSlotDefinition("inputs", "int", PortDirection::kInput, true)}; + bad_in.logical_ports.clear(); + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + // max_batch_size == 0 + bad_in.logical_ports = { + NodePortDefinition("texts", "TextBatch", true, "1:1")}; + bad_in.max_batch_size = 0; + EXPECT_FALSE(IoConverterRegistry::Instance().RegisterInputConverter(bad_in)); + + OutputConverterDefinition bad_out; + bad_out.converter_id = "bad.out"; + bad_out.transport = "cabi"; + bad_out.schema_id = "test"; + bad_out.schema_version = 1; + bad_out.external_type = "int"; + bad_out.external_slots = { + ExternalSlotDefinition("answers", "int", PortDirection::kOutput, true)}; + bad_out.max_batch_size = 64; + bad_out.logical_ports = { + NodePortDefinition("answers", "TextBatch", true, "1:1")}; + bad_out.encode_fn = nullptr; + EXPECT_FALSE( + IoConverterRegistry::Instance().RegisterOutputConverter(bad_out)); +} + +} // namespace llm_edgeflow diff --git a/tests/unit/adapter/test_text_converters.cpp b/tests/unit/adapter/test_text_converters.cpp new file mode 100644 index 00000000..3b0c8c47 --- /dev/null +++ b/tests/unit/adapter/test_text_converters.cpp @@ -0,0 +1,188 @@ +#include + +#include "adapter/adapter_status.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_binding_registry.h" +#include "adapter/io_converter.h" +#include "adapter/io_converter_registry.h" +#include "contracts/inference_payloads.h" +#include "core/alg_context.h" +#include "edgeflow/c_api.h" +#include "edgeflow/operator/types.h" + +namespace llm_edgeflow { + +class TextConvertersTest : public ::testing::Test {}; + +TEST_F(TextConvertersTest, TextPlainCAbiInputDecodeSuccess) { + const auto* conv = + IoConverterRegistry::Instance().FindInputConverter("text.plain.cabi.v1"); + ASSERT_NE(conv, nullptr); + ASSERT_NE(conv->decode_fn, nullptr); + + CompanyEntityInputStruct s1{1001, "Hello world"}; + CompanyEntityInputStruct s2{1002, "Second sentence"}; + const void* items[] = {&s1, &s2}; + + ExternalInputBatchView view; + view.items = items; + view.count = 2; + + InputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}); + InputDecodeOptions options; + options.converter_id = conv->converter_id; + + AlgContext ctx; + AdapterStatus status; + int ret = conv->decode_fn(view, options, bindings, &ctx, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + + const auto* req_ids = ctx.Read>("raw_request_ids"); + ASSERT_NE(req_ids, nullptr); + ASSERT_EQ(req_ids->size(), 2U); + EXPECT_EQ((*req_ids)[0], 1001U); + EXPECT_EQ((*req_ids)[1], 1002U); + + const auto* sentences = ctx.Read("input_sentences"); + ASSERT_NE(sentences, nullptr); + ASSERT_EQ(sentences->size(), 2U); + EXPECT_EQ((*sentences)[0].data, "Hello world"); + EXPECT_EQ((*sentences)[1].data, "Second sentence"); +} + +TEST_F(TextConvertersTest, TranslateJsonInputDecodeValidAndInvalid) { + const auto* conv = IoConverterRegistry::Instance().FindInputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(conv, nullptr); + ASSERT_NE(conv->decode_fn, nullptr); + + // 1. Valid JSON with query field + CompanyEntityInputStruct valid_s{ + 2001, "{\"query\": \"Translate me!\", \"lang\": \"en\"}"}; + const void* valid_items[] = {&valid_s}; + + ExternalInputBatchView valid_view; + valid_view.items = valid_items; + valid_view.count = 1; + + InputPortBindings bindings({{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}); + InputDecodeOptions options; + options.converter_id = conv->converter_id; + + AlgContext ctx; + AdapterStatus status; + int ret = conv->decode_fn(valid_view, options, bindings, &ctx, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + + const auto* sentences = ctx.Read("input_sentences"); + ASSERT_NE(sentences, nullptr); + ASSERT_EQ(sentences->size(), 1U); + EXPECT_EQ((*sentences)[0].data, "Translate me!"); + + // 2. Invalid JSON without query + CompanyEntityInputStruct invalid_s{2002, "{\"text\": \"No query field\"}"}; + const void* invalid_items[] = {&invalid_s}; + ExternalInputBatchView invalid_view; + invalid_view.items = invalid_items; + invalid_view.count = 1; + + AlgContext bad_ctx; + AdapterStatus bad_status; + ret = conv->decode_fn(invalid_view, options, bindings, &bad_ctx, &bad_status); + EXPECT_EQ(ret, COMPANY_ALG_ERR_INVALID_INPUT); + EXPECT_EQ(bad_status.FieldPath(), "json"); +} + +TEST_F(TextConvertersTest, TranslationJsonOutputEncodeCAbi) { + const auto* conv = IoConverterRegistry::Instance().FindOutputConverter( + "translate.json.cabi.v1"); + ASSERT_NE(conv, nullptr); + ASSERT_NE(conv->encode_fn, nullptr); + + AlgContext ctx; + std::vector req_ids = {3001}; + TextBatch answers = {{0, 0, "Bonjour le monde"}}; + ctx.Publish("raw_request_ids", req_ids); + ctx.Publish("llm_answers", answers); + + CompanyEntityOutputStruct out_struct{}; + void* out_ptrs[] = {&out_struct}; + + ExternalOutputBatchView dest; + dest.items = out_ptrs; + dest.capacity = 1; + + OutputPortBindings bindings( + {{"raw_request_ids", "raw_request_ids"}, {"llm_answers", "llm_answers"}}); + OutputEncodeOptions options; + options.converter_id = conv->converter_id; + + size_t written = 0; + AdapterStatus status; + int ret = conv->encode_fn(&ctx, bindings, options, &dest, &written, &status); + EXPECT_EQ(ret, COMPANY_ALG_SUCCESS); + EXPECT_EQ(written, 1U); + EXPECT_EQ(out_struct.request_id, 3001U); + EXPECT_EQ(out_struct.status_code, 0); + + auto parsed = nlohmann::json::parse(out_struct.entities_json); + EXPECT_EQ(parsed["translated"], "Bonjour le monde"); +} + +TEST_F(TextConvertersTest, ProductionBindingsUseDeclaredHostTypes) { + const auto* entity_binding = + IoBindingRegistry::Instance().FindBinding("entity_extract.cabi.v1"); + ASSERT_NE(entity_binding, nullptr); + + const auto* keyword_binding = + IoBindingRegistry::Instance().FindBinding("keyword_match.cabi.v1"); + ASSERT_NE(keyword_binding, nullptr); + + // Entity extract uses CompanyEntityInputStruct via text.plain.cabi.v1 + EXPECT_EQ(entity_binding->input_converter_id, "text.plain.cabi.v1"); + const auto* entity_conv = IoConverterRegistry::Instance().FindInputConverter( + entity_binding->input_converter_id); + ASSERT_NE(entity_conv, nullptr); + EXPECT_EQ(entity_conv->external_type, "CompanyEntityInputStruct"); + + // Keyword match uses CompanyKeywordInputStruct via keyword.plain.cabi.v1 + EXPECT_EQ(keyword_binding->input_converter_id, "keyword.plain.cabi.v1"); + const auto* keyword_conv = IoConverterRegistry::Instance().FindInputConverter( + keyword_binding->input_converter_id); + ASSERT_NE(keyword_conv, nullptr); + EXPECT_EQ(keyword_conv->external_type, "CompanyKeywordInputStruct"); + + // Output converters are distinct + EXPECT_EQ(entity_binding->output_converter_id, "document.structured.cabi.v1"); + EXPECT_EQ(keyword_binding->output_converter_id, "keyword.result.cabi.v1"); +} + +TEST_F(TextConvertersTest, InputConverterReusedAcrossTestBindings) { + // 证明同一个转换器 ID 可以在不同绑定间复用:通过测试专用绑定 + IoBindingDefinition test_reuse_binding; + test_reuse_binding.binding_id = "test_text_reuse.cabi.v1"; + test_reuse_binding.biz_name = "entity_extract_v1"; + test_reuse_binding.transport = "cabi"; + test_reuse_binding.input_converter_id = "text.plain.cabi.v1"; + test_reuse_binding.output_converter_id = "document.structured.cabi.v1"; + test_reuse_binding.input_ports = {{"raw_request_ids", "raw_request_ids"}, + {"input_sentences", "input_sentences"}}; + test_reuse_binding.output_ports = { + {"raw_request_ids", "raw_request_ids"}, + {"extracted_entities", "extracted_entities"}}; + test_reuse_binding.max_batch_size = 64; + + IoBindingRegistry::Instance().RegisterBinding(test_reuse_binding); + + const auto* b1 = + IoBindingRegistry::Instance().FindBinding("entity_extract.cabi.v1"); + const auto* b2 = + IoBindingRegistry::Instance().FindBinding("test_text_reuse.cabi.v1"); + ASSERT_NE(b1, nullptr); + ASSERT_NE(b2, nullptr); + EXPECT_EQ(b1->input_converter_id, b2->input_converter_id); +} + +} // namespace llm_edgeflow diff --git a/tests/unit/core/test_validated_pipeline_plan.cpp b/tests/unit/core/test_validated_pipeline_plan.cpp index 908fae0d..210dd775 100644 --- a/tests/unit/core/test_validated_pipeline_plan.cpp +++ b/tests/unit/core/test_validated_pipeline_plan.cpp @@ -49,6 +49,33 @@ NodeDefinition MakePlanTestNodeDefinition() { REGISTER_NODE_WITH_DEFINITION(PlanTestNode, MakePlanTestNodeDefinition()); +class IoBoundaryTestNode : public INode { + public: + inline static constexpr char kNodeType[] = "IoBoundaryTestNode"; + bool Init(const NodeInitContext&) override { return true; } + int Process(AlgContext*) override { return 0; } + const std::string& Name() const override { + static const std::string name = kNodeType; + return name; + } +}; + +NodeDefinition MakeIoBoundaryTestNodeDefinition() { + NodeDefinition def; + def.node_type = IoBoundaryTestNode::kNodeType; + def.category = "test"; + def.description = "IO Boundary test node"; + def.inputs = {NodePortDefinition{"input_data", "TextBatch", true, "1:1", + "preserve", "request"}}; + def.outputs = {NodePortDefinition{"output_data", "TextBatch", true, "1:1", + "preserve", "request"}}; + def.parallel_safe = true; + return def; +} + +REGISTER_NODE_WITH_DEFINITION(IoBoundaryTestNode, + MakeIoBoundaryTestNodeDefinition()); + class SerializedPlanTestModel : public IModel { public: inline static constexpr char kModelType[] = "serialized_plan_test"; @@ -753,4 +780,121 @@ TEST(ValidatedPipelinePlanTest, MultiModelBindingsAndConcurrencyDeduplication) { EXPECT_EQ(diag->related_nodes, std::vector{"multi_node"}); } +TEST(ValidatedPipelinePlanTest, + IoBoundaryValidationCoversIngressEgressAndExtraWrites) { + // 注册测试用 biz definition + BizDefinition test_biz; + test_biz.biz_name = "io_boundary_test_biz"; + test_biz.ingress = { + BizPortDefinition("text_in", "TextBatch", /*required=*/true), + BizPortDefinition("opt_in", "TextBatch", /*required=*/false)}; + test_biz.egress = { + BizPortDefinition("text_out", "TextBatch", /*required=*/true)}; + PipelineCatalog::RegisterBizDefinition(test_biz); + + nlohmann::json valid_pipeline = { + {"biz_name", "io_boundary_test_biz"}, + {"pipeline", nlohmann::json::array({ + {{"id", "node1"}, + {"node_type", "IoBoundaryTestNode"}, + {"depends_on", nlohmann::json::array()}, + {"ports", + {{"inputs", {{"input_data", "text_in"}}}, + {"outputs", {{"output_data", "text_out"}}}}}}, + })}}; + + // 1. 合法 IO boundary:覆盖必需 ingress,消费 egress + PipelineIoBoundary valid_boundary; + valid_boundary.input_published_ports = { + BizPortDefinition("text_in", "TextBatch", true)}; + valid_boundary.output_consumed_ports = { + BizPortDefinition("text_out", "TextBatch", true)}; + + auto plan_ok = PipelineValidator::ValidateAndPlan( + valid_pipeline, ValidationPolicy::kStrict, &valid_boundary); + EXPECT_TRUE(plan_ok.report.ok); + + // 2. 缺失必需 ingress 端口发布 + PipelineIoBoundary missing_in_boundary; + missing_in_boundary.output_consumed_ports = + valid_boundary.output_consumed_ports; + auto plan_missing_in = PipelineValidator::ValidateAndPlan( + valid_pipeline, ValidationPolicy::kStrict, &missing_in_boundary); + EXPECT_FALSE(plan_missing_in.report.ok); + auto diag_in = + std::find_if(plan_missing_in.report.diagnostics.begin(), + plan_missing_in.report.diagnostics.end(), [](const auto& d) { + return d.code == DiagnosticCode::kMissingInputProducer; + }); + ASSERT_NE(diag_in, plan_missing_in.report.diagnostics.end()); + EXPECT_EQ(diag_in->path, "/io/input"); + + // 3. 缺失输出消费者所需的生产者 + PipelineIoBoundary missing_out_boundary = valid_boundary; + missing_out_boundary.output_consumed_ports.push_back( + BizPortDefinition("unproduced_out", "TextBatch", true)); + auto plan_missing_out = PipelineValidator::ValidateAndPlan( + valid_pipeline, ValidationPolicy::kStrict, &missing_out_boundary); + EXPECT_FALSE(plan_missing_out.report.ok); + auto diag_out = std::find_if( + plan_missing_out.report.diagnostics.begin(), + plan_missing_out.report.diagnostics.end(), [](const auto& d) { + return d.code == DiagnosticCode::kMissingBizOutput; + }); + ASSERT_NE(diag_out, plan_missing_out.report.diagnostics.end()); + EXPECT_EQ(diag_out->path, "/io/output"); + + // 4. 输入额外发布与 Pipeline 内部节点输出冲突 (重复写入) + PipelineIoBoundary conflict_boundary = valid_boundary; + conflict_boundary.input_published_ports.push_back( + BizPortDefinition("text_out", "TextBatch", true)); + auto plan_conflict = PipelineValidator::ValidateAndPlan( + valid_pipeline, ValidationPolicy::kStrict, &conflict_boundary); + EXPECT_FALSE(plan_conflict.report.ok); + auto diag_conflict = + std::find_if(plan_conflict.report.diagnostics.begin(), + plan_conflict.report.diagnostics.end(), [](const auto& d) { + return d.code == DiagnosticCode::kDuplicatePortProducer; + }); + ASSERT_NE(diag_conflict, plan_conflict.report.diagnostics.end()); +} + +TEST(ValidatedPipelinePlanTest, PipelineBuildFromPlanLifecycle) { + nlohmann::json valid_pipeline = { + {"biz_name", "io_boundary_test_biz"}, + {"pipeline", nlohmann::json::array({ + {{"id", "node1"}, + {"node_type", "IoBoundaryTestNode"}, + {"depends_on", nlohmann::json::array()}, + {"ports", + {{"inputs", {{"input_data", "text_in"}}}, + {"outputs", {{"output_data", "text_out"}}}}}}, + })}}; + + PipelineIoBoundary boundary; + boundary.input_published_ports = { + BizPortDefinition("text_in", "TextBatch", true)}; + boundary.output_consumed_ports = { + BizPortDefinition("text_out", "TextBatch", true)}; + + auto plan = std::make_unique( + PipelineValidator::ValidateAndPlan(valid_pipeline, + ValidationPolicy::kStrict, &boundary)); + ASSERT_TRUE(plan->report.ok); + + Pipeline pipeline; + EXPECT_EQ(pipeline.GetState(), Pipeline::State::kEmpty); + + PipelineDiagnostic diag; + bool built = pipeline.BuildFromPlan(std::move(plan), &diag); + EXPECT_TRUE(built); + EXPECT_EQ(pipeline.GetState(), Pipeline::State::kReady); + EXPECT_TRUE(pipeline.IsReady()); + + // 重复构建应被拒绝 + auto plan2 = std::make_unique(); + EXPECT_FALSE(pipeline.BuildFromPlan(std::move(plan2), &diag)); + EXPECT_EQ(diag.code, DiagnosticCode::kInvalidBuildState); +} + } // namespace llm_edgeflow diff --git a/tests/unit/engine/test_engine_fault_tolerance_and_lifecycle.cpp b/tests/unit/engine/test_engine_fault_tolerance_and_lifecycle.cpp index 77a68a8e..36f714bd 100644 --- a/tests/unit/engine/test_engine_fault_tolerance_and_lifecycle.cpp +++ b/tests/unit/engine/test_engine_fault_tolerance_and_lifecycle.cpp @@ -317,7 +317,7 @@ TEST_F(EngineFaultToleranceAndLifecycleTest, LargePayloadRaiiDestruction) { // 4. 全局生命周期高频循环初始化与销毁压测 (30 Cycles) TEST_F(EngineFaultToleranceAndLifecycleTest, RapidGlobalLifecycleInitDeInit) { std::string cfg_path = - GetConfigPath("configs/pipeline_keyword_match_rules.json"); + GetConfigPath("configs/pipeline_keyword_match_cabi.json"); for (int cycle = 0; cycle < 30; ++cycle) { EXPECT_EQ(Alg_Init(), 0); @@ -326,7 +326,6 @@ TEST_F(EngineFaultToleranceAndLifecycleTest, RapidGlobalLifecycleInitDeInit) { param.config_file_path = cfg_path.c_str(); param.model_root_dir = "./models"; param.device_id = 0; - param.biz_type = ALG_BIZ_TYPE_KEYWORD_MATCH; void* handle = nullptr; ASSERT_EQ(Alg_Create(&handle, ¶m), 0); diff --git a/tests/unit/operator/test_operator_biz_bridge_registry.cpp b/tests/unit/operator/test_operator_biz_bridge_registry.cpp deleted file mode 100644 index 4859ecd2..00000000 --- a/tests/unit/operator/test_operator_biz_bridge_registry.cpp +++ /dev/null @@ -1,1108 +0,0 @@ -#include - -#include -#include - -#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" - -namespace llm_edgeflow { -namespace { - -std::vector RegisteredBizTypes() { - std::vector result; - for (const auto& adapter : - BizAdapterRegistry::Instance().GetAdaptersSnapshot()) { - if (adapter) result.push_back(adapter->BizType()); - } - return result; -} - -ResolvedOutputPoolSpec MakeDefaultOutputPoolSpec(const std::string& suffix) { - ResolvedOutputPoolSpec requested; - requested.type = suffix; - ResolvedOutputPoolSpec resolved; - std::string error; - const auto* binding = - OperatorValueTypeRegistry::Instance().GetBindingBySuffix(suffix); - EXPECT_NE(binding, nullptr); - if (!binding) return requested; - EXPECT_TRUE(ResolveOutputPoolSpec(*binding, requested, &resolved, &error)) - << error; - return resolved; -} - -TEST(OperatorBizBridgeRegistryTest, AllRegisteredBusinessesSelfRegistered) { - const auto& reg = OperatorBizBridgeRegistry::Instance(); - const auto biz_types = RegisteredBizTypes(); - ASSERT_FALSE(biz_types.empty()); - for (const auto biz_type : biz_types) { - const auto* desc = reg.GetBridge(biz_type); - ASSERT_NE(desc, nullptr) - << "Business " << static_cast(biz_type) << " must be registered"; - EXPECT_EQ(desc->biz_type, biz_type); - EXPECT_FALSE(desc->adapter_name.empty()); - EXPECT_FALSE(desc->internal_input_type_name.empty()); - EXPECT_FALSE(desc->internal_output_type_name.empty()); - EXPECT_FALSE(desc->registration_identity.empty()); - EXPECT_FALSE(desc->input_slots.empty()); - EXPECT_FALSE(desc->output_slots.empty()); - EXPECT_NE(desc->convert_sample_input, nullptr); - for (const auto& slot : desc->output_slots) { - EXPECT_NE(slot.convert_output, nullptr); - EXPECT_FALSE(slot.key_suffix.empty()); - } - EXPECT_NE(desc->create_shadow_output_dto, nullptr); - } -} - -TEST(OperatorBizBridgeRegistryTest, GlobalInitIsIdempotentAndThreadSafe) { - auto& reg = OperatorBizBridgeRegistry::Instance(); - const auto biz_types = RegisteredBizTypes(); - EXPECT_EQ(reg.GlobalInit(), 0); - EXPECT_EQ(reg.GlobalInit(), 0); - EXPECT_FALSE(reg.HasConflict()); - - std::vector workers; - for (int i = 0; i < 8; ++i) { - workers.emplace_back([&]() { - for (int k = 0; k < 100; ++k) { - for (const auto biz_type : biz_types) { - const auto* desc = reg.GetBridge(biz_type); - EXPECT_NE(desc, nullptr); - } - } - }); - } - for (auto& t : workers) { - t.join(); - } -} - -TEST(OperatorBizBridgeRegistryTest, - IsolatedRegistryRejectsMissingBridgeForRegisteredAdapter) { - OperatorBizBridgeRegistry local_reg; - const auto& global_reg = OperatorBizBridgeRegistry::Instance(); - const auto biz_types = RegisteredBizTypes(); - ASSERT_GT(biz_types.size(), 1u); - - // 故意漏掉一个已注册 Adapter 对应的 Bridge;审计必须从 Adapter 快照发现它。 - for (size_t i = 1; i < biz_types.size(); ++i) { - const auto* desc = global_reg.GetBridge(biz_types[i]); - ASSERT_NE(desc, nullptr); - ASSERT_TRUE(local_reg.RegisterBridge(*desc)); - } - - std::string diagnostic; - EXPECT_EQ(local_reg.GlobalInit(&diagnostic), -6); - EXPECT_NE(diagnostic.find("Missing Operator bridge"), std::string::npos); - EXPECT_NE(diagnostic.find("BizType " + std::to_string(static_cast( - biz_types.front()))), - std::string::npos); - const auto first_diagnostic = diagnostic; - diagnostic.clear(); - EXPECT_EQ(local_reg.GlobalInit(&diagnostic), -6); - EXPECT_EQ(diagnostic, first_diagnostic); - EXPECT_TRUE(local_reg.HasConflict()); -} - -TEST(OperatorBizBridgeRegistryTest, - IsolatedAuditReportsMismatchedDtoTypeAndMissingSlotBinding) { - const auto& global_reg = OperatorBizBridgeRegistry::Instance(); - const auto* original = global_reg.GetBridge(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(original, nullptr); - for (int mutation = 0; mutation < 3; ++mutation) { - OperatorBizBridgeRegistry local_reg; - for (const auto biz_type : RegisteredBizTypes()) { - const auto* registered = global_reg.GetBridge(biz_type); - ASSERT_NE(registered, nullptr); - auto desc = *registered; - if (biz_type == ALG_BIZ_TYPE_DOC_QA) { - if (mutation == 0) desc.internal_input_type_name = "WrongInputDto"; - if (mutation == 1) desc.internal_output_type_name = "WrongOutputDto"; - if (mutation == 2) - desc.input_slots.front().type_suffix = "missing_input_type"; - } - ASSERT_TRUE(local_reg.RegisterBridge(std::move(desc))); - } - std::string diagnostic; - EXPECT_EQ(local_reg.GlobalInit(&diagnostic), -6); - EXPECT_NE(diagnostic.find(original->adapter_name), std::string::npos); - const char* rejected_value = mutation == 0 ? "WrongInputDto" - : mutation == 1 ? "WrongOutputDto" - : "missing_input_type"; - EXPECT_NE(diagnostic.find(rejected_value), std::string::npos) << diagnostic; - const auto& expected = mutation == 0 ? original->internal_input_type_name - : mutation == 1 - ? original->internal_output_type_name - : original->input_slots.front().logical_name; - EXPECT_NE(diagnostic.find(expected), std::string::npos) << diagnostic; - } -} - -TEST(OperatorBizBridgeRegistryTest, - IsolatedRegistryIdempotencyAndLateRegistration) { - OperatorBizBridgeRegistry local_reg; - - // Copy bridges from global instance to local registry - const auto& global_reg = OperatorBizBridgeRegistry::Instance(); - for (const auto biz_type : RegisteredBizTypes()) { - const auto* desc = global_reg.GetBridge(biz_type); - ASSERT_NE(desc, nullptr); - EXPECT_TRUE(local_reg.RegisterBridge(*desc)); - } - - // First GlobalInit succeeds - std::string diagnostic = "stale error"; - EXPECT_EQ(local_reg.GlobalInit(&diagnostic), 0); - EXPECT_TRUE(diagnostic.empty()); - - // Second GlobalInit is idempotent and succeeds - EXPECT_EQ(local_reg.GlobalInit(), 0); - EXPECT_FALSE(local_reg.HasConflict()); - - // Late registration after audited_ returns false but does NOT pollute - // conflict state - OperatorBizBridgeDescriptor dummy_desc; - dummy_desc.biz_type = static_cast(1); - dummy_desc.adapter_name = "DocQA"; - EXPECT_FALSE(local_reg.RegisterBridge(dummy_desc)); - - // GlobalInit continues to succeed idempotently - EXPECT_EQ(local_reg.GlobalInit(), 0); - EXPECT_FALSE(local_reg.HasConflict()); -} - -TEST(OperatorBizBridgeRegistryTest, - IsolatedRegistryRejectsConflictingDescriptor) { - OperatorBizBridgeRegistry local_reg; - - const auto* orig_desc = - OperatorBizBridgeRegistry::Instance().GetBridge(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(orig_desc, nullptr); - EXPECT_TRUE(local_reg.RegisterBridge(*orig_desc)); - - // Re-register identical descriptor with same identity -> succeeds - EXPECT_TRUE(local_reg.RegisterBridge(*orig_desc)); - - // Re-register with different identity or slots -> conflict - OperatorBizBridgeDescriptor conflict_desc = *orig_desc; - conflict_desc.registration_identity = "ConflictingIdentityDocQA"; - EXPECT_FALSE(local_reg.RegisterBridge(conflict_desc)); - EXPECT_TRUE(local_reg.HasConflict()); - std::string diagnostic; - EXPECT_EQ(local_reg.GlobalInit(&diagnostic), -6); - EXPECT_NE(diagnostic.find("ConflictingIdentityDocQA"), std::string::npos); - EXPECT_NE(diagnostic.find(orig_desc->registration_identity), - std::string::npos); - const auto first_diagnostic = diagnostic; - conflict_desc.biz_type = static_cast(99); - conflict_desc.adapter_name = "LaterSlotConflict"; - conflict_desc.input_slots.front().direction = IoDirection::kOutput; - EXPECT_FALSE(local_reg.RegisterBridge(conflict_desc)); - EXPECT_EQ(local_reg.GlobalInit(&diagnostic), -6); - EXPECT_EQ(diagnostic, first_diagnostic); -} - -TEST(OperatorBizBridgeRegistryTest, - IsolatedRegistryRejectsEachIndividualCallbackChange) { - OperatorBizBridgeRegistry local_reg; - - const auto* orig_desc = - OperatorBizBridgeRegistry::Instance().GetBridge(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(orig_desc, nullptr); - EXPECT_TRUE(local_reg.RegisterBridge(*orig_desc)); - - // 1. Only convert_sample_input changed -> conflict - { - OperatorBizBridgeRegistry r; - EXPECT_TRUE(r.RegisterBridge(*orig_desc)); - OperatorBizBridgeDescriptor conflict = *orig_desc; - conflict.convert_sample_input = - [](const std::unordered_map&, - ProcessLocalShadowStorage&, const void**, - std::string*) -> int { return -99; }; - EXPECT_FALSE(r.RegisterBridge(conflict)); - EXPECT_TRUE(r.HasConflict()); - EXPECT_EQ(r.GlobalInit(), -6); - } - - // 2. Only convert_output changed -> conflict - { - OperatorBizBridgeRegistry r; - EXPECT_TRUE(r.RegisterBridge(*orig_desc)); - OperatorBizBridgeDescriptor conflict = *orig_desc; - 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); - } - - // 3. Only create_shadow_output_dto changed -> conflict - { - OperatorBizBridgeRegistry r; - EXPECT_TRUE(r.RegisterBridge(*orig_desc)); - OperatorBizBridgeDescriptor conflict = *orig_desc; - conflict.create_shadow_output_dto = - [](ProcessLocalShadowStorage&) -> void* { return nullptr; }; - EXPECT_FALSE(r.RegisterBridge(conflict)); - EXPECT_TRUE(r.HasConflict()); - EXPECT_EQ(r.GlobalInit(), -6); - } -} - -TEST(OperatorBizBridgeRegistryTest, - SevenBusinesses64SampleDirectDtoConversionMatrix) { - const auto& reg = OperatorBizBridgeRegistry::Instance(); - constexpr size_t kNumSamples = 64; - - // 1. KeywordMatch 64 samples - { - const auto* desc = reg.GetBridge(ALG_BIZ_TYPE_KEYWORD_MATCH); - ASSERT_NE(desc, nullptr); - ProcessLocalShadowStorage storage; - ResolvedOutputPoolSpec spec = MakeDefaultOutputPoolSpec("keyword_out"); - - std::vector raw_strings(kNumSamples); - std::vector c_strings(kNumSamples); - std::vector inputs(kNumSamples); - - for (size_t i = 0; i < kNumSamples; ++i) { - // 前 15 帧穷尽 1~15 字节,后续交替覆盖长字符串。 - if (i < 15) { - raw_strings[i] = std::string(i + 1, 'k'); - } else if (i % 2 == 0) { - raw_strings[i] = "kw_" + std::to_string(i); - } else { - raw_strings[i] = "Long customer complaint and urgent keyword string #" + - std::to_string(i); - } - c_strings[i] = CompanyString{static_cast(raw_strings[i].size()), - raw_strings[i].data()}; - inputs[i].request_id = 1000 + i; - inputs[i].sentence_text = &c_strings[i]; - - std::unordered_map slots = { - {"keyword_in", &inputs[i]}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), - 0); - ASSERT_NE(internal_dto, nullptr); - - const auto* dto = - static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 1000 + i); - EXPECT_STREQ(dto->sentence_text, raw_strings[i].c_str()); - - // Output mirror conversion test - KeywordResult out_dto{}; - out_dto.request_id = dto->request_id; - out_dto.is_hit = (i % 2 == 0) ? 1 : 0; - out_dto.match_result_json = - std::string("{\"match_idx\":") + std::to_string(i) + "}"; - - char out_buf[256] = {0}; - CompanyString out_cs{0, out_buf}; - CompanyOperatorKeywordOutput out_struct{}; - out_struct.match_result_json = &out_cs; - - 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); - EXPECT_STREQ(out_struct.match_result_json->data, - out_dto.match_result_json.c_str()); - EXPECT_EQ( - out_struct.match_result_json->length, - static_cast(std::strlen(out_dto.match_result_json.c_str()))); - } - } - - // 2. EntityExtract 64 samples - { - const auto* desc = reg.GetBridge(ALG_BIZ_TYPE_ENTITY_EXTRACT); - ASSERT_NE(desc, nullptr); - ProcessLocalShadowStorage storage; - ResolvedOutputPoolSpec spec = MakeDefaultOutputPoolSpec("entity_out"); - - for (size_t i = 0; i < kNumSamples; ++i) { - std::string raw_str = (i < 10) ? ("E" + std::to_string(i)) - : ("Entity extraction sentence #" + - std::to_string(i) + " in Beijing"); - CompanyString cs{static_cast(raw_str.size()), raw_str.data()}; - CompanyOperatorEntityInput input{2000 + i, &cs}; - - std::unordered_map slots = { - {"entity_in", &input}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), - 0); - const auto* dto = - static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 2000 + i); - EXPECT_STREQ(dto->sentence_text, raw_str.c_str()); - - EntityResult out_dto{}; - out_dto.request_id = dto->request_id; - out_dto.entities_json = - std::string("[\"Entity_") + std::to_string(i) + "\"]"; - - char out_buf[256] = {0}; - CompanyString out_cs{0, out_buf}; - CompanyOperatorEntityOutput out_struct{}; - out_struct.entities_json = &out_cs; - - 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, - out_dto.entities_json.c_str()); - } - } - - // 3. DocQA 64 samples - { - const auto* desc = reg.GetBridge(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(desc, nullptr); - ProcessLocalShadowStorage storage; - ResolvedOutputPoolSpec spec = MakeDefaultOutputPoolSpec("doc_out"); - - for (size_t i = 0; i < kNumSamples; ++i) { - std::string q_str = "Question #" + std::to_string(i); - std::string d_str = - "Context document text for sample #" + std::to_string(i); - CompanyString q_cs{static_cast(q_str.size()), q_str.data()}; - CompanyString d_cs{static_cast(d_str.size()), d_str.data()}; - CompanyOperatorDocInput input{3000 + i, &d_cs, &q_cs}; - - std::unordered_map slots = {{"doc_in", &input}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), - 0); - const auto* dto = static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 3000 + i); - EXPECT_STREQ(dto->query_text, q_str.c_str()); - EXPECT_STREQ(dto->doc_text, d_str.c_str()); - - DocResult out_dto{}; - out_dto.request_id = dto->request_id; - out_dto.confidence = 0.95f; - out_dto.chunk_count = 4; - out_dto.intent_name = std::string("INTENT_") + std::to_string(i) + ""; - out_dto.answer_text = std::string("Answer_") + std::to_string(i) + ""; - - char intent_buf[64] = {0}; - char answer_buf[1024] = {0}; - CompanyString intent_cs{0, intent_buf}; - CompanyString answer_cs{0, answer_buf}; - CompanyOperatorDocOutput out_struct{}; - out_struct.intent_name = &intent_cs; - out_struct.answer_text = &answer_cs; - - 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); - EXPECT_EQ(out_struct.chunk_count, 4); - EXPECT_STREQ(out_struct.intent_name->data, out_dto.intent_name.c_str()); - EXPECT_STREQ(out_struct.answer_text->data, out_dto.answer_text.c_str()); - } - } - - // 4. ComplianceAudit 64 samples - { - const auto* desc = reg.GetBridge(ALG_BIZ_TYPE_COMPLIANCE_AUDIT); - ASSERT_NE(desc, nullptr); - ProcessLocalShadowStorage storage; - ResolvedOutputPoolSpec spec = MakeDefaultOutputPoolSpec("audit_out"); - - for (size_t i = 0; i < kNumSamples; ++i) { - std::string u_str = "Audit user prompt text #" + std::to_string(i); - std::string c_str = "channel_" + std::to_string(i % 5); - CompanyString u_cs{static_cast(u_str.size()), u_str.data()}; - CompanyString c_cs{static_cast(c_str.size()), c_str.data()}; - CompanyOperatorAuditInput input{4000 + i, &u_cs, &c_cs}; - - std::unordered_map slots = { - {"audit_in", &input}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), - 0); - const auto* dto = - static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 4000 + i); - EXPECT_STREQ(dto->user_text, u_str.c_str()); - EXPECT_STREQ(dto->channel_name, c_str.c_str()); - - AuditResult out_dto{}; - out_dto.request_id = dto->request_id; - out_dto.risk_score = 0.1f * (i % 10); - out_dto.risk_level = "SAFE"; - out_dto.matched_policy_clause = - std::string("Policy Clause #") + std::to_string(i) + ""; - out_dto.audit_verdict_json = - std::string("{\"verdict\":") + std::to_string(i) + "}"; - - char r_buf[32] = {0}, p_buf[256] = {0}, v_buf[1024] = {0}; - CompanyString r_cs{0, r_buf}, p_cs{0, p_buf}, v_cs{0, v_buf}; - CompanyOperatorAuditOutput out_struct{}; - out_struct.risk_level = &r_cs; - out_struct.matched_policy_clause = &p_cs; - out_struct.audit_verdict_json = &v_cs; - - 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"); - EXPECT_STREQ(out_struct.matched_policy_clause->data, - out_dto.matched_policy_clause.c_str()); - EXPECT_STREQ(out_struct.audit_verdict_json->data, - out_dto.audit_verdict_json.c_str()); - } - } - - // 5. AudioAsrIntent 64 samples - { - const auto* desc = reg.GetBridge(ALG_BIZ_TYPE_AUDIO_ASR_INTENT); - ASSERT_NE(desc, nullptr); - ProcessLocalShadowStorage storage; - ResolvedOutputPoolSpec spec = MakeDefaultOutputPoolSpec("audio_out"); - - std::vector> pcm_pool(kNumSamples, - std::vector(16000)); - for (size_t i = 0; i < kNumSamples; ++i) { - pcm_pool[i][0] = 0.001f * static_cast(i); - pcm_pool[i][15999] = 0.002f * static_cast(i) + 0.5f; - CompanyOperatorAudioInput input{5000 + i, pcm_pool[i].data(), 16000, - 16000}; - - std::unordered_map slots = { - {"audio_in", &input}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), - 0); - const auto* dto = - static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 5000 + i); - EXPECT_EQ(dto->pcm_length, 16000); - EXPECT_EQ(dto->sample_rate, 16000); - EXPECT_FLOAT_EQ(dto->pcm_buffer[0], pcm_pool[i][0]); - EXPECT_FLOAT_EQ(dto->pcm_buffer[15999], pcm_pool[i][15999]); - - AudioResult out_dto{}; - out_dto.request_id = dto->request_id; - out_dto.transcribed_text = - std::string("Transcribed speech audio #") + std::to_string(i) + ""; - out_dto.intent_slot_json = - std::string("{\"intent\":\"AUDIO_") + std::to_string(i) + "\"}"; - - char t_buf[512] = {0}, slot_buf[1024] = {0}; - CompanyString t_cs{0, t_buf}, slot_cs{0, slot_buf}; - CompanyOperatorAudioOutput out_struct{}; - out_struct.transcribed_text = &t_cs; - out_struct.intent_slot_json = &slot_cs; - - 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, - out_dto.transcribed_text.c_str()); - EXPECT_STREQ(out_struct.intent_slot_json->data, - out_dto.intent_slot_json.c_str()); - } - } - - // 6. CrossRerank 64 samples with 8 distinct candidates - { - const auto* desc = reg.GetBridge(ALG_BIZ_TYPE_CROSS_RERANK); - ASSERT_NE(desc, nullptr); - ProcessLocalShadowStorage storage; - ResolvedOutputPoolSpec spec = MakeDefaultOutputPoolSpec("rerank_out"); - - for (size_t i = 0; i < kNumSamples; ++i) { - std::string q_str = "Rerank Query #" + std::to_string(i); - CompanyString q_cs{static_cast(q_str.size()), q_str.data()}; - std::vector c_strs(8); - std::vector c_cs(8); - CompanyOperatorRerankInput input{}; - input.request_id = 6000 + i; - input.query_text = &q_cs; - input.candidate_count = 8; - - for (int c = 0; c < 8; ++c) { - c_strs[c] = - "Passage " + std::to_string(c) + " for req " + std::to_string(i); - c_cs[c] = CompanyString{static_cast(c_strs[c].size()), - c_strs[c].data()}; - input.candidate_passages[c] = &c_cs[c]; - } - - std::unordered_map slots = { - {"rerank_in", &input}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), - 0); - const auto* dto = - static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 6000 + i); - EXPECT_STREQ(dto->query_text, q_str.c_str()); - EXPECT_EQ(dto->candidate_count, 8); - for (int c = 0; c < 8; ++c) { - EXPECT_STREQ(dto->candidate_passages[c], c_strs[c].c_str()); - } - - RerankResult out_dto{}; - out_dto.request_id = dto->request_id; - out_dto.count = 8; - for (int c = 0; c < 8; ++c) { - out_dto.scores[c] = 0.1f * static_cast(8 - c); - out_dto.sorted_indices[c] = c; - } - - CompanyOperatorRerankOutput out_struct{}; - 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); - for (int c = 0; c < 8; ++c) { - EXPECT_FLOAT_EQ(out_struct.scores[c], out_dto.scores[c]); - EXPECT_EQ(out_struct.sorted_indices[c], out_dto.sorted_indices[c]); - } - } - } - - // 7. OcrDocQA 64 samples - { - const auto* desc = reg.GetBridge(ALG_BIZ_TYPE_OCR_DOC_QA); - ASSERT_NE(desc, nullptr); - ProcessLocalShadowStorage storage; - ResolvedOutputPoolSpec spec = MakeDefaultOutputPoolSpec("od_out"); - - for (size_t i = 0; i < kNumSamples; ++i) { - std::string uri_str = "data/invoices/inv_" + std::to_string(i) + ".jpg"; - std::string q_str = - "Extract total amount for invoice #" + std::to_string(i); - CompanyString uri_cs{static_cast(uri_str.size()), - uri_str.data()}; - CompanyString q_cs{static_cast(q_str.size()), q_str.data()}; - CompanyFrame frame{7000 + i, &uri_cs, nullptr}; - - std::unordered_map slots = {{"frame", &frame}, - {"string", &q_cs}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), - 0); - const auto* dto = - static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 7000 + i); - EXPECT_STREQ(dto->image_path, uri_str.c_str()); - EXPECT_STREQ(dto->query_prompt, q_str.c_str()); - - OcrDocResult out_dto{}; - out_dto.request_id = dto->request_id; - out_dto.detected_box_count = static_cast(i % 12); - out_dto.extracted_invoice_json = - std::string("{\"inv_id\":") + std::to_string(i) + "}"; - - char res_buf[2048] = {0}; - CompanyString res_cs{0, res_buf}; - CompanyOdOutput out_struct{}; - out_struct.result_json = &res_cs; - - 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); - EXPECT_STREQ(out_struct.result_json->data, - out_dto.extracted_invoice_json.c_str()); - } - } -} - -TEST(OperatorBizBridgeRegistryTest, - IsolatedRegistryRejectsSlotDirectionMismatch) { - OperatorBizBridgeRegistry local_reg; - - const auto* orig_desc = - OperatorBizBridgeRegistry::Instance().GetBridge(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(orig_desc, nullptr); - - OperatorBizBridgeDescriptor bad_desc = *orig_desc; - bad_desc.biz_type = static_cast(99); - bad_desc.input_slots[0].direction = IoDirection::kOutput; // Mismatch - EXPECT_FALSE(local_reg.RegisterBridge(bad_desc)); - EXPECT_TRUE(local_reg.HasConflict()); - std::string diagnostic; - EXPECT_EQ(local_reg.GlobalInit(&diagnostic), -6); - EXPECT_NE(diagnostic.find("input direction"), std::string::npos); - EXPECT_NE(diagnostic.find(bad_desc.input_slots[0].logical_name), - std::string::npos); - EXPECT_NE(diagnostic.find(bad_desc.adapter_name), std::string::npos); - - for (int mutation = 0; mutation < 2; ++mutation) { - OperatorBizBridgeRegistry invalid; - auto descriptor = *orig_desc; - if (mutation == 0) - descriptor.input_slots.front().key_suffix = "output_only"; - if (mutation == 1) - descriptor.input_slots.front().convert_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; - const auto* orig_desc = - OperatorBizBridgeRegistry::Instance().GetBridge(ALG_BIZ_TYPE_DOC_QA); - ASSERT_NE(orig_desc, nullptr); - - OperatorBizBridgeDescriptor descriptor = *orig_desc; - descriptor.output_slots.push_back(descriptor.output_slots.front()); - descriptor.output_slots.back().logical_name = "secondary_output"; - descriptor.output_slots.back().key_suffix = "secondary"; - ASSERT_TRUE(local_reg.RegisterBridge(descriptor)); - EXPECT_FALSE(local_reg.HasConflict()); - ASSERT_NE(local_reg.GetBridge(descriptor.biz_type), nullptr); - EXPECT_EQ(local_reg.GetBridge(descriptor.biz_type)->output_slots.size(), 2u); - - for (int mutation = 0; mutation < 4; ++mutation) { - SCOPED_TRACE(mutation); - OperatorBizBridgeRegistry invalid; - auto bad = descriptor; - if (mutation == 0) bad.output_slots.back().key_suffix.clear(); - if (mutation == 1) - bad.output_slots.back().logical_name = - bad.output_slots.front().logical_name; - if (mutation == 2) bad.output_slots.back().convert_output = nullptr; - if (mutation == 3) bad.output_slots.back().key_suffix = "unreachable.key"; - EXPECT_FALSE(invalid.RegisterBridge(bad)); - EXPECT_TRUE(invalid.HasConflict()); - } - - // Changing only a slot conversion is a conflicting registration too. - auto changed = descriptor; - changed.output_slots.back().convert_output = [](const void*, void*, - const ResolvedOutputPoolSpec&, - std::string*) { return -99; }; - EXPECT_FALSE(local_reg.RegisterBridge(changed)); - EXPECT_TRUE(local_reg.HasConflict()); -} - -TEST(OperatorBizBridgeRegistryTest, ConcurrentReadFreezeInterleavingTSan) { - OperatorBizBridgeRegistry local_reg; - const auto& global_reg = OperatorBizBridgeRegistry::Instance(); - const auto biz_types = RegisteredBizTypes(); - for (const auto biz_type : biz_types) { - const auto* desc = global_reg.GetBridge(biz_type); - ASSERT_NE(desc, nullptr); - EXPECT_TRUE(local_reg.RegisterBridge(*desc)); - } - - std::atomic stop_flag{false}; - std::vector readers; - for (int i = 0; i < 4; ++i) { - readers.emplace_back([&]() { - while (!stop_flag.load()) { - for (const auto biz_type : biz_types) { - const auto* desc = local_reg.GetBridge(biz_type); - EXPECT_NE(desc, nullptr); - } - } - }); - } - - std::thread freezer([&]() { - for (int i = 0; i < 50; ++i) { - EXPECT_EQ(local_reg.GlobalInit(), 0); - OperatorBizBridgeDescriptor late_desc; - late_desc.biz_type = static_cast(99); - EXPECT_FALSE(local_reg.RegisterBridge(late_desc)); - } - }); - - freezer.join(); - stop_flag.store(true); - for (auto& r : readers) { - r.join(); - } - EXPECT_EQ(local_reg.GlobalInit(), 0); - EXPECT_FALSE(local_reg.HasConflict()); -} - -TEST(OperatorBizBridgeRegistryTest, - CrossRerankHandlesNegativeCandidateCountSafely) { - const auto* desc = OperatorBizBridgeRegistry::Instance().GetBridge( - ALG_BIZ_TYPE_CROSS_RERANK); - ASSERT_NE(desc, nullptr); - - ProcessLocalShadowStorage storage; - std::string q_str = "Query"; - CompanyString q_cs{static_cast(q_str.size()), q_str.data()}; - CompanyOperatorRerankInput input{}; - input.request_id = 999; - input.query_text = &q_cs; - input.candidate_count = -5; - - std::unordered_map slots = {{"rerank_in", &input}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), 0); - ASSERT_NE(internal_dto, nullptr); - const auto* dto = - static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 999); - EXPECT_STREQ(dto->query_text, "Query"); - for (int i = 0; i < 8; ++i) { - EXPECT_EQ(dto->candidate_passages[i], nullptr); - } -} - -// RFC-0053: Shared Text Carrier Bridge & Typed Builder contract verification -TEST(OperatorBizBridgeRegistryTest, TextCarrierSharedBridgeAndTypedBuilder) { - for (const auto biz_type : - {ALG_BIZ_TYPE_ENTITY_EXTRACT, ALG_BIZ_TYPE_TRANSLATE}) { - const auto* desc = - OperatorBizBridgeRegistry::Instance().GetBridge(biz_type); - ASSERT_NE(desc, nullptr); - - EXPECT_EQ(desc->internal_input_type_name, "CompanyEntityInputStruct"); - EXPECT_EQ(desc->internal_output_type_name, "EntityResult"); - ASSERT_EQ(desc->input_slots.size(), 1u); - ASSERT_EQ(desc->output_slots.size(), 1u); - EXPECT_EQ(desc->input_slots[0].logical_name, "entity_in"); - EXPECT_EQ(desc->input_slots[0].type_suffix, "entity_in"); - EXPECT_EQ(desc->output_slots[0].logical_name, "entity_out"); - EXPECT_EQ(desc->output_slots[0].type_suffix, "entity_out"); - - ProcessLocalShadowStorage storage; - ResolvedOutputPoolSpec spec = MakeDefaultOutputPoolSpec("entity_out"); - - std::string text = "sample query payload"; - CompanyString cs{static_cast(text.size()), text.data()}; - CompanyOperatorEntityInput in{4242, &cs}; - - std::unordered_map slots = {{"entity_in", &in}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ(desc->convert_sample_input(slots, storage, &internal_dto, &err), - 0); - ASSERT_NE(internal_dto, nullptr); - const auto* in_dto = - static_cast(internal_dto); - EXPECT_EQ(in_dto->request_id, 4242u); - EXPECT_STREQ(in_dto->sentence_text, text.c_str()); - - EntityResult out_dto{4242, "{\"out\":\"ok\"}", 0}; - char out_buf[128] = {0}; - CompanyString out_cs{0, out_buf}; - CompanyOperatorEntityOutput out_struct{}; - out_struct.entities_json = &out_cs; - - 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); - EXPECT_STREQ(out_struct.entities_json->data, "{\"out\":\"ok\"}"); - } -} - -// RFC-0053: Typed builder bridges undergo the same strict registry validation -TEST(OperatorBizBridgeRegistryTest, - TypedBuilderUndergoesFullRegistryValidation) { - OperatorBizBridgeRegistry local_reg; - const auto& global_reg = OperatorBizBridgeRegistry::Instance(); - - for (const auto biz_type : RegisteredBizTypes()) { - if (biz_type == ALG_BIZ_TYPE_ENTITY_EXTRACT) { - auto desc = MakeTextCarrierBridge(ALG_BIZ_TYPE_ENTITY_EXTRACT, - "EntityExtract", "test.typed_carrier"); - desc.input_slots.front().type_suffix = "unknown_invalid_suffix"; - ASSERT_TRUE(local_reg.RegisterBridge(std::move(desc))); - } else { - const auto* registered = global_reg.GetBridge(biz_type); - ASSERT_NE(registered, nullptr); - ASSERT_TRUE(local_reg.RegisterBridge(*registered)); - } - } - - std::string diagnostic; - int ret = local_reg.GlobalInit(&diagnostic); - EXPECT_EQ(ret, -6); - EXPECT_NE(diagnostic.find("unknown_invalid_suffix"), std::string::npos) - << diagnostic; -} - -// 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, - &ConvertTextCarrierOutput>(ALG_BIZ_TYPE_ENTITY_EXTRACT, "EntityExtract", - "CompanyEntityInputStruct", "test.custom_slot", - "custom_in", "custom_out"); - - 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"; - CompanyString cs{static_cast(text.size()), text.data()}; - CompanyOperatorEntityInput in{8888, &cs}; - - // 1. When slots is keyed by custom logical name, it converts successfully - std::unordered_map slots_custom = { - {"custom_in", &in}}; - const void* internal_dto = nullptr; - std::string err; - ASSERT_EQ( - desc.convert_sample_input(slots_custom, storage, &internal_dto, &err), 0); - ASSERT_NE(internal_dto, nullptr); - const auto* dto = static_cast(internal_dto); - EXPECT_EQ(dto->request_id, 8888u); - EXPECT_STREQ(dto->sentence_text, "custom slot query"); - - // 2. When slots is empty or has null payload, it returns -3 - std::unordered_map slots_empty; - EXPECT_EQ( - desc.convert_sample_input(slots_empty, storage, &internal_dto, &err), -3); - - std::unordered_map slots_null = { - {"custom_in", nullptr}}; - EXPECT_EQ(desc.convert_sample_input(slots_null, storage, &internal_dto, &err), - -3); - - // 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 -} // namespace llm_edgeflow diff --git a/tests/unit/operator/test_operator_output_pool.cpp b/tests/unit/operator/test_operator_output_pool.cpp index 2f3c04c6..02b9513c 100644 --- a/tests/unit/operator/test_operator_output_pool.cpp +++ b/tests/unit/operator/test_operator_output_pool.cpp @@ -939,4 +939,180 @@ TEST_F(OperatorOutputPoolTest, EXPECT_TRUE(aligned); } +// -------------------------------------------------------------------------- +// AcquireOperatorOutputBlocks 异常安全与回滚测试 +// -------------------------------------------------------------------------- + +TEST_F(OperatorOutputPoolTest, + TrackRollsBackBlockOnRegistrationAllocationFailure) { + const auto* binding = + OperatorValueTypeRegistry::Instance().GetBindingBySuffix("keyword_out"); + ASSERT_NE(binding, nullptr); + + ResolvedOutputPoolSpec spec; + spec.type = "keyword_out"; + + std::shared_ptr pool; + std::string err; + ASSERT_EQ( + OutputPoolState::Create("keyword_out", 3, spec, binding, &pool, &err), 0); + ASSERT_EQ(pool->FreeBlockCount(), 3u); + ASSERT_EQ(pool->CheckedOutCount(), 0u); + + void* block = nullptr; + ASSERT_EQ(pool->Acquire(&block), 0); + ASSERT_NE(block, nullptr); + EXPECT_EQ(pool->CheckedOutCount(), 1u); + + // 故障注入:未 Reserve 的 ScopedOutputLeaseGuard 在 Track 时 push_back 抛出 + // bad_alloc + ScopedOutputLeaseGuard guard; + bool threw_bad_alloc = false; + try { + test_support::ScopedAllocationFailure failure(0); + guard.Track(pool, block); + } catch (const std::bad_alloc&) { + threw_bad_alloc = true; + } + EXPECT_TRUE(threw_bad_alloc); + + // 验证块已被自动归还,CheckedOut 归零,无泄漏 + EXPECT_EQ(pool->CheckedOutCount(), 0u); + EXPECT_EQ(pool->FreeBlockCount(), 3u); + + // 下一次合法调用能够成功 + guard.Reserve(1); + void* block_next = nullptr; + ASSERT_EQ(pool->Acquire(&block_next), 0); + EXPECT_EQ(pool->CheckedOutCount(), 1u); + EXPECT_NO_THROW(guard.Track(pool, block_next)); + guard.Rollback(); + EXPECT_EQ(pool->CheckedOutCount(), 0u); + EXPECT_EQ(pool->FreeBlockCount(), 3u); +} + +TEST_F(OperatorOutputPoolTest, AcquireBlocksRollsBackOnSecondSlotFailure) { + const auto* binding = + OperatorValueTypeRegistry::Instance().GetBindingBySuffix("keyword_out"); + ASSERT_NE(binding, nullptr); + + ResolvedOutputPoolSpec spec; + spec.type = "keyword_out"; + + std::shared_ptr pool_a; + std::shared_ptr pool_b; + std::string err; + ASSERT_EQ( + OutputPoolState::Create("keyword_out", 2, spec, binding, &pool_a, &err), + 0); + ASSERT_EQ( + OutputPoolState::Create("keyword_out", 1, spec, binding, &pool_b, &err), + 0); + + // 关闭 pool_b,使其 Acquire 立即失败 (返回 -9) 而不发生条件变量无限阻塞 + pool_b->CloseAndDrain(); + + // 构造单帧多槽位: slot_a 和 slot_b + std::vector> frame_bindings(1); + frame_bindings[0].push_back({"slot_a_key", "slot_a", "keyword_out"}); + frame_bindings[0].push_back({"slot_b_key", "slot_b", "keyword_out"}); + + std::unordered_map> pools; + pools["slot_a"] = pool_a; + pools["slot_b"] = pool_b; + + // 执行获取:slot_a 成功,slot_b 失败 + { + ScopedOutputLeaseGuard guard; + std::vector acquired; + std::string acq_err; + int ret = AcquireOperatorOutputBlocks(frame_bindings, pools, &guard, + &acquired, &acq_err); + EXPECT_EQ(ret, -4); + EXPECT_NE(acq_err.find("Output pool exhausted for slot slot_b"), + std::string::npos); + // guard 析构触发 Rollback + } + + // 验证 slot_a 的块已被完全回滚,无泄漏 + EXPECT_EQ(pool_a->CheckedOutCount(), 0u); + EXPECT_EQ(pool_a->FreeBlockCount(), 2u); + + // 为后续合法调用提供正常的 pool_b_new + std::shared_ptr pool_b_new; + ASSERT_EQ(OutputPoolState::Create("keyword_out", 1, spec, binding, + &pool_b_new, &err), + 0); + pools["slot_b"] = pool_b_new; + + // 下一次合法调用能够成功 + { + ScopedOutputLeaseGuard guard; + std::vector acquired; + std::string acq_err; + int ret = AcquireOperatorOutputBlocks(frame_bindings, pools, &guard, + &acquired, &acq_err); + EXPECT_EQ(ret, 0); + EXPECT_EQ(acquired.size(), 2u); + EXPECT_EQ(pool_a->CheckedOutCount(), 1u); + EXPECT_EQ(pool_b_new->CheckedOutCount(), 1u); + } + EXPECT_EQ(pool_a->CheckedOutCount(), 0u); + EXPECT_EQ(pool_b_new->CheckedOutCount(), 0u); +} + +TEST_F(OperatorOutputPoolTest, AcquireBlocksRollsBackOnSecondFrameFailure) { + const auto* binding = + OperatorValueTypeRegistry::Instance().GetBindingBySuffix("keyword_out"); + ASSERT_NE(binding, nullptr); + + ResolvedOutputPoolSpec spec; + spec.type = "keyword_out"; + + std::shared_ptr pool; + std::string err; + ASSERT_EQ( + OutputPoolState::Create("keyword_out", 2, spec, binding, &pool, &err), 0); + ASSERT_EQ(pool->FreeBlockCount(), 2u); + + // 第 0 帧使用有效槽位 keyword_out,第 1 帧使用缺失槽位 missing_slot + std::vector> frame_bindings(2); + frame_bindings[0].push_back({"k0", "keyword_out", "keyword_out"}); + frame_bindings[1].push_back({"k1", "missing_slot", "keyword_out"}); + + std::unordered_map> pools; + pools["keyword_out"] = pool; + + { + ScopedOutputLeaseGuard guard; + std::vector acquired; + std::string acq_err; + int ret = AcquireOperatorOutputBlocks(frame_bindings, pools, &guard, + &acquired, &acq_err); + EXPECT_EQ(ret, -5); + EXPECT_NE(acq_err.find("Missing output pool for slot missing_slot"), + std::string::npos); + } + + // 验证第 0 帧检出的块已被 Rollback 归还,CheckedOut 归零 + EXPECT_EQ(pool->CheckedOutCount(), 0u); + EXPECT_EQ(pool->FreeBlockCount(), 2u); + + // 下一次单帧合法调用必须成功 + { + std::vector> single_frame(1); + single_frame[0].push_back({"k0", "keyword_out", "keyword_out"}); + ScopedOutputLeaseGuard guard; + std::vector acquired; + std::string acq_err; + int ret = AcquireOperatorOutputBlocks(single_frame, pools, &guard, + &acquired, &acq_err); + EXPECT_EQ(ret, 0); + EXPECT_EQ(acquired.size(), 1u); + EXPECT_EQ(pool->CheckedOutCount(), 1u); + } + EXPECT_EQ(pool->CheckedOutCount(), 0u); + EXPECT_EQ(pool->FreeBlockCount(), 2u); +} + } // namespace llm_edgeflow diff --git a/tests/unit/operator/test_operator_value_registry.cpp b/tests/unit/operator/test_operator_value_registry.cpp index 4ebf2f01..14d1a321 100644 --- a/tests/unit/operator/test_operator_value_registry.cpp +++ b/tests/unit/operator/test_operator_value_registry.cpp @@ -5,9 +5,9 @@ #include #include -#include "adapter/biz_adapter_registry.h" +#include "adapter/converter_authoring.h" +#include "adapter/io_converter_registry.h" #include "adapter/operator/json_output_config_reader.h" -#include "adapter/operator/operator_biz_bridge_registry.h" #include "adapter/operator/operator_value_type_registry.h" #include "core/alg_context.h" #include "scoped_allocation_failure.h" @@ -1060,8 +1060,7 @@ TEST(OperatorValueRegistryTest, NoexceptOOMFaultTolerance) { &any, 10, "test", &err); break; case 3: - result = OperatorBizBridgeRegistry::CopyToPooledString( - "source", nullptr, 1, "field", &err); + result = CopyToOperatorString("source", nullptr, 1, "field", &err); break; } injected |= fail.Triggered(); @@ -1126,10 +1125,10 @@ TEST(OperatorValueRegistryTest, TEST(OperatorValueRegistryTest, CAndOperatorAgreeOnChannelNameBoundaries) { const auto* binding = OperatorValueTypeRegistry::Instance().GetBindingBySuffix("audit_in"); - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_COMPLIANCE_AUDIT); + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("audit.plain.cabi.v1"); ASSERT_NE(binding, nullptr); - ASSERT_NE(adapter, nullptr); + ASSERT_NE(in_conv, nullptr); std::string query = "hello"; CompanyString text{static_cast(query.size()), query.data()}; for (int length : {-1, 0, 256, 257}) { @@ -1143,7 +1142,20 @@ TEST(OperatorValueRegistryTest, CAndOperatorAgreeOnChannelNameBoundaries) { const void* inputs[]{&c_input}; AlgContext ctx; const bool expected = length <= 256; - EXPECT_EQ(adapter->Unpack(inputs, 1, &ctx) == 0, expected); + ExternalInputBatchView view; + view.items = inputs; + view.count = 1; + view.type_id = in_conv->external_type; + InputPortBindings port_bindings({{"raw_request_ids", "raw_request_ids"}, + {"user_texts", "user_texts"}, + {"channel_names", "channel_names"}}); + InputDecodeOptions options; + options.converter_id = in_conv->converter_id; + options.transport = "cabi"; + options.max_batch_size = 64; + int dec_ret = + in_conv->decode_fn(view, options, port_bindings, &ctx, nullptr); + EXPECT_EQ(dec_ret == 0, expected); EXPECT_EQ(binding->validate_external(&op_input, {}, nullptr) == 0, expected); } @@ -1152,10 +1164,10 @@ TEST(OperatorValueRegistryTest, CAndOperatorAgreeOnChannelNameBoundaries) { TEST(OperatorValueRegistryTest, CAndOperatorAgreeOnPcmBoundaries) { const auto* binding = OperatorValueTypeRegistry::Instance().GetBindingBySuffix("audio_in"); - auto adapter = - BizAdapterRegistry::Instance().GetAdapter(ALG_BIZ_TYPE_AUDIO_ASR_INTENT); + const auto* in_conv = + IoConverterRegistry::Instance().FindInputConverter("audio.pcm.cabi.v1"); ASSERT_NE(binding, nullptr); - ASSERT_NE(adapter, nullptr); + ASSERT_NE(in_conv, nullptr); std::vector samples(biz_input::kMaxAudioPcmSamples, 0); struct Case { int length; @@ -1185,7 +1197,19 @@ TEST(OperatorValueRegistryTest, CAndOperatorAgreeOnPcmBoundaries) { test.rate}; const void* inputs[]{&c_input}; AlgContext ctx; - EXPECT_EQ(adapter->Unpack(inputs, 1, &ctx) == 0, test.valid); + ExternalInputBatchView view; + view.items = inputs; + view.count = 1; + view.type_id = in_conv->external_type; + InputPortBindings port_bindings({{"raw_request_ids", "raw_request_ids"}, + {"audio_inputs", "audio_inputs"}}); + InputDecodeOptions options; + options.converter_id = in_conv->converter_id; + options.transport = "cabi"; + options.max_batch_size = 64; + int dec_ret = + in_conv->decode_fn(view, options, port_bindings, &ctx, nullptr); + EXPECT_EQ(dec_ret == 0, test.valid); EXPECT_EQ(binding->validate_external(&op_input, {}, nullptr) == 0, test.valid); } diff --git a/tools/pipeline_studio/server.py b/tools/pipeline_studio/server.py index b0cf16c7..ad774637 100755 --- a/tools/pipeline_studio/server.py +++ b/tools/pipeline_studio/server.py @@ -539,7 +539,7 @@ def check_revisions() -> tuple[bytes, bytes]: backup_json = staging / "previous.json" staged_json.write_bytes(encoded) staged_conf_data = copy.deepcopy(conf) - staged_conf_data["data"]["pipe_path"] = str(staged_json.relative_to(PROJECT_ROOT)) + staged_conf_data["data"]["pipe_path"] = staged_json.name staged_conf.write_text(json.dumps(staged_conf_data, ensure_ascii=False, indent=2)) configuration = self.resolve_run_conf(staged_conf, profile) @@ -582,13 +582,13 @@ def profile_inputs(self, pipeline: Any, profile_name: str) -> tuple[dict, Any]: conf = read_json(profile_conf) if ( not isinstance(conf, dict) - or set(conf) != {"data"} - or not isinstance(conf["data"], dict) + or set(conf) not in ({"data"}, {"schema_version", "data"}) + or not isinstance(conf.get("data"), dict) or not isinstance(conf["data"].get("pipe_path"), str) or not isinstance(conf["data"].get("outputs"), dict) ): raise StudioError( - "INVALID_PROFILE_CONFIG", "Profile .conf 必须仅包含 data 对象" + "INVALID_PROFILE_CONFIG", "Profile .conf 必须包含 schema_version 与 data 对象" ) data = conf["data"] original_pipeline_path = Path(data["pipe_path"]) @@ -898,7 +898,7 @@ def preflight( profile_obj, conf_data = self.deployment_candidate( pipeline, profile_name, model_root, filename, conf_name, model_path_actions) - conf_data["data"]["pipe_path"] = str(staged_pipe.relative_to(PROJECT_ROOT)) + conf_data["data"]["pipe_path"] = staged_pipe.name staged_conf.write_text(json.dumps(conf_data, ensure_ascii=False, indent=2)) configuration = self.resolve_run_conf(staged_conf, profile_obj) @@ -1005,7 +1005,7 @@ def _run_job( json.dumps(pipeline, ensure_ascii=False, indent=2), encoding="utf-8" ) temp_conf = copy.deepcopy(conf) - temp_conf["data"]["pipe_path"] = str(pipeline_path.relative_to(PROJECT_ROOT)) + temp_conf["data"]["pipe_path"] = pipeline_path.name conf_path = temp_root / "pipeline.conf" conf_path.write_text(json.dumps(temp_conf, indent=2), encoding="utf-8") configuration = self.resolve_run_conf(conf_path, profile) diff --git a/tools/verify_selection.py b/tools/verify_selection.py index 8e261cfe..7e8d867c 100644 --- a/tools/verify_selection.py +++ b/tools/verify_selection.py @@ -67,15 +67,31 @@ def within(root, relative): return path -def build_run_conf(pipeline, outputs, pipe_path, model_root, bundle_root): +BIZ_TO_OPERATOR_BINDING = { + "keyword_match_v1": "keyword_match.operator.v1", + "entity_extract_v1": "entity_extract.operator.v1", + "smart_doc_qa_v1": "doc_qa.operator.v1", + "dialogue_compliance_audit_v1": "compliance_audit.operator.v1", + "multimodal_ocr_invoice_qa": "ocr_doc_qa.operator.v1", + "speech_audio_asr_intent_slot": "audio_asr.operator.v1", + "dense_cross_rerank_scoring": "cross_rerank.operator.v1", + "text_translation_v1": "translate.operator.v1", +} + + +def build_run_conf(pipeline, outputs, pipe_path, model_root, bundle_root, io_binding=None): """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) - pipeline_path = within(bundle_root, pipe_path).relative_to(bundle_root) + pipeline_path = Path(pipe_path).name 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, "outputs": outputs}} + binding = io_binding or BIZ_TO_OPERATOR_BINDING.get(pipeline.get("biz_name")) + data = {"pipe_path": str(pipeline_path), + "model_paths": model_paths, "outputs": outputs} + if binding: + data["io_binding"] = binding + return {"schema_version": 1, "data": data} def validate_manifest(manifest):