diff --git a/pom.xml b/pom.xml index cb0aeeeeb9cdd6ced2b7b381a75d40fd3a42704f..79cbf92324bb92d3d37eb97d4e710cd9d21cb791 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ 2.3.1 2.3.9 5.8.25 - 1.18.20 + 1.18.40 3.5.7 1.2.83 5.8.25 @@ -85,6 +85,13 @@ ${java.version} ${java.version} ${project.build.sourceEncoding} + + + org.projectlombok + lombok + ${lombok.version} + + diff --git "a/sql/Mysql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" "b/sql/Mysql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" index e9714c3a9bd68b1c7dcdacb680002e8f74813190..b34f531406842c020563d0df6ab5cd313549edf1 100644 --- "a/sql/Mysql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" +++ "b/sql/Mysql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" @@ -1,25 +1,47 @@ /* O2-O8 RAG 完整测试知识路由初始化脚本 + 当前脚本适用于 KnowledgeBase 硬边界改造后的新模型。 + 使用顺序: - 1. 先在管理端一次性上传 document/O2-O8-RAG能力完整测试验收方案.md 第 4.1 节列出的 15 份必传样例。 - 2. 每份文档完成“确认策略方案”和“构建索引执行”,确认 parse_status=3、strategy_status=3、index_status=3。 - 3. 直接执行本脚本。脚本会按 original_file_name 自动选择每个文件最新上传的有效文档记录。 - - 注意: - - 本脚本只初始化测试知识域、知识主题、文档画像和主题文档关联。 - - 本脚本会更新 super_agent_document 的 knowledge_scope_code、knowledge_scope_name、business_category、document_tags。 - - 本脚本使用 INSERT ... ON DUPLICATE KEY UPDATE,可重复执行。 - - 本脚本不会修改文档解析、策略方案、索引状态、chunk、向量库、ES/BM25、KG 或 RAPTOR 数据。 - - 如需重新跑完整验收,优先重新上传/重新解析文档后重跑本脚本;不需要手动填写文档 ID,也不需要删除不同批次文档。 + 1. 先在管理端创建 3 个测试知识库: + - 解析回归知识库 + - 运营制度知识库 + - GraphRAG图谱评测知识库 + 2. 按 document/O2-O8-RAG能力完整测试验收方案.md 第 1 步上传清单上传 15 份必传样例。 + 3. 等每份文档完成解析、策略确认和索引构建,确认 parse_status=3、strategy_status=3、index_status=3。 + 4. 执行本脚本。脚本会自动按知识库名称和 original_file_name 选择最新有效文档记录。 + + 本脚本会创建或更新: + - super_agent_knowledge_scope_node + - super_agent_knowledge_topic_node + - super_agent_topic_document_relation + - super_agent_document_profile + + 本脚本不会: + - 创建知识库。 + - 修改文档所属知识库。 + - 修改文档解析、策略方案、索引状态、chunk、向量库、ES/BM25、KG 或 RAPTOR 数据。 + - 写入旧字段:知识域编码、知识域名称、业务分类、文档标签。 + + 本脚本使用固定高位 ID,可重复执行。如果你的数据库极端情况下已占用 @base_id 这一段, + 可以把 @base_id 改成其他未使用的大整数。 */ +SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci; + START TRANSACTION; /* ========================================================= - 0. 自动按文件名选择最新文档 ID + 0. 固定参数 ========================================================= */ +SET @base_id = 8800041800000000000; + +SET @kb_parse_name = '解析回归知识库'; +SET @kb_operation_name = '运营制度知识库'; +SET @kb_graph_name = 'GraphRAG图谱评测知识库'; + SET @file_o2_provider_pdf = 'O2-provider-artifact验收样例.pdf'; SET @file_o2_ocr_pdf = 'O2-扫描OCR验收样例-图片型PDF.pdf'; SET @file_o2_ocr_png = 'O2-扫描OCR验收样例-文字截图.png'; @@ -38,329 +60,250 @@ SET @file_release_graph_alias = 'O6多社区排序-生产发布回滚别名B.md' SET @file_data_graph_spec = 'O6多社区排序-客户数据访问控制规范A.md'; SET @file_data_graph_alias = 'O6多社区排序-客户数据访问控制别名B.md'; -SET @doc_o2_provider_pdf_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_o2_provider_pdf ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_o2_ocr_pdf_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_o2_ocr_pdf ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_o2_ocr_png_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_o2_ocr_png ORDER BY create_time DESC, id DESC LIMIT 1); - -SET @doc_xinglian_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_xinglian ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_release_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_release ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_incident_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_incident ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_data_policy_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_data_policy ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_travel_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_travel ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_onboarding_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_onboarding ORDER BY create_time DESC, id DESC LIMIT 1); - -SET @doc_audit_evidence_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_audit_evidence ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_audit_alias_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_audit_alias ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_release_graph_spec_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_release_graph_spec ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_release_graph_alias_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_release_graph_alias ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_data_graph_spec_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_data_graph_spec ORDER BY create_time DESC, id DESC LIMIT 1); -SET @doc_data_graph_alias_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_data_graph_alias ORDER BY create_time DESC, id DESC LIMIT 1); - -SELECT id, document_name, original_file_name, parse_status, strategy_status, index_status, last_index_task_id, create_time -FROM super_agent_document -WHERE id IN ( - @doc_o2_provider_pdf_id, - @doc_o2_ocr_pdf_id, - @doc_o2_ocr_png_id, - @doc_xinglian_id, - @doc_release_id, - @doc_incident_id, - @doc_data_policy_id, - @doc_travel_id, - @doc_onboarding_id, - @doc_audit_evidence_id, - @doc_audit_alias_id, - @doc_release_graph_spec_id, - @doc_release_graph_alias_id, - @doc_data_graph_spec_id, - @doc_data_graph_alias_id -) -ORDER BY original_file_name, create_time DESC; - -/* ========================================================= - 1. 固定配置编码 - ========================================================= */ - -SET @scope_parse_code = 'rag_o_parse'; -SET @scope_parse_name = 'O2 解析固定回归'; -SET @scope_operation_code = 'rag_o_operation'; -SET @scope_operation_name = '运营制度与RAG问答评测'; -SET @scope_graph_code = 'rag_o_graph'; -SET @scope_graph_name = 'GraphRAG跨文档图谱评测'; +DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_required_kb; +CREATE TEMPORARY TABLE tmp_o2_o8_required_kb ( + kb_name VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL PRIMARY KEY, + expected_doc_count INT NOT NULL +) ENGINE=InnoDB; -/* - 这些 id 只用于新插入范围、主题、关系、画像时。 - 如果你的数据库里极端情况下已经占用了这些 id,可以把 @base_id 改成其他未使用的大整数。 -*/ -SET @base_id = 8800041700000000000; +INSERT INTO tmp_o2_o8_required_kb (kb_name, expected_doc_count) +VALUES +(@kb_parse_name, 3), +(@kb_operation_name, 6), +(@kb_graph_name, 6); + +DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_expected_document; +CREATE TEMPORARY TABLE tmp_o2_o8_expected_document ( + batch_code VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + kb_name VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + original_file_name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + PRIMARY KEY (kb_name, original_file_name) +) ENGINE=InnoDB; + +INSERT INTO tmp_o2_o8_expected_document (batch_code, kb_name, original_file_name) +VALUES +('A-O2', @kb_parse_name, @file_o2_provider_pdf), +('A-O2', @kb_parse_name, @file_o2_ocr_pdf), +('A-O2', @kb_parse_name, @file_o2_ocr_png), + +('B-业务', @kb_operation_name, @file_xinglian), +('B-业务', @kb_operation_name, @file_release), +('B-业务', @kb_operation_name, @file_incident), +('B-业务', @kb_operation_name, @file_data_policy), +('B-业务', @kb_operation_name, @file_travel), +('B-业务', @kb_operation_name, @file_onboarding), + +('C-O6', @kb_graph_name, @file_audit_evidence), +('C-O6', @kb_graph_name, @file_audit_alias), +('C-O6', @kb_graph_name, @file_release_graph_spec), +('C-O6', @kb_graph_name, @file_release_graph_alias), +('C-O6', @kb_graph_name, @file_data_graph_spec), +('C-O6', @kb_graph_name, @file_data_graph_alias); + +DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_assert_fail; +CREATE TEMPORARY TABLE tmp_o2_o8_assert_fail ( + id INT NOT NULL PRIMARY KEY, + reason VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL +) ENGINE=InnoDB; + +INSERT INTO tmp_o2_o8_assert_fail (id, reason) +VALUES (1, 'sentinel'); /* ========================================================= - 1.1 自动取数和状态保护 + 1. 自动解析知识库和文档 ID ========================================================= */ -SET @missing_doc_ids = CONCAT_WS(',', - IF(@doc_o2_provider_pdf_id IS NULL, @file_o2_provider_pdf, NULL), - IF(@doc_o2_ocr_pdf_id IS NULL, @file_o2_ocr_pdf, NULL), - IF(@doc_o2_ocr_png_id IS NULL, @file_o2_ocr_png, NULL), - IF(@doc_xinglian_id IS NULL, @file_xinglian, NULL), - IF(@doc_release_id IS NULL, @file_release, NULL), - IF(@doc_incident_id IS NULL, @file_incident, NULL), - IF(@doc_data_policy_id IS NULL, @file_data_policy, NULL), - IF(@doc_travel_id IS NULL, @file_travel, NULL), - IF(@doc_onboarding_id IS NULL, @file_onboarding, NULL), - IF(@doc_audit_evidence_id IS NULL, @file_audit_evidence, NULL), - IF(@doc_audit_alias_id IS NULL, @file_audit_alias, NULL), - IF(@doc_release_graph_spec_id IS NULL, @file_release_graph_spec, NULL), - IF(@doc_release_graph_alias_id IS NULL, @file_release_graph_alias, NULL), - IF(@doc_data_graph_spec_id IS NULL, @file_data_graph_spec, NULL), - IF(@doc_data_graph_alias_id IS NULL, @file_data_graph_alias, NULL) +DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_kb; +CREATE TEMPORARY TABLE tmp_o2_o8_kb AS +SELECT + required.kb_name, + required.expected_doc_count, + ( + SELECT kb.id + FROM super_agent_knowledge_base kb + WHERE kb.status = 1 + AND kb.base_name = required.kb_name + ORDER BY kb.id DESC + LIMIT 1 + ) AS knowledge_base_id +FROM tmp_o2_o8_required_kb required; + +SET @missing_kbs = ( + SELECT GROUP_CONCAT(kb_name ORDER BY kb_name SEPARATOR ', ') + FROM tmp_o2_o8_kb + WHERE knowledge_base_id IS NULL ); -SET @missing_doc_ids_error = IF( - @missing_doc_ids IS NULL OR @missing_doc_ids = '', - NULL, - CONCAT('这些文件没有找到 status=1 的最新上传记录,请先上传后再执行脚本: ', @missing_doc_ids) +SELECT + CASE + WHEN @missing_kbs IS NULL OR @missing_kbs = '' THEN 'OK: 3 个测试知识库均已找到' + ELSE CONCAT('ERROR: 以下知识库不存在或未启用,请先创建后再执行脚本: ', @missing_kbs) + END AS knowledge_base_check; + +INSERT INTO tmp_o2_o8_assert_fail (id, reason) +SELECT 1, 'missing enabled knowledge base' +WHERE @missing_kbs IS NOT NULL AND @missing_kbs <> ''; + +DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_latest_document; +CREATE TEMPORARY TABLE tmp_o2_o8_latest_document AS +SELECT + expected.batch_code, + expected.kb_name, + kb.knowledge_base_id, + expected.original_file_name, + ( + SELECT d.id + FROM super_agent_document d + WHERE d.status = 1 + AND d.knowledge_base_id = kb.knowledge_base_id + AND d.original_file_name = expected.original_file_name + ORDER BY d.create_time DESC, d.id DESC + LIMIT 1 + ) AS document_id +FROM tmp_o2_o8_expected_document expected +JOIN tmp_o2_o8_kb kb ON kb.kb_name = expected.kb_name; + +SET @missing_docs = ( + SELECT GROUP_CONCAT(CONCAT(kb_name, '/', original_file_name) ORDER BY kb_name, original_file_name SEPARATOR '; ') + FROM tmp_o2_o8_latest_document + WHERE document_id IS NULL ); SELECT CASE - WHEN @missing_doc_ids_error IS NULL THEN 'OK: 已自动找到 15 个文档 ID' - ELSE @missing_doc_ids_error - END AS parameter_check; - -SET @missing_doc_ids_sql = IF( - @missing_doc_ids_error IS NULL, - 'SELECT 1', - CONCAT('SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''', @missing_doc_ids_error, '''') -); -PREPARE missing_doc_ids_stmt FROM @missing_doc_ids_sql; -EXECUTE missing_doc_ids_stmt; -DEALLOCATE PREPARE missing_doc_ids_stmt; + WHEN @missing_docs IS NULL OR @missing_docs = '' THEN 'OK: 已按知识库和文件名找到 15 份最新有效文档' + ELSE CONCAT('ERROR: 以下文档未在预期知识库下找到 status=1 记录: ', @missing_docs) + END AS document_id_check; + +INSERT INTO tmp_o2_o8_assert_fail (id, reason) +SELECT 1, 'missing expected document' +WHERE @missing_docs IS NOT NULL AND @missing_docs <> ''; SET @not_ready_docs = ( SELECT GROUP_CONCAT(CONCAT( - original_file_name, - '(parse=', IFNULL(CAST(parse_status AS CHAR), 'NULL'), - ', strategy=', IFNULL(CAST(strategy_status AS CHAR), 'NULL'), - ', index=', IFNULL(CAST(index_status AS CHAR), 'NULL'), + latest.kb_name, '/', + d.original_file_name, + '(parse=', IFNULL(CAST(d.parse_status AS CHAR), 'NULL'), + ', strategy=', IFNULL(CAST(d.strategy_status AS CHAR), 'NULL'), + ', index=', IFNULL(CAST(d.index_status AS CHAR), 'NULL'), + ', task=', IFNULL(CAST(d.last_index_task_id AS CHAR), 'NULL'), ')' - ) ORDER BY original_file_name SEPARATOR '; ') - FROM super_agent_document - WHERE id IN ( - @doc_o2_provider_pdf_id, - @doc_o2_ocr_pdf_id, - @doc_o2_ocr_png_id, - @doc_xinglian_id, - @doc_release_id, - @doc_incident_id, - @doc_data_policy_id, - @doc_travel_id, - @doc_onboarding_id, - @doc_audit_evidence_id, - @doc_audit_alias_id, - @doc_release_graph_spec_id, - @doc_release_graph_alias_id, - @doc_data_graph_spec_id, - @doc_data_graph_alias_id - ) - AND (IFNULL(parse_status, -1) <> 3 OR IFNULL(strategy_status, -1) <> 3 OR IFNULL(index_status, -1) <> 3) -); - -SET @not_ready_docs_error = IF( - @not_ready_docs IS NULL OR @not_ready_docs = '', - NULL, - CONCAT('以下最新文档还没有完成 parse_status=3、strategy_status=3、index_status=3,请等待完成后重跑: ', @not_ready_docs) + ) ORDER BY latest.kb_name, d.original_file_name SEPARATOR '; ') + FROM tmp_o2_o8_latest_document latest + JOIN super_agent_document d ON d.id = latest.document_id + WHERE IFNULL(d.parse_status, -1) <> 3 + OR IFNULL(d.strategy_status, -1) <> 3 + OR IFNULL(d.index_status, -1) <> 3 + OR d.last_index_task_id IS NULL ); SELECT CASE - WHEN @not_ready_docs_error IS NULL THEN 'OK: 15 个最新文档均已完成解析、策略确认和索引构建' - ELSE @not_ready_docs_error + WHEN @not_ready_docs IS NULL OR @not_ready_docs = '' THEN 'OK: 15 份文档均已完成解析、策略确认和索引构建' + ELSE CONCAT('ERROR: 以下文档未完成 parse=3、strategy=3、index=3 或缺 last_index_task_id: ', @not_ready_docs) END AS document_status_check; -SET @not_ready_docs_sql = IF( - @not_ready_docs_error IS NULL, - 'SELECT 1', - CONCAT('SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''', @not_ready_docs_error, '''') -); -PREPARE not_ready_docs_stmt FROM @not_ready_docs_sql; -EXECUTE not_ready_docs_stmt; -DEALLOCATE PREPARE not_ready_docs_stmt; +INSERT INTO tmp_o2_o8_assert_fail (id, reason) +SELECT 1, 'document not ready' +WHERE @not_ready_docs IS NOT NULL AND @not_ready_docs <> ''; + +SET @kb_parse_id = (SELECT knowledge_base_id FROM tmp_o2_o8_kb WHERE kb_name = @kb_parse_name); +SET @kb_operation_id = (SELECT knowledge_base_id FROM tmp_o2_o8_kb WHERE kb_name = @kb_operation_name); +SET @kb_graph_id = (SELECT knowledge_base_id FROM tmp_o2_o8_kb WHERE kb_name = @kb_graph_name); + +SET @doc_o2_provider_pdf_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_o2_provider_pdf); +SET @doc_o2_ocr_pdf_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_o2_ocr_pdf); +SET @doc_o2_ocr_png_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_o2_ocr_png); + +SET @doc_xinglian_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_xinglian); +SET @doc_release_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_release); +SET @doc_incident_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_incident); +SET @doc_data_policy_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_data_policy); +SET @doc_travel_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_travel); +SET @doc_onboarding_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_onboarding); + +SET @doc_audit_evidence_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_audit_evidence); +SET @doc_audit_alias_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_audit_alias); +SET @doc_release_graph_spec_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_release_graph_spec); +SET @doc_release_graph_alias_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_release_graph_alias); +SET @doc_data_graph_spec_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_data_graph_spec); +SET @doc_data_graph_alias_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_data_graph_alias); + +SELECT + latest.batch_code, + latest.kb_name, + latest.knowledge_base_id, + d.id AS document_id, + d.document_name, + d.original_file_name, + d.parse_status, + d.strategy_status, + d.index_status, + d.last_index_task_id +FROM tmp_o2_o8_latest_document latest +JOIN super_agent_document d ON d.id = latest.document_id +ORDER BY latest.kb_name, latest.original_file_name; /* ========================================================= - 2. 更新 15 份文档主表元数据 + 2. 固定 ID ========================================================= */ -UPDATE super_agent_document -SET document_name = 'O2-provider-artifact验收样例', - knowledge_scope_code = @scope_parse_code, - knowledge_scope_name = @scope_parse_name, - business_category = 'O2解析固定样例', - document_tags = 'O2,Document Mind,layout,表格,FIGURE,bbox,artifact,解析回归', - edit_time = NOW() -WHERE id = @doc_o2_provider_pdf_id AND status = 1; - -UPDATE super_agent_document -SET document_name = 'O2-扫描OCR验收样例-图片型PDF', - knowledge_scope_code = @scope_parse_code, - knowledge_scope_name = @scope_parse_name, - business_category = 'O2解析固定样例', - document_tags = 'O2,OCR,图片型PDF,PAGE_IMAGE,TABLE_IMAGE,bbox,解析回归', - edit_time = NOW() -WHERE id = @doc_o2_ocr_pdf_id AND status = 1; - -UPDATE super_agent_document -SET document_name = 'O2-扫描OCR验收样例-文字截图', - knowledge_scope_code = @scope_parse_code, - knowledge_scope_name = @scope_parse_name, - business_category = 'O2解析固定样例', - document_tags = 'O2,OCR,PNG,图片文本,蓝桥订单,RAG-O2-20260630,支付回调延迟', - edit_time = NOW() -WHERE id = @doc_o2_ocr_png_id AND status = 1; - -UPDATE super_agent_document -SET document_name = '星联智服全渠道客服平台上线与运营管理手册', - knowledge_scope_code = @scope_operation_code, - knowledge_scope_name = @scope_operation_name, - business_category = '客服平台运营手册', - document_tags = '星联智服,客服平台,上线运营,知识治理,机器人策略,灰度验证,上线观察,故障处理,RAG,O8基线', - edit_time = NOW() -WHERE id = @doc_xinglian_id AND status = 1; - -UPDATE super_agent_document -SET document_name = '生产环境发布与回滚操作规范', - knowledge_scope_code = @scope_operation_code, - knowledge_scope_name = @scope_operation_name, - business_category = '生产发布规范', - document_tags = '生产发布,回滚,灰度节奏,发布暂停,NovaRAG,召回成功率,强制回滚,O8路由,O7跨文档总结', - edit_time = NOW() -WHERE id = @doc_release_id AND status = 1; - -UPDATE super_agent_document -SET document_name = '核心业务系统故障应急响应预案', - knowledge_scope_code = @scope_operation_code, - knowledge_scope_name = @scope_operation_name, - business_category = '故障应急预案', - document_tags = '故障应急,NovaRAG,检索服务降级,P1,P2,人工转接,多文档路由,O3 rerank', - edit_time = NOW() -WHERE id = @doc_incident_id AND status = 1; - -UPDATE super_agent_document -SET document_name = '客户数据分级与访问控制管理制度', - knowledge_scope_code = @scope_operation_code, - knowledge_scope_name = @scope_operation_name, - business_category = '数据访问制度', - document_tags = '客户数据,L4高敏感,访问控制,审批,日志保存,DataCleanRoom,表格问答,citation', - edit_time = NOW() -WHERE id = @doc_data_policy_id AND status = 1; - -UPDATE super_agent_document -SET document_name = '差旅与费用报销管理办法', - knowledge_scope_code = @scope_operation_code, - knowledge_scope_name = @scope_operation_name, - business_category = '费用报销制度', - document_tags = '差旅,费用报销,住宿标准,审批金额阈值,表格问答', - edit_time = NOW() -WHERE id = @doc_travel_id AND status = 1; - -UPDATE super_agent_document -SET document_name = '澄星智能新员工入职培训手册', - knowledge_scope_code = @scope_operation_code, - knowledge_scope_name = @scope_operation_name, - business_category = '入职培训手册', - document_tags = '入职培训,首周日程,30天,60天,90天,表格问答', - edit_time = NOW() -WHERE id = @doc_onboarding_id AND status = 1; - -UPDATE super_agent_document -SET document_name = 'O6跨文档图谱-审计证据规范A', - knowledge_scope_code = @scope_graph_code, - knowledge_scope_name = @scope_graph_name, - business_category = 'O6 GraphRAG样例', - document_tags = 'O6,GraphRAG,AuditTrail,审计系统,权限记录,跨文档canonical,关系证据', - edit_time = NOW() -WHERE id = @doc_audit_evidence_id AND status = 1; - -UPDATE super_agent_document -SET document_name = 'O6跨文档图谱-审计系统别名说明B', - knowledge_scope_code = @scope_graph_code, - knowledge_scope_name = @scope_graph_name, - business_category = 'O6 GraphRAG样例', - document_tags = 'O6,GraphRAG,AuditTrail,审计系统,别名,系统职责,负边界', - edit_time = NOW() -WHERE id = @doc_audit_alias_id AND status = 1; - -UPDATE super_agent_document -SET document_name = 'O6多社区排序-生产发布回滚规范A', - knowledge_scope_code = @scope_graph_code, - knowledge_scope_name = @scope_graph_name, - business_category = 'O6 GraphRAG样例', - document_tags = 'O6,GraphRAG,ReleaseControl,CAB,值班SRE,生产发布,回滚演练,community', - edit_time = NOW() -WHERE id = @doc_release_graph_spec_id AND status = 1; - -UPDATE super_agent_document -SET document_name = 'O6多社区排序-生产发布回滚别名B', - knowledge_scope_code = @scope_graph_code, - knowledge_scope_name = @scope_graph_name, - business_category = 'O6 GraphRAG样例', - document_tags = 'O6,GraphRAG,ReleaseControl,生产发布控制台,变更评审委员会,别名,community', - edit_time = NOW() -WHERE id = @doc_release_graph_alias_id AND status = 1; - -UPDATE super_agent_document -SET document_name = 'O6多社区排序-客户数据访问控制规范A', - knowledge_scope_code = @scope_graph_code, - knowledge_scope_name = @scope_graph_name, - business_category = 'O6 GraphRAG样例', - document_tags = 'O6,GraphRAG,DataAccessGuard,客户数据访问控制,数据治理负责人,信息安全部,community', - edit_time = NOW() -WHERE id = @doc_data_graph_spec_id AND status = 1; - -UPDATE super_agent_document -SET document_name = 'O6多社区排序-客户数据访问控制别名B', - knowledge_scope_code = @scope_graph_code, - knowledge_scope_name = @scope_graph_name, - business_category = 'O6 GraphRAG样例', - document_tags = 'O6,GraphRAG,DataAccessGuard,客户数据Owner,安全复核组,别名,负边界', - edit_time = NOW() -WHERE id = @doc_data_graph_alias_id AND status = 1; +SET @scope_parse_id = @base_id + 1; +SET @scope_operation_id = @base_id + 2; +SET @scope_graph_id = @base_id + 3; + +SET @topic_o2_docmind_id = @base_id + 101; +SET @topic_o2_ocr_id = @base_id + 102; +SET @topic_o2_table_bbox_id = @base_id + 103; + +SET @topic_operation_xinglian_id = @base_id + 201; +SET @topic_operation_release_id = @base_id + 202; +SET @topic_operation_incident_id = @base_id + 203; +SET @topic_operation_data_id = @base_id + 204; +SET @topic_operation_travel_id = @base_id + 205; +SET @topic_operation_onboarding_id = @base_id + 206; +SET @topic_operation_raptor_id = @base_id + 207; + +SET @topic_graph_audit_id = @base_id + 301; +SET @topic_graph_release_id = @base_id + 302; +SET @topic_graph_data_id = @base_id + 303; +SET @topic_graph_boundary_id = @base_id + 304; /* ========================================================= 3. 知识范围配置 ========================================================= */ INSERT INTO super_agent_knowledge_scope_node ( - id, scope_code, scope_name, parent_scope_code, description, aliases, examples, sort_order, + id, knowledge_base_id, scope_name, parent_scope_id, description, aliases, examples, sort_order, create_time, edit_time, status ) VALUES ( - @base_id + 1, - @scope_parse_code, - @scope_parse_name, + @scope_parse_id, + @kb_parse_id, + 'O2 解析固定回归', NULL, - '用于承接 O2 固定解析回归样例,只验证 OCR、layout、reading order、表格、bbox、artifact 和 O9 文档侧观测,不作为生产业务问答知识域。', + '用于承接 O2 固定解析回归样例,只验证 OCR、layout、reading order、表格、bbox、artifact 和 O9 文档侧观测。', 'O2解析,OCR回归,Document Mind回归,解析固定样例,文档侧观测', '["O2 扫描 OCR 样例里有没有识别到关键短语","O2 固定样例中的表格有几行几列","这份 PDF 样例是否包含图示或图片区域"]', 10, NOW(), NOW(), 1 ), ( - @base_id + 2, - @scope_operation_code, - @scope_operation_name, + @scope_operation_id, + @kb_operation_id, + '运营制度与RAG问答评测', NULL, - '用于承接运营制度、客服平台上线、发布回滚、故障应急、数据访问、差旅报销和入职培训类问答,重点验收 O3/O4/O7/O8。', + '用于承接运营制度、客服平台上线、生产发布、故障应急、数据访问、差旅报销和入职培训类问答,重点验收 O3/O4/O7/O8。', '运营制度,RAG问答评测,客服平台,生产发布,故障应急,客户数据,差旅报销,入职培训', '["检索命中率突然下降的可能原因都有哪些","NovaRAG 检索服务降级时按什么顺序处理","L4 高敏感信息的审批要求是什么"]', 20, NOW(), NOW(), 1 ), ( - @base_id + 3, - @scope_graph_code, - @scope_graph_name, + @scope_graph_id, + @kb_graph_id, + 'GraphRAG跨文档图谱评测', NULL, '用于承接 O6 GraphRAG 跨文档别名、canonical、实体关系、community、多社区排序和负边界测试。', 'O6图谱,GraphRAG,跨文档图谱,AuditTrail,ReleaseControl,DataAccessGuard,多社区排序', @@ -369,8 +312,9 @@ VALUES NOW(), NOW(), 1 ) ON DUPLICATE KEY UPDATE + knowledge_base_id = VALUES(knowledge_base_id), scope_name = VALUES(scope_name), - parent_scope_code = VALUES(parent_scope_code), + parent_scope_id = VALUES(parent_scope_id), description = VALUES(description), aliases = VALUES(aliases), examples = VALUES(examples), @@ -380,35 +324,34 @@ ON DUPLICATE KEY UPDATE /* ========================================================= 4. 知识主题配置 - answer_shape 固定使用 explain/list/steps/compare/structure - execution_preference 固定使用 retrieval/graph_assist/graph_then_evidence ========================================================= */ INSERT INTO super_agent_knowledge_topic_node ( - id, topic_code, topic_name, scope_code, description, aliases, examples, + id, knowledge_base_id, topic_name, scope_id, description, aliases, examples, answer_shape, execution_preference, sort_order, create_time, edit_time, status ) VALUES -(@base_id + 101, 'o2_parse_artifact', 'O2 Document Mind 解析产物', @scope_parse_code, '验证普通 PDF 的 layout、表格、FIGURE block、artifact、bbox 和 RAG 产物联动。', 'Document Mind,layout,artifact,bbox,FIGURE,表格解析', '["O2 固定样例中的表格能否被识别成结构化表格","这份 PDF 样例是否包含图示或图片区域"]', 'structure', 'retrieval', 10, NOW(), NOW(), 1), -(@base_id + 102, 'o2_ocr_pdf', 'O2 图片型 PDF OCR', @scope_parse_code, '验证图片型 PDF 的 OCR 文本、页面图片、表格图片、bbox overlay 和关键业务短语。', '图片型PDF,OCR,PAGE_IMAGE,TABLE_IMAGE,扫描件', '["图片型 PDF OCR 是否识别到了编号条款和业务关键词"]', 'explain', 'retrieval', 20, NOW(), NOW(), 1), -(@base_id + 103, 'o2_ocr_png', 'O2 图片 OCR', @scope_parse_code, '验证 PNG 图片文件进入解析主链路并识别关键短语和图片表格。', 'PNG OCR,文字截图,蓝桥订单,RAG-O2-20260630,支付回调延迟', '["O2 扫描 OCR 样例里有没有识别到蓝桥订单 7391"]', 'explain', 'retrieval', 30, NOW(), NOW(), 1), - -(@base_id + 201, 'operation_service_go_live', '客服平台上线与运营', @scope_operation_code, '回答星联智服客服平台上线、知识治理、机器人策略、灰度验证、上线观察、故障处理和质量评估问题。', '星联智服,客服平台,上线运营,知识治理,机器人策略,观察时长,检索命中率,人工转接率', '["检索命中率突然下降的可能原因都有哪些","人工转接率异常升高检查顺序是什么","上线观察与值班规则中观察时长有哪些"]', 'steps', 'retrieval', 10, NOW(), NOW(), 1), -(@base_id + 202, 'operation_release_rollback', '生产发布与回滚', @scope_operation_code, '回答生产发布、灰度节奏、发布暂停、强制回滚、NovaRAG 召回成功率和发布风险控制问题。', '生产发布,回滚,灰度节奏,发布暂停,强制回滚,NovaRAG,召回成功率', '["生产发布默认灰度节奏分几个阶段","强制回滚条件有哪些","哪些情况下默认动作是暂停发布"]', 'steps', 'retrieval', 20, NOW(), NOW(), 1), -(@base_id + 203, 'operation_incident_response', '故障应急响应', @scope_operation_code, '回答核心业务系统故障分级、NovaRAG 检索服务降级、应急处理顺序和升级边界。', '故障应急,NovaRAG降级,检索服务降级,P1,P2,人工转接激增', '["NovaRAG 检索服务降级时按什么顺序处理","连续 15 分钟无法返回检索结果故障等级怎么判断"]', 'steps', 'retrieval', 30, NOW(), NOW(), 1), -(@base_id + 204, 'operation_data_access', '客户数据访问控制', @scope_operation_code, '回答客户数据分级、L4 高敏感数据访问、审批、导出限制、日志保存和审计要求。', '客户数据,L4高敏感,访问控制,审批,DataCleanRoom,日志保存,表格问答', '["L4 高敏感信息的审批要求和默认有效期是什么","L3 和 L4 数据的日志保存期限分别是多少"]', 'list', 'retrieval', 40, NOW(), NOW(), 1), -(@base_id + 205, 'operation_travel_reimbursement', '差旅与费用报销', @scope_operation_code, '回答差旅住宿标准、报销金额阈值、审批流程和费用合规问题。', '差旅,费用报销,住宿标准,审批阈值,财务BP', '["北京出差酒店住宿上限是多少","10000 元以上报销需要哪些审批"]', 'list', 'retrieval', 50, NOW(), NOW(), 1), -(@base_id + 206, 'operation_onboarding_training', '新员工入职培训', @scope_operation_code, '回答入职培训日程、首周安排、30/60/90 天关注重点和培训模块。', '入职培训,首周日程,30天,60天,90天,培训模块', '["入职当天 09:30-10:30 的培训模块是什么","第 30 天、第 60 天、第 90 天分别关注什么"]', 'list', 'retrieval', 60, NOW(), NOW(), 1), -(@base_id + 207, 'operation_raptor_summary', '运营制度跨文档总结', @scope_operation_code, '用于 O7 RAPTOR 单文档和跨文档总结测试,聚合客服平台上线、生产发布和故障应急主线。', 'RAPTOR,跨文档总结,上线风险控制,灰度验证,回滚评估,质量复盘', '["请总结星联智服平台从灰度上线到生产发布再到质量复盘的完整治理流程","这两份规范中和上线风险控制相关的要求有哪些"]', 'compare', 'retrieval', 70, NOW(), NOW(), 1), - -(@base_id + 301, 'graph_audit_trail', 'AuditTrail 审计权限图谱', @scope_graph_code, '验证审计系统和 AuditTrail 的跨文档 canonical、别名、权限记录关系和负边界。', 'AuditTrail,审计系统,权限记录,异常权限扩散,信息安全部,系统管理员', '["审计系统有哪些权限相关要求","审计系统本身是否审批权限或直接回收权限"]', 'list', 'graph_then_evidence', 10, NOW(), NOW(), 1), -(@base_id + 302, 'graph_release_control', 'ReleaseControl 生产发布图谱', @scope_graph_code, '验证 ReleaseControl、CAB、值班 SRE、发布申请、灰度观察和回滚演练的跨文档关系。', 'ReleaseControl,生产发布控制台,CAB,变更评审委员会,值班SRE,回滚演练', '["ReleaseControl 和变更评审委员会、值班 SRE 分别是什么关系","生产发布回滚相关的跨文档图谱社区总结是什么"]', 'list', 'graph_then_evidence', 20, NOW(), NOW(), 1), -(@base_id + 303, 'graph_data_access_guard', 'DataAccessGuard 客户数据图谱', @scope_graph_code, '验证 DataAccessGuard、数据治理负责人、信息安全部、客户数据 Owner 和访问台账的跨文档关系。', 'DataAccessGuard,客户数据访问控制平台,数据治理负责人,信息安全部,客户数据Owner,安全复核组', '["DataAccessGuard 和数据治理负责人、信息安全部分别是什么关系","客户数据访问控制相关的跨文档图谱社区总结是什么"]', 'list', 'graph_then_evidence', 30, NOW(), NOW(), 1), -(@base_id + 304, 'graph_multi_community_boundary', 'GraphRAG 多社区边界', @scope_graph_code, '验证生产发布 community 和客户数据访问 community 的排序、边界和负样例。', '多社区排序,community边界,负样例,职责边界,弱关系外推', '["ReleaseControl 是否负责 L4 高敏感客户数据访问范围确认","DataAccessGuard 是否负责回滚演练和发布窗口管控"]', 'explain', 'graph_assist', 40, NOW(), NOW(), 1) +(@topic_o2_docmind_id, @kb_parse_id, 'Document Mind 与版面解析', @scope_parse_id, '验证普通 PDF 的 layout、表格、FIGURE block、artifact、bbox 和 RAG 产物联动。', 'Document Mind,layout,artifact,bbox,FIGURE,表格解析', '["O2 固定样例中的表格能否被识别成结构化表格","这份 PDF 样例是否包含图示或图片区域"]', 'structure', 'retrieval', 10, NOW(), NOW(), 1), +(@topic_o2_ocr_id, @kb_parse_id, 'OCR 与图片文本解析', @scope_parse_id, '验证图片型 PDF 和 PNG 图片文件的 OCR 文本、页面图片、表格图片和关键业务短语。', '图片型PDF,OCR,PNG OCR,PAGE_IMAGE,TABLE_IMAGE,蓝桥订单,RAG-O2-20260630,支付回调延迟', '["O2 扫描 OCR 样例里有没有识别到蓝桥订单 7391","图片型 PDF OCR 是否识别到了编号条款和业务关键词"]', 'explain', 'retrieval', 20, NOW(), NOW(), 1), +(@topic_o2_table_bbox_id, @kb_parse_id, '表格和页面定位产物', @scope_parse_id, '验证固定样例表格结构、table bbox、PAGE_IMAGE、TABLE_IMAGE 和页面 overlay。', '表格结构,table bbox,PAGE_IMAGE,TABLE_IMAGE,overlay,页面定位', '["O2 固定样例中的表格有几行几列","表格和页面定位产物是否可见"]', 'structure', 'retrieval', 30, NOW(), NOW(), 1), + +(@topic_operation_xinglian_id, @kb_operation_id, '星联智服上线运营', @scope_operation_id, '回答星联智服客服平台上线、知识治理、机器人策略设计、灰度验证、上线观察、故障处理和质量评估问题。', '星联智服,客服平台,上线运营,知识治理,机器人策略,观察时长,检索命中率,人工转接率', '["检索命中率突然下降的可能原因都有哪些","人工转接率异常升高检查顺序是什么","上线观察与值班规则中观察时长有哪些"]', 'steps', 'retrieval', 10, NOW(), NOW(), 1), +(@topic_operation_release_id, @kb_operation_id, '生产发布与回滚', @scope_operation_id, '回答生产发布、灰度节奏、发布暂停、强制回滚、NovaRAG 召回成功率和发布风险控制问题。', '生产发布,回滚,灰度节奏,发布暂停,强制回滚,NovaRAG,召回成功率', '["生产发布默认灰度节奏分几个阶段","强制回滚条件有哪些","哪些情况下默认动作是暂停发布"]', 'steps', 'retrieval', 20, NOW(), NOW(), 1), +(@topic_operation_incident_id, @kb_operation_id, 'NovaRAG 故障应急', @scope_operation_id, '回答核心业务系统故障分级、NovaRAG 检索服务降级、应急处理顺序和升级边界。', '故障应急,NovaRAG降级,检索服务降级,P1,P2,人工转接激增', '["NovaRAG 检索服务降级时按什么顺序处理","连续 15 分钟无法返回检索结果故障等级怎么判断"]', 'steps', 'retrieval', 30, NOW(), NOW(), 1), +(@topic_operation_data_id, @kb_operation_id, '客户数据访问控制', @scope_operation_id, '回答客户数据分级、L4 高敏感数据访问、审批、导出限制、日志保存和审计要求。', '客户数据,L4高敏感,访问控制,审批,DataCleanRoom,日志保存,表格问答', '["L4 高敏感信息的审批要求和默认有效期是什么","L3 和 L4 数据的日志保存期限分别是多少"]', 'list', 'retrieval', 40, NOW(), NOW(), 1), +(@topic_operation_travel_id, @kb_operation_id, '差旅费用报销', @scope_operation_id, '回答差旅住宿标准、报销金额阈值、审批流程和费用合规问题。', '差旅,费用报销,住宿标准,审批阈值,财务BP', '["北京出差酒店住宿上限是多少","10000 元以上报销需要哪些审批"]', 'list', 'retrieval', 50, NOW(), NOW(), 1), +(@topic_operation_onboarding_id, @kb_operation_id, '入职培训与 30/60/90', @scope_operation_id, '回答入职培训日程、首周安排、30/60/90 天关注重点和培训模块。', '入职培训,首周日程,30天,60天,90天,培训模块', '["入职当天 09:30-10:30 的培训模块是什么","第 30 天、第 60 天、第 90 天分别关注什么"]', 'list', 'retrieval', 60, NOW(), NOW(), 1), +(@topic_operation_raptor_id, @kb_operation_id, '运营制度跨文档总结', @scope_operation_id, '用于 O7 RAPTOR 单文档和跨文档总结测试,聚合客服平台上线、生产发布和故障应急主线。', 'RAPTOR,跨文档总结,上线风险控制,灰度验证,回滚评估,质量复盘', '["请总结星联智服平台从灰度上线到生产发布再到质量复盘的完整治理流程","这两份规范中和上线风险控制相关的要求有哪些"]', 'compare', 'retrieval', 70, NOW(), NOW(), 1), + +(@topic_graph_audit_id, @kb_graph_id, '审计系统权限图谱', @scope_graph_id, '验证审计系统和 AuditTrail 的跨文档 canonical、别名、权限记录关系和负边界。', 'AuditTrail,审计系统,权限记录,异常权限扩散,信息安全部,系统管理员', '["审计系统有哪些权限相关要求","审计系统本身是否审批权限或直接回收权限"]', 'list', 'graph_then_evidence', 10, NOW(), NOW(), 1), +(@topic_graph_release_id, @kb_graph_id, 'ReleaseControl 生产发布回滚图谱', @scope_graph_id, '验证 ReleaseControl、CAB、值班 SRE、发布申请、灰度观察和回滚演练的跨文档关系。', 'ReleaseControl,生产发布控制台,CAB,变更评审委员会,值班SRE,回滚演练', '["ReleaseControl 和变更评审委员会、值班 SRE 分别是什么关系","生产发布回滚相关的跨文档图谱社区总结是什么"]', 'list', 'graph_then_evidence', 20, NOW(), NOW(), 1), +(@topic_graph_data_id, @kb_graph_id, 'DataAccessGuard 客户数据访问控制图谱', @scope_graph_id, '验证 DataAccessGuard、数据治理负责人、信息安全部、客户数据 Owner 和访问台账的跨文档关系。', 'DataAccessGuard,客户数据访问控制平台,数据治理负责人,信息安全部,客户数据Owner,安全复核组', '["DataAccessGuard 和数据治理负责人、信息安全部分别是什么关系","客户数据访问控制相关的跨文档图谱社区总结是什么"]', 'list', 'graph_then_evidence', 30, NOW(), NOW(), 1), +(@topic_graph_boundary_id, @kb_graph_id, 'GraphRAG 多社区边界', @scope_graph_id, '验证生产发布 community 和客户数据访问 community 的排序、边界和负样例。', '多社区排序,community边界,负样例,职责边界,弱关系外推', '["ReleaseControl 是否负责 L4 高敏感客户数据访问范围确认","DataAccessGuard 是否负责回滚演练和发布窗口管控"]', 'explain', 'graph_assist', 40, NOW(), NOW(), 1) ON DUPLICATE KEY UPDATE + knowledge_base_id = VALUES(knowledge_base_id), topic_name = VALUES(topic_name), - scope_code = VALUES(scope_code), + scope_id = VALUES(scope_id), description = VALUES(description), aliases = VALUES(aliases), examples = VALUES(examples), @@ -420,7 +363,6 @@ ON DUPLICATE KEY UPDATE /* ========================================================= 5. 文档画像配置 - 说明:如果系统已自动生成画像,这里会覆盖为更适合验收的手工画像。 ========================================================= */ INSERT INTO super_agent_document_profile ( @@ -433,7 +375,7 @@ VALUES (@base_id + 401, @doc_o2_provider_pdf_id, 1, 'O2 固定解析样例,用于验证 Document Mind 解析、layout、表格、FIGURE block、bbox、artifact 和后续 RAG 产物联动。', 'spec', '["Document Mind解析","layout","表格解析","FIGURE block","bbox","artifact"]', '["O2 固定样例中的表格能否被识别成结构化表格","这份 PDF 样例是否包含图示或图片区域"]', 0, 1, 1, 0, 'manual', 2, NULL, NOW(), NOW(), 1), (@base_id + 402, @doc_o2_ocr_pdf_id, 1, 'O2 图片型 PDF OCR 样例,用于验证扫描 PDF 的 OCR 文本、页面图片、表格图片、bbox overlay 和解析观测。', 'spec', '["图片型PDF OCR","PAGE_IMAGE","TABLE_IMAGE","bbox overlay","解析观测"]', '["图片型 PDF OCR 是否识别到了编号条款和业务关键词"]', 0, 1, 1, 0, 'manual', 2, NULL, NOW(), NOW(), 1), (@base_id + 403, @doc_o2_ocr_png_id, 1, 'O2 PNG 图片 OCR 样例,用于验证图片文件进入解析主链路并识别蓝桥订单、RAG-O2-20260630 和支付回调延迟等关键短语。', 'spec', '["PNG OCR","图片文本","蓝桥订单","RAG-O2-20260630","支付回调延迟"]', '["O2 扫描 OCR 样例里有没有识别到蓝桥订单 7391"]', 0, 1, 1, 0, 'manual', 2, NULL, NOW(), NOW(), 1), -(@base_id + 404, @doc_xinglian_id, 1, '星联智服客服平台上线运营手册,覆盖需求澄清、知识治理、机器人策略设计、灰度验证、生产发布、上线观察、故障应急和运营质量评估。', 'manual', '["客服平台上线","知识治理","机器人策略","灰度验证","上线观察","故障处理","运营质量评估"]', '["检索命中率突然下降的可能原因都有哪些","人工转接率异常升高检查顺序是什么","上线观察与值班规则中观察时长有哪些"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1), +(@base_id + 404, @doc_xinglian_id, 1, '星联智服客服平台上线运营手册,覆盖需求澄清、知识治理、机器人策略设计、灰度验证、生产发布、上线观察、故障处置和运营质量评估。', 'manual', '["客服平台上线","知识治理","机器人策略","灰度验证","上线观察","故障处理","运营质量评估"]', '["检索命中率突然下降的可能原因都有哪些","人工转接率异常升高检查顺序是什么","上线观察与值班规则中观察时长有哪些"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1), (@base_id + 405, @doc_release_id, 1, '生产环境发布与回滚操作规范,覆盖发布暂停原则、默认灰度节奏、强制回滚条件、NovaRAG 召回成功率和发布风险控制。', 'rule', '["生产发布","灰度节奏","发布暂停","强制回滚","NovaRAG召回成功率","风险控制"]', '["生产发布默认灰度节奏分几个阶段","强制回滚条件有哪些","哪些情况下默认动作是暂停发布"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1), (@base_id + 406, @doc_incident_id, 1, '核心业务系统故障应急响应预案,覆盖故障分级、NovaRAG 检索服务降级、应急处置顺序和升级边界。', 'troubleshooting', '["故障应急","故障分级","NovaRAG降级","检索服务降级","人工转接激增"]', '["NovaRAG 检索服务降级时按什么顺序处理","连续 15 分钟无法返回检索结果故障等级怎么判断"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1), (@base_id + 407, @doc_data_policy_id, 1, '客户数据分级与访问控制管理制度,覆盖数据等级、L4 高敏感数据访问、审批层级、导出限制、日志保存和审计要求。', 'rule', '["客户数据分级","L4高敏感","访问审批","DataCleanRoom","日志保存","表格问答"]', '["L4 高敏感信息的审批要求和默认有效期是什么","L3 和 L4 数据的日志保存期限分别是多少"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1), @@ -446,7 +388,7 @@ VALUES (@base_id + 414, @doc_data_graph_spec_id, 1, 'O6 客户数据访问控制规范 A,用于验证 DataAccessGuard、数据治理负责人、信息安全部和访问台账 community。', 'rule', '["DataAccessGuard","数据治理负责人","信息安全部","访问台账"]', '["DataAccessGuard 和数据治理负责人、信息安全部分别是什么关系"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1), (@base_id + 415, @doc_data_graph_alias_id, 1, 'O6 客户数据访问控制别名 B,用于验证 DataAccessGuard、客户数据访问控制平台、客户数据 Owner 和安全复核组的别名归一与负边界。', 'rule', '["DataAccessGuard别名","客户数据Owner","安全复核组","负边界"]', '["客户数据访问控制相关的跨文档图谱社区总结是什么","DataAccessGuard 是否负责回滚演练和发布窗口管控"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1) ON DUPLICATE KEY UPDATE - profile_version = COALESCE(profile_version, 0) + 1, + profile_version = VALUES(profile_version), document_summary = VALUES(document_summary), document_type = VALUES(document_type), core_topics = VALUES(core_topics), @@ -463,59 +405,61 @@ ON DUPLICATE KEY UPDATE /* ========================================================= 6. 主题文档关联配置 - 说明:只清理本脚本引入主题下的旧跨文档关联,不清理其他业务主题。 ========================================================= */ UPDATE super_agent_topic_document_relation SET status = 0, edit_time = NOW() -WHERE topic_code IN ( - 'o2_parse_artifact', - 'o2_ocr_pdf', - 'o2_ocr_png', - 'operation_service_go_live', - 'operation_release_rollback', - 'operation_incident_response', - 'operation_data_access', - 'operation_travel_reimbursement', - 'operation_onboarding_training', - 'operation_raptor_summary', - 'graph_audit_trail', - 'graph_release_control', - 'graph_data_access_guard', - 'graph_multi_community_boundary' -); +WHERE knowledge_base_id IN (@kb_parse_id, @kb_operation_id, @kb_graph_id) + AND topic_id IN ( + @topic_o2_docmind_id, + @topic_o2_ocr_id, + @topic_o2_table_bbox_id, + @topic_operation_xinglian_id, + @topic_operation_release_id, + @topic_operation_incident_id, + @topic_operation_data_id, + @topic_operation_travel_id, + @topic_operation_onboarding_id, + @topic_operation_raptor_id, + @topic_graph_audit_id, + @topic_graph_release_id, + @topic_graph_data_id, + @topic_graph_boundary_id + ); INSERT INTO super_agent_topic_document_relation ( - id, topic_code, document_id, relation_score, relation_source, reason, + id, knowledge_base_id, topic_id, document_id, relation_score, relation_source, reason, create_time, edit_time, status ) VALUES -(@base_id + 501, 'o2_parse_artifact', @doc_o2_provider_pdf_id, 0.9800, 'manual', '该样例用于验证普通 PDF 的 Document Mind、layout、表格、FIGURE、bbox 和 artifact。', NOW(), NOW(), 1), -(@base_id + 502, 'o2_ocr_pdf', @doc_o2_ocr_pdf_id, 0.9800, 'manual', '该样例用于验证图片型 PDF OCR、PAGE_IMAGE、TABLE_IMAGE 和 bbox overlay。', NOW(), NOW(), 1), -(@base_id + 503, 'o2_ocr_png', @doc_o2_ocr_png_id, 0.9800, 'manual', '该样例用于验证 PNG 图片 OCR 和关键短语识别。', NOW(), NOW(), 1), - -(@base_id + 504, 'operation_service_go_live', @doc_xinglian_id, 0.9900, 'manual', '该手册是星联智服客服平台上线运营和 O8 主基线的核心文档。', NOW(), NOW(), 1), -(@base_id + 505, 'operation_release_rollback', @doc_release_id, 0.9900, 'manual', '该规范集中描述生产发布、灰度节奏、发布暂停和强制回滚条件。', NOW(), NOW(), 1), -(@base_id + 506, 'operation_release_rollback', @doc_xinglian_id, 0.6200, 'manual', '星联智服手册包含上线观察和回滚评估相关内容,可作为发布回滚跨文档对照。', NOW(), NOW(), 1), -(@base_id + 507, 'operation_incident_response', @doc_incident_id, 0.9900, 'manual', '该预案集中描述 NovaRAG 检索服务降级和核心故障应急处理。', NOW(), NOW(), 1), -(@base_id + 508, 'operation_incident_response', @doc_xinglian_id, 0.6500, 'manual', '星联智服手册包含检索命中率下降、回答口径不完整和人工转接率异常等故障处理章节。', NOW(), NOW(), 1), -(@base_id + 509, 'operation_data_access', @doc_data_policy_id, 0.9900, 'manual', '该制度集中描述客户数据分级、L4 数据访问审批、导出限制和日志保存。', NOW(), NOW(), 1), -(@base_id + 510, 'operation_travel_reimbursement', @doc_travel_id, 0.9900, 'manual', '该办法集中描述差旅住宿标准和报销审批金额阈值。', NOW(), NOW(), 1), -(@base_id + 511, 'operation_onboarding_training', @doc_onboarding_id, 0.9900, 'manual', '该手册集中描述新员工入职培训日程和 30/60/90 天关注重点。', NOW(), NOW(), 1), -(@base_id + 512, 'operation_raptor_summary', @doc_xinglian_id, 0.9600, 'manual', '该手册提供客服平台上线、运营监控和质量复盘主线,适合 RAPTOR 总结。', NOW(), NOW(), 1), -(@base_id + 513, 'operation_raptor_summary', @doc_release_id, 0.9400, 'manual', '该规范提供生产发布、灰度验证和回滚评估主线,适合跨文档 RAPTOR 总结。', NOW(), NOW(), 1), -(@base_id + 514, 'operation_raptor_summary', @doc_incident_id, 0.7200, 'manual', '该预案提供故障应急和降级处理干扰样例,用于验证跨文档总结边界。', NOW(), NOW(), 1), - -(@base_id + 515, 'graph_audit_trail', @doc_audit_evidence_id, 0.9900, 'manual', '该文档提供 AuditTrail 权限记录和异常权限扩散的关系证据。', NOW(), NOW(), 1), -(@base_id + 516, 'graph_audit_trail', @doc_audit_alias_id, 0.9700, 'manual', '该文档提供审计系统与 AuditTrail 的别名、职责和负边界。', NOW(), NOW(), 1), -(@base_id + 517, 'graph_release_control', @doc_release_graph_spec_id, 0.9900, 'manual', '该文档提供 ReleaseControl、CAB、值班 SRE 和回滚演练关系证据。', NOW(), NOW(), 1), -(@base_id + 518, 'graph_release_control', @doc_release_graph_alias_id, 0.9700, 'manual', '该文档提供 ReleaseControl、生产发布控制台、变更评审委员会和 CAB 的别名归一证据。', NOW(), NOW(), 1), -(@base_id + 519, 'graph_data_access_guard', @doc_data_graph_spec_id, 0.9900, 'manual', '该文档提供 DataAccessGuard、数据治理负责人、信息安全部和访问台账关系证据。', NOW(), NOW(), 1), -(@base_id + 520, 'graph_data_access_guard', @doc_data_graph_alias_id, 0.9700, 'manual', '该文档提供 DataAccessGuard、客户数据访问控制平台、客户数据 Owner 和安全复核组的别名归一证据。', NOW(), NOW(), 1), -(@base_id + 521, 'graph_multi_community_boundary', @doc_release_graph_spec_id, 0.9000, 'manual', '用于验证生产发布回滚 community 在多社区排序中的边界。', NOW(), NOW(), 1), -(@base_id + 522, 'graph_multi_community_boundary', @doc_release_graph_alias_id, 0.8800, 'manual', '用于验证生产发布别名文档不会被客户数据访问问题错误选中。', NOW(), NOW(), 1), -(@base_id + 523, 'graph_multi_community_boundary', @doc_data_graph_spec_id, 0.9000, 'manual', '用于验证客户数据访问 community 在多社区排序中的边界。', NOW(), NOW(), 1), -(@base_id + 524, 'graph_multi_community_boundary', @doc_data_graph_alias_id, 0.8800, 'manual', '用于验证客户数据访问别名文档不会被生产发布问题错误选中。', NOW(), NOW(), 1) +(@base_id + 501, @kb_parse_id, @topic_o2_docmind_id, @doc_o2_provider_pdf_id, 0.9800, 'manual', '该样例用于验证普通 PDF 的 Document Mind、layout、表格、FIGURE、bbox 和 artifact。', NOW(), NOW(), 1), +(@base_id + 502, @kb_parse_id, @topic_o2_ocr_id, @doc_o2_ocr_pdf_id, 0.9800, 'manual', '该样例用于验证图片型 PDF OCR、PAGE_IMAGE、TABLE_IMAGE 和 bbox overlay。', NOW(), NOW(), 1), +(@base_id + 503, @kb_parse_id, @topic_o2_ocr_id, @doc_o2_ocr_png_id, 0.9800, 'manual', '该样例用于验证 PNG 图片 OCR 和关键短语识别。', NOW(), NOW(), 1), +(@base_id + 504, @kb_parse_id, @topic_o2_table_bbox_id, @doc_o2_provider_pdf_id, 0.9600, 'manual', '该样例用于验证结构化表格、table bbox 和页面定位产物。', NOW(), NOW(), 1), +(@base_id + 505, @kb_parse_id, @topic_o2_table_bbox_id, @doc_o2_ocr_pdf_id, 0.9400, 'manual', '该样例用于验证 OCR 表格图片和 TABLE_IMAGE 产物。', NOW(), NOW(), 1), + +(@base_id + 506, @kb_operation_id, @topic_operation_xinglian_id, @doc_xinglian_id, 0.9900, 'manual', '该手册是星联智服客服平台上线运营和 O8 主基线的核心文档。', NOW(), NOW(), 1), +(@base_id + 507, @kb_operation_id, @topic_operation_release_id, @doc_release_id, 0.9900, 'manual', '该规范集中描述生产发布、灰度节奏、发布暂停和强制回滚条件。', NOW(), NOW(), 1), +(@base_id + 508, @kb_operation_id, @topic_operation_release_id, @doc_xinglian_id, 0.6200, 'manual', '星联智服手册包含上线观察和回滚评估相关内容,可作为发布回滚跨文档对照。', NOW(), NOW(), 1), +(@base_id + 509, @kb_operation_id, @topic_operation_incident_id, @doc_incident_id, 0.9900, 'manual', '该预案集中描述 NovaRAG 检索服务降级和核心故障应急处理。', NOW(), NOW(), 1), +(@base_id + 510, @kb_operation_id, @topic_operation_incident_id, @doc_xinglian_id, 0.6500, 'manual', '星联智服手册包含检索命中率下降、回答口径不完整和人工转接率异常等故障处理章节。', NOW(), NOW(), 1), +(@base_id + 511, @kb_operation_id, @topic_operation_data_id, @doc_data_policy_id, 0.9900, 'manual', '该制度集中描述客户数据分级、L4 数据访问审批、导出限制和日志保存。', NOW(), NOW(), 1), +(@base_id + 512, @kb_operation_id, @topic_operation_travel_id, @doc_travel_id, 0.9900, 'manual', '该办法集中描述差旅住宿标准和报销审批金额阈值。', NOW(), NOW(), 1), +(@base_id + 513, @kb_operation_id, @topic_operation_onboarding_id, @doc_onboarding_id, 0.9900, 'manual', '该手册集中描述新员工入职培训日程和 30/60/90 天关注重点。', NOW(), NOW(), 1), +(@base_id + 514, @kb_operation_id, @topic_operation_raptor_id, @doc_xinglian_id, 0.9600, 'manual', '该手册提供客服平台上线、运营监控和质量复盘主线,适合 RAPTOR 总结。', NOW(), NOW(), 1), +(@base_id + 515, @kb_operation_id, @topic_operation_raptor_id, @doc_release_id, 0.9400, 'manual', '该规范提供生产发布、灰度验证和回滚评估主线,适合跨文档 RAPTOR 总结。', NOW(), NOW(), 1), +(@base_id + 516, @kb_operation_id, @topic_operation_raptor_id, @doc_incident_id, 0.7200, 'manual', '该预案提供故障应急和降级处理干扰样例,用于验证跨文档总结边界。', NOW(), NOW(), 1), + +(@base_id + 517, @kb_graph_id, @topic_graph_audit_id, @doc_audit_evidence_id, 0.9900, 'manual', '该文档提供 AuditTrail 权限记录和异常权限扩散的关系证据。', NOW(), NOW(), 1), +(@base_id + 518, @kb_graph_id, @topic_graph_audit_id, @doc_audit_alias_id, 0.9700, 'manual', '该文档提供审计系统与 AuditTrail 的别名、职责和负边界。', NOW(), NOW(), 1), +(@base_id + 519, @kb_graph_id, @topic_graph_release_id, @doc_release_graph_spec_id, 0.9900, 'manual', '该文档提供 ReleaseControl、CAB、值班 SRE 和回滚演练关系证据。', NOW(), NOW(), 1), +(@base_id + 520, @kb_graph_id, @topic_graph_release_id, @doc_release_graph_alias_id, 0.9700, 'manual', '该文档提供 ReleaseControl、生产发布控制台、变更评审委员会和 CAB 的别名归一证据。', NOW(), NOW(), 1), +(@base_id + 521, @kb_graph_id, @topic_graph_data_id, @doc_data_graph_spec_id, 0.9900, 'manual', '该文档提供 DataAccessGuard、数据治理负责人、信息安全部和访问台账关系证据。', NOW(), NOW(), 1), +(@base_id + 522, @kb_graph_id, @topic_graph_data_id, @doc_data_graph_alias_id, 0.9700, 'manual', '该文档提供 DataAccessGuard、客户数据访问控制平台、客户数据 Owner 和安全复核组的别名归一证据。', NOW(), NOW(), 1), +(@base_id + 523, @kb_graph_id, @topic_graph_boundary_id, @doc_release_graph_spec_id, 0.9000, 'manual', '用于验证生产发布回滚 community 在多社区排序中的边界。', NOW(), NOW(), 1), +(@base_id + 524, @kb_graph_id, @topic_graph_boundary_id, @doc_release_graph_alias_id, 0.8800, 'manual', '用于验证生产发布别名文档不会被客户数据访问问题错误选中。', NOW(), NOW(), 1), +(@base_id + 525, @kb_graph_id, @topic_graph_boundary_id, @doc_data_graph_spec_id, 0.9000, 'manual', '用于验证客户数据访问 community 在多社区排序中的边界。', NOW(), NOW(), 1), +(@base_id + 526, @kb_graph_id, @topic_graph_boundary_id, @doc_data_graph_alias_id, 0.8800, 'manual', '用于验证客户数据访问别名文档不会被生产发布问题错误选中。', NOW(), NOW(), 1) ON DUPLICATE KEY UPDATE relation_score = VALUES(relation_score), relation_source = VALUES(relation_source), @@ -528,63 +472,63 @@ ON DUPLICATE KEY UPDATE ========================================================= */ SELECT - id, - original_file_name, - document_name, - knowledge_scope_code, - knowledge_scope_name, - business_category, - document_tags, - parse_status, - strategy_status, - index_status, - last_index_task_id -FROM super_agent_document -WHERE id IN ( - @doc_o2_provider_pdf_id, - @doc_o2_ocr_pdf_id, - @doc_o2_ocr_png_id, - @doc_xinglian_id, - @doc_release_id, - @doc_incident_id, - @doc_data_policy_id, - @doc_travel_id, - @doc_onboarding_id, - @doc_audit_evidence_id, - @doc_audit_alias_id, - @doc_release_graph_spec_id, - @doc_release_graph_alias_id, - @doc_data_graph_spec_id, - @doc_data_graph_alias_id -) -ORDER BY knowledge_scope_code, original_file_name; + kb.kb_name, + COUNT(d.id) AS doc_count +FROM tmp_o2_o8_latest_document latest +JOIN super_agent_document d ON d.id = latest.document_id +JOIN tmp_o2_o8_kb kb ON kb.knowledge_base_id = d.knowledge_base_id +GROUP BY kb.kb_name +ORDER BY kb.kb_name; SELECT - scope_code, - scope_name, - aliases, - sort_order, - status -FROM super_agent_knowledge_scope_node -WHERE scope_code IN (@scope_parse_code, @scope_operation_code, @scope_graph_code) -ORDER BY sort_order; + s.knowledge_base_id, + kb.base_name AS knowledge_base_name, + s.id AS scope_id, + s.scope_name, + s.aliases, + s.sort_order, + s.status +FROM super_agent_knowledge_scope_node s +JOIN super_agent_knowledge_base kb ON kb.id = s.knowledge_base_id +WHERE s.id IN (@scope_parse_id, @scope_operation_id, @scope_graph_id) +ORDER BY kb.base_name, s.sort_order; SELECT - topic_code, - topic_name, - scope_code, - answer_shape, - execution_preference, - sort_order, - status -FROM super_agent_knowledge_topic_node -WHERE scope_code IN (@scope_parse_code, @scope_operation_code, @scope_graph_code) -ORDER BY scope_code, sort_order; + t.knowledge_base_id, + kb.base_name AS knowledge_base_name, + t.id AS topic_id, + t.topic_name, + t.scope_id, + s.scope_name, + t.answer_shape, + t.execution_preference, + t.sort_order, + t.status +FROM super_agent_knowledge_topic_node t +JOIN super_agent_knowledge_base kb ON kb.id = t.knowledge_base_id +JOIN super_agent_knowledge_scope_node s ON s.id = t.scope_id +WHERE t.id IN ( + @topic_o2_docmind_id, + @topic_o2_ocr_id, + @topic_o2_table_bbox_id, + @topic_operation_xinglian_id, + @topic_operation_release_id, + @topic_operation_incident_id, + @topic_operation_data_id, + @topic_operation_travel_id, + @topic_operation_onboarding_id, + @topic_operation_raptor_id, + @topic_graph_audit_id, + @topic_graph_release_id, + @topic_graph_data_id, + @topic_graph_boundary_id +) +ORDER BY kb.base_name, s.sort_order, t.sort_order; SELECT p.document_id, - d.document_name, - d.knowledge_scope_code, + d.knowledge_base_name, + d.original_file_name, p.document_type, p.profile_status, p.graph_friendly, @@ -593,7 +537,7 @@ SELECT p.supports_graph_assist, p.status FROM super_agent_document_profile p -LEFT JOIN super_agent_document d ON d.id = p.document_id +JOIN super_agent_document d ON d.id = p.document_id WHERE p.document_id IN ( @doc_o2_provider_pdf_id, @doc_o2_ocr_pdf_id, @@ -611,37 +555,68 @@ WHERE p.document_id IN ( @doc_data_graph_spec_id, @doc_data_graph_alias_id ) -ORDER BY d.knowledge_scope_code, d.original_file_name; +ORDER BY d.knowledge_base_name, d.original_file_name; SELECT - r.topic_code, + kb.base_name AS knowledge_base_name, + s.scope_name, t.topic_name, - t.scope_code, - r.document_id, - d.document_name, + d.original_file_name, r.relation_score, r.relation_source, r.reason, r.status FROM super_agent_topic_document_relation r -LEFT JOIN super_agent_knowledge_topic_node t ON t.topic_code = r.topic_code -LEFT JOIN super_agent_document d ON d.id = r.document_id -WHERE r.topic_code IN ( - 'o2_parse_artifact', - 'o2_ocr_pdf', - 'o2_ocr_png', - 'operation_service_go_live', - 'operation_release_rollback', - 'operation_incident_response', - 'operation_data_access', - 'operation_travel_reimbursement', - 'operation_onboarding_training', - 'operation_raptor_summary', - 'graph_audit_trail', - 'graph_release_control', - 'graph_data_access_guard', - 'graph_multi_community_boundary' -) -ORDER BY t.scope_code, t.sort_order, r.relation_score DESC; +JOIN super_agent_knowledge_base kb ON kb.id = r.knowledge_base_id +JOIN super_agent_knowledge_topic_node t ON t.id = r.topic_id +JOIN super_agent_knowledge_scope_node s ON s.id = t.scope_id +JOIN super_agent_document d ON d.id = r.document_id +WHERE r.knowledge_base_id IN (@kb_parse_id, @kb_operation_id, @kb_graph_id) + AND r.topic_id IN ( + @topic_o2_docmind_id, + @topic_o2_ocr_id, + @topic_o2_table_bbox_id, + @topic_operation_xinglian_id, + @topic_operation_release_id, + @topic_operation_incident_id, + @topic_operation_data_id, + @topic_operation_travel_id, + @topic_operation_onboarding_id, + @topic_operation_raptor_id, + @topic_graph_audit_id, + @topic_graph_release_id, + @topic_graph_data_id, + @topic_graph_boundary_id + ) + AND r.status = 1 +ORDER BY kb.base_name, s.sort_order, t.sort_order, r.relation_score DESC, d.original_file_name; + +SELECT + r.id, + r.knowledge_base_id AS relation_kb, + t.knowledge_base_id AS topic_kb, + d.knowledge_base_id AS doc_kb, + d.original_file_name +FROM super_agent_topic_document_relation r +JOIN super_agent_knowledge_topic_node t ON t.id = r.topic_id +JOIN super_agent_document d ON d.id = r.document_id +WHERE r.status = 1 + AND r.topic_id IN ( + @topic_o2_docmind_id, + @topic_o2_ocr_id, + @topic_o2_table_bbox_id, + @topic_operation_xinglian_id, + @topic_operation_release_id, + @topic_operation_incident_id, + @topic_operation_data_id, + @topic_operation_travel_id, + @topic_operation_onboarding_id, + @topic_operation_raptor_id, + @topic_graph_audit_id, + @topic_graph_release_id, + @topic_graph_data_id, + @topic_graph_boundary_id + ) + AND (r.knowledge_base_id <> t.knowledge_base_id OR r.knowledge_base_id <> d.knowledge_base_id); COMMIT; diff --git "a/sql/Mysql/O8-\350\257\201\346\215\256\350\272\253\344\273\275\345\255\227\346\256\265\345\242\236\351\207\217\350\204\232\346\234\254-2026-07-05.sql" "b/sql/Mysql/O8-\350\257\201\346\215\256\350\272\253\344\273\275\345\255\227\346\256\265\345\242\236\351\207\217\350\204\232\346\234\254-2026-07-05.sql" new file mode 100644 index 0000000000000000000000000000000000000000..2dc91f0ae13bda14572b809e4a3b833ddb9bded3 --- /dev/null +++ "b/sql/Mysql/O8-\350\257\201\346\215\256\350\272\253\344\273\275\345\255\227\346\256\265\345\242\236\351\207\217\350\204\232\346\234\254-2026-07-05.sql" @@ -0,0 +1,16 @@ +-- O8 证据身份原则修复:为现有 super_agent_chat_retrieval_result 表补齐观测字段。 +-- 适用场景:已存在旧表结构时执行;全新初始化可直接使用 create_table_mysql.sql。 + +ALTER TABLE super_agent_chat_retrieval_result + ADD COLUMN chunk_type VARCHAR(32) DEFAULT NULL COMMENT '切块类型:TEXT/LIST/TABLE/TITLE/RAPTOR_SOURCE_CHUNK等' AFTER chunk_id, + ADD COLUMN context_identity VARCHAR(255) DEFAULT NULL COMMENT '上下文身份:ParentBlock、GraphRAG包装、RAPTOR摘要等' AFTER chunk_char_count, + ADD COLUMN citation_identity VARCHAR(255) DEFAULT NULL COMMENT '真实可引用证据身份:chunk/quote/table cell/source chunk' AFTER context_identity, + ADD COLUMN citation_evidence_type VARCHAR(64) DEFAULT NULL COMMENT '引用证据类型:CHUNK/TABLE_CELL_OR_ROW/KG_QUOTE_SOURCE/RAPTOR_SOURCE_CHUNK/CONTEXT_ONLY' AFTER citation_identity, + ADD COLUMN context_only TINYINT(1) DEFAULT '0' COMMENT '是否仅为上下文,不可直接作为citation证据' AFTER citation_evidence_type, + ADD COLUMN source_evidence_resolved TINYINT(1) DEFAULT '0' COMMENT '是否已解析到真实可引用source evidence' AFTER context_only; + +CREATE INDEX idx_retrieval_result_citation_identity + ON super_agent_chat_retrieval_result (citation_identity); + +CREATE INDEX idx_retrieval_result_context_only + ON super_agent_chat_retrieval_result (context_only, source_evidence_resolved); diff --git a/sql/Mysql/create_table_mysql.sql b/sql/Mysql/create_table_mysql.sql index 31548bbb6718f8ad4961a5b117b2ec0ec40749ee..007d107e522f33a4ad8d6f80c4d66e0f4bb546c9 100644 --- a/sql/Mysql/create_table_mysql.sql +++ b/sql/Mysql/create_table_mysql.sql @@ -5,6 +5,9 @@ CREATE TABLE IF NOT EXISTS super_agent_chat_dialogue ( chat_mode TINYINT(1) NOT NULL DEFAULT '1' COMMENT '1:当前文档问答 2:开放式提问', selected_document_id BIGINT DEFAULT NULL COMMENT '当前会话显式锁定的提问文档id', selected_document_name VARCHAR(255) DEFAULT NULL COMMENT '当前会话显式锁定的提问文档名称', + knowledge_base_selection_mode VARCHAR(16) NOT NULL DEFAULT 'NONE' COMMENT '当前会话知识库选择模式 NONE/ALL/SELECTED', + selected_knowledge_base_ids_json JSON DEFAULT NULL COMMENT '当前会话已选知识库id快照', + selected_knowledge_base_names_json JSON DEFAULT NULL COMMENT '当前会话已选知识库名称快照', create_time DATETIME DEFAULT NULL COMMENT '创建时间', edit_time DATETIME DEFAULT NULL COMMENT '编辑时间', status TINYINT(1) DEFAULT '1' COMMENT '1:正常 0:删除', @@ -28,6 +31,10 @@ CREATE TABLE IF NOT EXISTS super_agent_chat_exchange ( finish_note TEXT DEFAULT NULL COMMENT '失败或终止说明', first_token_latency_ms BIGINT DEFAULT NULL COMMENT '首包耗时,毫秒', total_latency_ms BIGINT DEFAULT NULL COMMENT '总耗时,毫秒', + knowledge_base_selection_mode VARCHAR(16) NOT NULL DEFAULT 'NONE' COMMENT '当轮知识库选择模式 NONE/ALL/SELECTED', + selected_knowledge_base_ids_json JSON DEFAULT NULL COMMENT '当轮已选知识库id快照', + selected_knowledge_base_names_json JSON DEFAULT NULL COMMENT '当轮已选知识库名称快照', + retrieval_config_snapshot_json JSON DEFAULT NULL COMMENT '当轮生效RAG检索配置快照', create_time DATETIME DEFAULT NULL COMMENT '创建时间', edit_time DATETIME DEFAULT NULL COMMENT '编辑时间', status TINYINT(1) DEFAULT '1' COMMENT '1:正常 0:删除', @@ -104,6 +111,26 @@ CREATE TABLE IF NOT EXISTS GRAPH_CHECKPOINT ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Spring AI Alibaba Graph checkpoint 表'; +CREATE TABLE IF NOT EXISTS `super_agent_knowledge_base` ( + `id` bigint NOT NULL COMMENT '主键id', + `base_name` varchar(128) NOT NULL COMMENT '知识库名称', + `description` varchar(1024) DEFAULT NULL COMMENT '知识库描述', + `embedding_model` varchar(128) DEFAULT NULL COMMENT '向量模型快照', + `retrieval_config_json` JSON DEFAULT NULL COMMENT '检索配置JSON', + `graph_rag_config_json` JSON DEFAULT NULL COMMENT 'GraphRAG配置JSON', + `raptor_config_json` JSON DEFAULT NULL COMMENT 'RAPTOR配置JSON', + `metadata_filter_json` JSON DEFAULT NULL COMMENT '元数据过滤配置JSON', + `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否默认知识库 1:是 0:否', + `sort_order` int DEFAULT '0' COMMENT '排序值', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `edit_time` datetime DEFAULT NULL COMMENT '编辑时间', + `status` tinyint(1) DEFAULT '1' COMMENT '1:正常 0:删除', + PRIMARY KEY (`id`), + KEY `idx_knowledge_base_default` (`is_default`, `status`), + KEY `idx_knowledge_base_sort` (`sort_order`, `id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='轻量知识库表'; + + CREATE TABLE IF NOT EXISTS `super_agent_document` ( `id` bigint NOT NULL COMMENT '主键id', `document_name` varchar(255) NOT NULL COMMENT '文档名称', @@ -124,10 +151,8 @@ CREATE TABLE IF NOT EXISTS `super_agent_document` ( `content_quality_level` tinyint DEFAULT '0' COMMENT '内容质量 0:未知 1:低 2:中 3:高', `parse_text_path` varchar(512) DEFAULT NULL COMMENT '解析文本存储路径', `parse_error_msg` varchar(1000) DEFAULT NULL COMMENT '解析失败原因', - `knowledge_scope_code` varchar(64) DEFAULT NULL COMMENT '业务知识域编码,例如 oa / crm / finance', - `knowledge_scope_name` varchar(128) DEFAULT NULL COMMENT '业务知识域名称,例如 OA系统 / CRM系统', - `business_category` varchar(128) DEFAULT NULL COMMENT '业务分类,例如 流程 / 规则 / 操作手册', - `document_tags` varchar(512) DEFAULT NULL COMMENT '逗号分隔标签快照', + `knowledge_base_id` bigint NOT NULL COMMENT '所属知识库id', + `knowledge_base_name` varchar(128) NOT NULL COMMENT '所属知识库名称快照', `current_plan_id` bigint DEFAULT NULL COMMENT '当前策略方案id', `last_parse_task_id` bigint DEFAULT NULL COMMENT '最近一次成功解析任务id', `structure_node_count` int DEFAULT '0' COMMENT '最近一次结构化解析生成的节点数', @@ -140,7 +165,7 @@ CREATE TABLE IF NOT EXISTS `super_agent_document` ( KEY `idx_parse_status` (`parse_status`), KEY `idx_strategy_status` (`strategy_status`), KEY `idx_index_status` (`index_status`), - KEY `idx_knowledge_scope_code` (`knowledge_scope_code`), + KEY `idx_knowledge_base_id` (`knowledge_base_id`), KEY `idx_current_plan_id` (`current_plan_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档表'; @@ -716,9 +741,9 @@ CREATE TABLE IF NOT EXISTS `super_agent_raptor_node` ( CREATE TABLE IF NOT EXISTS `super_agent_knowledge_scope_node` ( `id` bigint NOT NULL COMMENT '主键id', - `scope_code` varchar(64) NOT NULL COMMENT '知识范围编码', + `knowledge_base_id` bigint NOT NULL COMMENT '所属知识库id', `scope_name` varchar(128) NOT NULL COMMENT '知识范围名称', - `parent_scope_code` varchar(64) DEFAULT NULL COMMENT '父级知识范围编码', + `parent_scope_id` bigint DEFAULT NULL COMMENT '父级知识范围id', `description` varchar(1024) DEFAULT NULL COMMENT '范围描述', `aliases` varchar(512) DEFAULT NULL COMMENT '别名,英文逗号分隔', `examples` text COMMENT '典型问题 JSON 数组', @@ -727,17 +752,17 @@ CREATE TABLE IF NOT EXISTS `super_agent_knowledge_scope_node` ( `edit_time` datetime DEFAULT NULL COMMENT '编辑时间', `status` tinyint(1) DEFAULT '1' COMMENT '1:正常 0:删除', PRIMARY KEY (`id`), - UNIQUE KEY `uk_scope_code` (`scope_code`), - KEY `idx_parent_scope_code` (`parent_scope_code`), + KEY `idx_knowledge_base_id` (`knowledge_base_id`), + KEY `idx_parent_scope_id` (`parent_scope_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识范围节点表'; CREATE TABLE IF NOT EXISTS `super_agent_knowledge_topic_node` ( `id` bigint NOT NULL COMMENT '主键id', - `topic_code` varchar(64) NOT NULL COMMENT '主题编码', + `knowledge_base_id` bigint NOT NULL COMMENT '所属知识库id', `topic_name` varchar(128) NOT NULL COMMENT '主题名称', - `scope_code` varchar(64) NOT NULL COMMENT '所属知识范围编码', + `scope_id` bigint NOT NULL COMMENT '所属知识范围id', `description` varchar(1024) DEFAULT NULL COMMENT '主题描述', `aliases` varchar(512) DEFAULT NULL COMMENT '别名,英文逗号分隔', `examples` text COMMENT '典型问题 JSON 数组', @@ -748,8 +773,8 @@ CREATE TABLE IF NOT EXISTS `super_agent_knowledge_topic_node` ( `edit_time` datetime DEFAULT NULL COMMENT '编辑时间', `status` tinyint(1) DEFAULT '1' COMMENT '1:正常 0:删除', PRIMARY KEY (`id`), - UNIQUE KEY `uk_topic_code` (`topic_code`), - KEY `idx_scope_code` (`scope_code`), + KEY `idx_knowledge_base_id` (`knowledge_base_id`), + KEY `idx_scope_id` (`scope_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识主题节点表'; @@ -782,7 +807,8 @@ CREATE TABLE IF NOT EXISTS `super_agent_document_profile` ( CREATE TABLE IF NOT EXISTS `super_agent_topic_document_relation` ( `id` bigint NOT NULL COMMENT '主键id', - `topic_code` varchar(64) NOT NULL COMMENT '主题编码', + `knowledge_base_id` bigint NOT NULL COMMENT '所属知识库id', + `topic_id` bigint NOT NULL COMMENT '主题id', `document_id` bigint NOT NULL COMMENT '文档id', `relation_score` decimal(8,4) DEFAULT '0.0000' COMMENT '关联分数', `relation_source` varchar(64) DEFAULT NULL COMMENT '关联来源 auto/manual/mixed', @@ -791,9 +817,10 @@ CREATE TABLE IF NOT EXISTS `super_agent_topic_document_relation` ( `edit_time` datetime DEFAULT NULL COMMENT '编辑时间', `status` tinyint(1) DEFAULT '1' COMMENT '1:正常 0:删除', PRIMARY KEY (`id`), - UNIQUE KEY `uk_topic_document` (`topic_code`, `document_id`), + UNIQUE KEY `uk_base_topic_document` (`knowledge_base_id`, `topic_id`, `document_id`), + KEY `idx_knowledge_base_id` (`knowledge_base_id`), KEY `idx_document_id` (`document_id`), - KEY `idx_topic_code` (`topic_code`), + KEY `idx_topic_id` (`topic_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主题文档关联表'; @@ -805,6 +832,10 @@ CREATE TABLE IF NOT EXISTS `super_agent_knowledge_route_trace` ( `question` text COMMENT '原始问题', `rewrite_question` text COMMENT '改写问题', `mode` varchar(32) DEFAULT NULL COMMENT '运行模式 shadow/auto', + `knowledge_base_selection_mode` varchar(16) DEFAULT NULL COMMENT '知识库选择模式 NONE/ALL/SELECTED', + `selected_knowledge_base_ids_json` JSON DEFAULT NULL COMMENT '已选知识库id快照', + `selected_knowledge_base_names_json` JSON DEFAULT NULL COMMENT '已选知识库名称快照', + `allowed_document_ids_json` JSON DEFAULT NULL COMMENT '知识库硬边界允许检索的文档id快照', `top_scopes_json` text COMMENT '候选知识范围 JSON', `top_topics_json` text COMMENT '候选主题 JSON', `top_documents_json` text COMMENT '候选文档 JSON', @@ -849,12 +880,18 @@ CREATE TABLE IF NOT EXISTS super_agent_chat_retrieval_result ( document_id BIGINT DEFAULT NULL COMMENT '文档id', document_name VARCHAR(255) DEFAULT NULL COMMENT '文档名称', chunk_id BIGINT DEFAULT NULL COMMENT '文档切块id', + chunk_type VARCHAR(32) DEFAULT NULL COMMENT '切块类型:TEXT/LIST/TABLE/TITLE/RAPTOR_SOURCE_CHUNK等', chunk_no INT DEFAULT NULL COMMENT '切块序号', parent_block_id BIGINT DEFAULT NULL COMMENT '父块id', parent_block_no INT DEFAULT NULL COMMENT '父块序号', section_path VARCHAR(512) DEFAULT NULL COMMENT '章节路径', chunk_text_preview VARCHAR(500) DEFAULT NULL COMMENT '文档块内容预览(前500字符)', chunk_char_count INT DEFAULT NULL COMMENT '文档块字符数', + context_identity VARCHAR(255) DEFAULT NULL COMMENT '上下文身份:ParentBlock、GraphRAG包装、RAPTOR摘要等', + citation_identity VARCHAR(255) DEFAULT NULL COMMENT '真实可引用证据身份:chunk/quote/table cell/source chunk', + citation_evidence_type VARCHAR(64) DEFAULT NULL COMMENT '引用证据类型:CHUNK/TABLE_CELL_OR_ROW/KG_QUOTE_SOURCE/RAPTOR_SOURCE_CHUNK/CONTEXT_ONLY', + context_only TINYINT(1) DEFAULT '0' COMMENT '是否仅为上下文,不可直接作为citation证据', + source_evidence_resolved TINYINT(1) DEFAULT '0' COMMENT '是否已解析到真实可引用source evidence', create_time DATETIME DEFAULT NULL COMMENT '创建时间', edit_time DATETIME DEFAULT NULL COMMENT '编辑时间', status TINYINT(1) DEFAULT '1' COMMENT '1:正常 0:删除', @@ -863,7 +900,9 @@ CREATE TABLE IF NOT EXISTS super_agent_chat_retrieval_result ( KEY idx_retrieval_result_trace (trace_id), KEY idx_retrieval_result_sub_question (exchange_id, sub_question_index), KEY idx_retrieval_result_channel (channel_type, is_selected), - KEY idx_retrieval_result_document (document_id, chunk_id) + KEY idx_retrieval_result_document (document_id, chunk_id), + KEY idx_retrieval_result_citation_identity (citation_identity), + KEY idx_retrieval_result_context_only (context_only, source_evidence_resolved) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='检索结果快照表'; CREATE TABLE IF NOT EXISTS super_agent_chat_channel_execution ( diff --git "a/sql/Mysql/\346\230\237\350\201\224\346\231\272\346\234\215\344\270\216XX200\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" "b/sql/Mysql/\346\230\237\350\201\224\346\231\272\346\234\215\344\270\216XX200\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" deleted file mode 100644 index a353506b63ad5e475c680bd6b05382ebab132e21..0000000000000000000000000000000000000000 --- "a/sql/Mysql/\346\230\237\350\201\224\346\231\272\346\234\215\344\270\216XX200\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" +++ /dev/null @@ -1,577 +0,0 @@ -/* - 星联智服全渠道客服平台上线与运营管理手册.md - XX-200智能网关产品手册.pdf - 知识路由初始化脚本 - - 使用顺序: - 1. 先在管理台上传这两份文档。 - 2. 进入每份文档详情页,完成“确认策略方案”和“构建索引执行”。 - 3. 回到文档列表或数据库中拿到两份文档的 document_id。 - 4. 替换下面两个变量 @doc_customer_service_id / @doc_xx200_id。 - 5. 执行本脚本。 - - 注意: - - 本脚本会更新 super_agent_document 的知识域编码、名称、业务分类和标签。 - - 本脚本会写入知识范围、知识主题、文档画像、主题文档关联。 - - 本脚本使用 INSERT ... ON DUPLICATE KEY UPDATE,可重复执行。 - - 本脚本不会修改文档解析、策略方案、索引状态、chunk、向量库数据。 -*/ - -START TRANSACTION; - -/* ========================================================= - 0. 请先替换这里的两个文档 ID - ========================================================= */ - -SET @doc_customer_service_id = 0; -- TODO: 替换为“星联智服全渠道客服平台上线与运营管理手册.md”的 document_id -SET @doc_xx200_id = 0; -- TODO: 替换为“XX-200智能网关产品手册.pdf”的 document_id - -/* - 如果你不确定文档 ID,可以先执行下面的查询,再把查出来的 id 填到上面变量里: - - SELECT id, document_name, original_file_name, index_status, last_index_task_id - FROM super_agent_document - WHERE status = 1 - AND ( - document_name LIKE '%星联智服%' - OR original_file_name LIKE '%星联智服%' - OR document_name LIKE '%XX-200%' - OR original_file_name LIKE '%XX-200%' - ) - ORDER BY create_time DESC; -*/ - -/* ========================================================= - 1. 固定配置编码 - ========================================================= */ - -SET @scope_customer_service_code = 'customer_service_platform_ops'; -SET @scope_customer_service_name = '客服平台上线运营'; -SET @scope_xx200_code = 'xx200_gateway_product'; -SET @scope_xx200_name = 'XX-200智能网关'; - -/* - 这些 id 只用于新插入范围、主题、关系、画像时。 - 如果你的数据库里极端情况下已经占用了这些 id,可以把 @base_id 改成其他未使用的大整数。 -*/ -SET @base_id = 8800041600000000000; - -/* ========================================================= - 2. 更新两份文档主表元数据 - ========================================================= */ - -UPDATE super_agent_document -SET - document_name = '星联智服全渠道客服平台上线与运营管理手册', - knowledge_scope_code = @scope_customer_service_code, - knowledge_scope_name = @scope_customer_service_name, - business_category = '平台运营手册', - document_tags = '客服平台,上线,灰度发布,生产发布,回滚,知识治理,RAG,值班,故障应急,质量评估', - edit_time = NOW() -WHERE id = @doc_customer_service_id - AND status = 1; - -UPDATE super_agent_document -SET - document_name = 'XX-200智能网关产品技术手册', - knowledge_scope_code = @scope_xx200_code, - knowledge_scope_name = @scope_xx200_name, - business_category = '产品技术手册', - document_tags = 'XX-200,智能网关,边缘计算,安装部署,网络配置,协议配置,Modbus,日志查看,故障排查,工业物联网', - edit_time = NOW() -WHERE id = @doc_xx200_id - AND status = 1; - -/* ========================================================= - 3. 知识范围配置 - ========================================================= */ - -INSERT INTO super_agent_knowledge_scope_node ( - id, scope_code, scope_name, parent_scope_code, description, aliases, examples, sort_order, - create_time, edit_time, status -) -VALUES -( - @base_id + 1, - @scope_customer_service_code, - @scope_customer_service_name, - NULL, - '用于承接客服平台项目上线、知识治理、灰度发布、生产发布、值班观察、故障应急、质量评估等运营管理类问题。', - '客服平台,全渠道客服平台,星联智服,上线运营,知识治理,灰度发布', - '["平台上线总流程有哪几个阶段","灰度验证期间要看哪些指标","什么时候需要回滚","上线后要观察多久"]', - 10, - NOW(), NOW(), 1 -), -( - @base_id + 2, - @scope_xx200_code, - @scope_xx200_name, - NULL, - '用于承接XX-200智能网关的产品规格、安装部署、网络配置、协议配置、日志查看与故障排查等产品技术问题。', - 'XX-200,智能网关,工业网关,边缘网关,网关产品,安装部署', - '["XX-200支持哪些协议","XX-200怎么安装部署","默认登录地址和账号是什么","双WAN怎么配置","故障日志在哪里看"]', - 20, - NOW(), NOW(), 1 -) -ON DUPLICATE KEY UPDATE - scope_name = VALUES(scope_name), - parent_scope_code = VALUES(parent_scope_code), - description = VALUES(description), - aliases = VALUES(aliases), - examples = VALUES(examples), - sort_order = VALUES(sort_order), - edit_time = NOW(), - status = 1; - -/* ========================================================= - 4. 知识主题配置 - answer_shape 固定为 list / explain / steps - execution_preference 固定为 retrieval / graph_assist - ========================================================= */ - -INSERT INTO super_agent_knowledge_topic_node ( - id, topic_code, topic_name, scope_code, description, aliases, examples, - answer_shape, execution_preference, sort_order, - create_time, edit_time, status -) -VALUES -( - @base_id + 101, - 'platform_go_live_process', - '平台上线总流程', - @scope_customer_service_code, - '回答客服平台项目上线从立项、知识治理、灰度验证到生产发布的整体流程。', - '上线流程,上线步骤,项目上线,上线里程碑', - '["平台上线总流程有哪几个阶段","项目上线要经过哪些里程碑"]', - 'steps', - 'retrieval', - 10, - NOW(), NOW(), 1 -), -( - @base_id + 102, - 'knowledge_governance', - '知识采集与治理', - @scope_customer_service_code, - '回答知识来源分类、知识接入前检查、知识域划分和不适合接入知识库的内容。', - '知识治理,知识接入,知识域,知识库治理', - '["知识接入前要检查什么","知识域应该怎么划分","哪些内容不适合接入机器人知识库"]', - 'list', - 'retrieval', - 20, - NOW(), NOW(), 1 -), -( - @base_id + 103, - 'gray_release_and_rollback', - '灰度验证与回滚', - @scope_customer_service_code, - '回答灰度范围、灰度期指标、禁止事项,以及哪些情况要触发回滚评估。', - '灰度验证,灰度发布,回滚,回滚条件,灰度指标', - '["灰度期间必须看哪些指标","什么时候需要回滚","灰度期间有哪些禁止事项"]', - 'list', - 'retrieval', - 30, - NOW(), NOW(), 1 -), -( - @base_id + 104, - 'post_launch_observation', - '上线观察与值班', - @scope_customer_service_code, - '回答普通版本和高风险版本的观察时长、值班安排和观察日报要求。', - '上线观察,观察时长,值班规则,观察日报', - '["上线后要观察多久","值班安排怎么配","观察日报至少写什么"]', - 'list', - 'retrieval', - 40, - NOW(), NOW(), 1 -), -( - @base_id + 105, - 'fault_response', - '典型故障处理', - @scope_customer_service_code, - '回答平台在检索命中率下降、回答不完整、转人工率升高等场景下的处理方法。', - '故障处理,检索命中率下降,人工转接率异常,回答口径不完整', - '["检索命中率突然下降怎么排查","回答口径不完整怎么办","人工转接率异常升高怎么查"]', - 'steps', - 'retrieval', - 50, - NOW(), NOW(), 1 -), -( - @base_id + 106, - 'quality_evaluation', - '运营质量评估', - @scope_customer_service_code, - '回答质量评估层次、指标定义和每周每月每季度的评审节奏。', - '质量评估,质量指标,运营指标,复盘节奏', - '["运营质量指标怎么分层","质量评审节奏是怎样的","常见质量指标有哪些"]', - 'list', - 'retrieval', - 60, - NOW(), NOW(), 1 -), -( - @base_id + 201, - 'product_overview_spec', - '产品概述与技术规格', - @scope_xx200_code, - '回答XX-200的产品简介、核心特性、处理器、内存、网络接口、串口、电源和工作环境等规格。', - '产品概述,技术规格,核心特性,参数规格', - '["XX-200有哪些核心特性","XX-200的技术规格是什么","支持哪些协议和接口"]', - 'list', - 'retrieval', - 10, - NOW(), NOW(), 1 -), -( - @base_id + 202, - 'installation_deployment', - '安装部署', - @scope_xx200_code, - '回答安装前准备、DIN导轨安装、接电、连线和浏览器访问管理界面等步骤。', - '安装部署,安装前准备,硬件安装,上电安装', - '["安装前要准备什么","XX-200怎么安装","上电部署步骤是什么"]', - 'steps', - 'retrieval', - 20, - NOW(), NOW(), 1 -), -( - @base_id + 203, - 'initial_access_login', - '初始访问与首次登录', - @scope_xx200_code, - '回答LAN1默认IP、浏览器访问地址、默认账号密码和首次登录改密要求。', - '默认IP,默认账号,首次登录,初始配置', - '["默认登录地址是什么","默认账号密码是什么","首次登录后密码要求是什么"]', - 'steps', - 'retrieval', - 30, - NOW(), NOW(), 1 -), -( - @base_id + 204, - 'network_configuration', - '网络配置', - @scope_xx200_code, - '回答LAN/WAN使用方式、双WAN负载均衡示例、DNS、健康检查和故障切换。', - '网络配置,双WAN,静态IP,DHCP,PPPoE,负载均衡', - '["双WAN负载均衡怎么配置","WAN支持哪些接入方式","健康检查怎么设置"]', - 'steps', - 'retrieval', - 40, - NOW(), NOW(), 1 -), -( - @base_id + 205, - 'protocol_configuration', - '协议配置', - @scope_xx200_code, - '回答Modbus RTU采集的串口配置、设备模板、点位定义和采集周期建议。', - '协议配置,Modbus,RS-485,设备模板,采集点位', - '["Modbus RTU怎么配置","温湿度传感器点位怎么建","采集周期建议多少"]', - 'steps', - 'retrieval', - 50, - NOW(), NOW(), 1 -), -( - @base_id + 206, - 'troubleshooting_and_logs', - '故障排查与日志查看', - @scope_xx200_code, - '回答常见故障现象、可能原因、解决方案,以及系统日志、应用日志、审计日志和导出诊断包。', - '故障排查,日志查看,诊断包,系统日志,应用日志,审计日志', - '["PWR灯不亮怎么处理","无法访问管理界面怎么排查","日志在哪里看","怎么导出诊断包"]', - 'steps', - 'retrieval', - 60, - NOW(), NOW(), 1 -) -ON DUPLICATE KEY UPDATE - topic_name = VALUES(topic_name), - scope_code = VALUES(scope_code), - description = VALUES(description), - aliases = VALUES(aliases), - examples = VALUES(examples), - answer_shape = VALUES(answer_shape), - execution_preference = VALUES(execution_preference), - sort_order = VALUES(sort_order), - edit_time = NOW(), - status = 1; - -/* ========================================================= - 5. 文档画像配置 - 说明:如果系统已自动生成画像,这里会覆盖为更适合演示的手工画像。 - ========================================================= */ - -INSERT INTO super_agent_document_profile ( - id, document_id, profile_version, document_summary, document_type, core_topics, example_questions, - graph_friendly, supports_graph_outline, supports_item_lookup, supports_graph_assist, - profile_source, profile_status, error_msg, - create_time, edit_time, status -) -VALUES -( - @base_id + 301, - @doc_customer_service_id, - 1, - '本手册用于规范星联智服全渠道客服平台从需求澄清、知识治理、机器人策略设计、灰度验证、生产发布、上线观察、故障应急到运营质量评估的全链路管理要求。', - 'manual', - '["平台上线总流程","知识采集与治理","灰度验证与回滚","上线观察与值班","典型故障处理","运营质量评估"]', - '["平台上线总流程有哪几个阶段","灰度验证期间必须看哪些指标","什么时候需要触发回滚评估","上线后要观察多久","检索命中率突然下降怎么排查"]', - 1, 1, 1, 1, - 'manual', - 2, - NULL, - NOW(), NOW(), 1 -), -( - @base_id + 302, - @doc_xx200_id, - 1, - '本手册介绍XX-200智能网关的产品概述、核心特性、技术规格、安装部署、初始访问、网络配置、协议配置、常见故障排查和日志查看方式。', - 'manual', - '["产品概述与技术规格","安装部署","初始访问与首次登录","网络配置","协议配置","故障排查与日志查看"]', - '["XX-200支持哪些工业协议","默认登录地址和账号密码是什么","安装前要准备什么","双WAN负载均衡怎么配置","Modbus RTU怎么配置","日志在哪里看"]', - 1, 1, 1, 1, - 'manual', - 2, - NULL, - NOW(), NOW(), 1 -) -ON DUPLICATE KEY UPDATE - profile_version = COALESCE(profile_version, 0) + 1, - document_summary = VALUES(document_summary), - document_type = VALUES(document_type), - core_topics = VALUES(core_topics), - example_questions = VALUES(example_questions), - graph_friendly = VALUES(graph_friendly), - supports_graph_outline = VALUES(supports_graph_outline), - supports_item_lookup = VALUES(supports_item_lookup), - supports_graph_assist = VALUES(supports_graph_assist), - profile_source = VALUES(profile_source), - profile_status = VALUES(profile_status), - error_msg = VALUES(error_msg), - edit_time = NOW(), - status = 1; - -/* ========================================================= - 6. 主题文档关联配置 - 说明:先清理这些主题下的旧跨文档关联,再写入目标关联。 - ========================================================= */ - -UPDATE super_agent_topic_document_relation -SET status = 0, edit_time = NOW() -WHERE topic_code IN ( - 'platform_go_live_process', - 'knowledge_governance', - 'gray_release_and_rollback', - 'post_launch_observation', - 'fault_response', - 'quality_evaluation' -) -AND document_id <> @doc_customer_service_id; - -UPDATE super_agent_topic_document_relation -SET status = 0, edit_time = NOW() -WHERE topic_code IN ( - 'product_overview_spec', - 'installation_deployment', - 'initial_access_login', - 'network_configuration', - 'protocol_configuration', - 'troubleshooting_and_logs' -) -AND document_id <> @doc_xx200_id; - -INSERT INTO super_agent_topic_document_relation ( - id, topic_code, document_id, relation_score, relation_source, reason, - create_time, edit_time, status -) -VALUES -( - @base_id + 401, - 'platform_go_live_process', - @doc_customer_service_id, - 0.9800, - 'manual', - '该手册完整描述了客服平台从立项到上线观察的全流程。', - NOW(), NOW(), 1 -), -( - @base_id + 402, - 'knowledge_governance', - @doc_customer_service_id, - 0.9700, - 'manual', - '该手册包含知识来源分类、知识接入检查和知识域划分建议。', - NOW(), NOW(), 1 -), -( - @base_id + 403, - 'gray_release_and_rollback', - @doc_customer_service_id, - 0.9800, - 'manual', - '该手册明确给出了灰度范围、灰度指标、禁止事项和回滚触发条件。', - NOW(), NOW(), 1 -), -( - @base_id + 404, - 'post_launch_observation', - @doc_customer_service_id, - 0.9700, - 'manual', - '该手册包含上线观察时长、值班安排和观察日报模板。', - NOW(), NOW(), 1 -), -( - @base_id + 405, - 'fault_response', - @doc_customer_service_id, - 0.9600, - 'manual', - '该手册给出了检索命中率下降、回答不完整和人工转接异常的处理方法。', - NOW(), NOW(), 1 -), -( - @base_id + 406, - 'quality_evaluation', - @doc_customer_service_id, - 0.9500, - 'manual', - '该手册明确了质量评估层次、指标定义和评审节奏。', - NOW(), NOW(), 1 -), -( - @base_id + 501, - 'product_overview_spec', - @doc_xx200_id, - 0.9800, - 'manual', - '该手册集中描述了XX-200的产品简介、核心特性和完整技术规格。', - NOW(), NOW(), 1 -), -( - @base_id + 502, - 'installation_deployment', - @doc_xx200_id, - 0.9800, - 'manual', - '该手册包含安装前准备、硬件安装和上电部署步骤。', - NOW(), NOW(), 1 -), -( - @base_id + 503, - 'initial_access_login', - @doc_xx200_id, - 0.9700, - 'manual', - '该手册明确给出了默认IP、访问地址、默认账号密码和首次登录改密要求。', - NOW(), NOW(), 1 -), -( - @base_id + 504, - 'network_configuration', - @doc_xx200_id, - 0.9800, - 'manual', - '该手册包含双WAN、静态IP、DHCP、DNS、健康检查和故障切换配置。', - NOW(), NOW(), 1 -), -( - @base_id + 505, - 'protocol_configuration', - @doc_xx200_id, - 0.9700, - 'manual', - '该手册包含Modbus RTU串口配置、设备模板和点位定义。', - NOW(), NOW(), 1 -), -( - @base_id + 506, - 'troubleshooting_and_logs', - @doc_xx200_id, - 0.9800, - 'manual', - '该手册包含常见故障现象、可能原因、解决方案和日志查看方式。', - NOW(), NOW(), 1 -) -ON DUPLICATE KEY UPDATE - relation_score = VALUES(relation_score), - relation_source = VALUES(relation_source), - reason = VALUES(reason), - edit_time = NOW(), - status = 1; - -/* ========================================================= - 7. 执行后检查 - ========================================================= */ - -SELECT - id, - document_name, - knowledge_scope_code, - knowledge_scope_name, - business_category, - document_tags, - index_status, - last_index_task_id -FROM super_agent_document -WHERE id IN (@doc_customer_service_id, @doc_xx200_id); - -SELECT - scope_code, - scope_name, - aliases, - sort_order, - status -FROM super_agent_knowledge_scope_node -WHERE scope_code IN (@scope_customer_service_code, @scope_xx200_code) -ORDER BY sort_order; - -SELECT - topic_code, - topic_name, - scope_code, - answer_shape, - execution_preference, - sort_order, - status -FROM super_agent_knowledge_topic_node -WHERE scope_code IN (@scope_customer_service_code, @scope_xx200_code) -ORDER BY scope_code, sort_order; - -SELECT - r.topic_code, - t.topic_name, - r.document_id, - d.document_name, - r.relation_score, - r.relation_source, - r.reason, - r.status -FROM super_agent_topic_document_relation r -LEFT JOIN super_agent_knowledge_topic_node t ON t.topic_code = r.topic_code -LEFT JOIN super_agent_document d ON d.id = r.document_id -WHERE r.topic_code IN ( - 'platform_go_live_process', - 'knowledge_governance', - 'gray_release_and_rollback', - 'post_launch_observation', - 'fault_response', - 'quality_evaluation', - 'product_overview_spec', - 'installation_deployment', - 'initial_access_login', - 'network_configuration', - 'protocol_configuration', - 'troubleshooting_and_logs' -) -ORDER BY t.scope_code, t.sort_order, r.relation_score DESC; - -COMMIT; diff --git "a/sql/Mysql/\346\270\205\347\251\272\350\241\250\346\225\260\346\215\256.sql" "b/sql/Mysql/\346\270\205\347\251\272\350\241\250\346\225\260\346\215\256.sql" deleted file mode 100644 index 1356971a92d157e0f63a86d941f0be4520579f98..0000000000000000000000000000000000000000 --- "a/sql/Mysql/\346\270\205\347\251\272\350\241\250\346\225\260\346\215\256.sql" +++ /dev/null @@ -1,37 +0,0 @@ -SET FOREIGN_KEY_CHECKS = 0; - -TRUNCATE TABLE `super_agent_chat_dialogue`; -TRUNCATE TABLE `super_agent_chat_exchange`; -TRUNCATE TABLE `super_agent_chat_memory_summary`; -TRUNCATE TABLE `super_agent_chat_exchange_trace_stage`; -TRUNCATE TABLE `GRAPH_THREAD`; -TRUNCATE TABLE `GRAPH_CHECKPOINT`; -TRUNCATE TABLE `super_agent_document`; -TRUNCATE TABLE `super_agent_document_strategy_plan`; -TRUNCATE TABLE `super_agent_document_strategy_step`; -TRUNCATE TABLE `super_agent_document_task`; -TRUNCATE TABLE `super_agent_document_task_log`; -TRUNCATE TABLE `super_agent_document_parse_artifact`; -TRUNCATE TABLE `super_agent_document_block`; -TRUNCATE TABLE `super_agent_document_table`; -TRUNCATE TABLE `super_agent_document_table_column`; -TRUNCATE TABLE `super_agent_document_table_row`; -TRUNCATE TABLE `super_agent_document_table_cell`; -TRUNCATE TABLE `super_agent_document_structure_node`; -TRUNCATE TABLE `super_agent_document_parent_block`; -TRUNCATE TABLE `super_agent_document_chunk`; -TRUNCATE TABLE `super_agent_kg_entity`; -TRUNCATE TABLE `super_agent_kg_relation`; -TRUNCATE TABLE `super_agent_kg_evidence`; -TRUNCATE TABLE `super_agent_kg_community`; -TRUNCATE TABLE `super_agent_raptor_node`; -TRUNCATE TABLE `super_agent_knowledge_scope_node`; -TRUNCATE TABLE `super_agent_knowledge_topic_node`; -TRUNCATE TABLE `super_agent_document_profile`; -TRUNCATE TABLE `super_agent_topic_document_relation`; -TRUNCATE TABLE `super_agent_knowledge_route_trace`; -TRUNCATE TABLE `super_agent_chat_retrieval_result`; -TRUNCATE TABLE `super_agent_chat_channel_execution`; -TRUNCATE TABLE `super_agent_chat_stage_benchmark`; - -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git "a/sql/PostgresSql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\346\270\205\347\220\206\346\227\247PG\345\220\221\351\207\217\346\225\260\346\215\256.sql" "b/sql/PostgresSql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\346\270\205\347\220\206\346\227\247PG\345\220\221\351\207\217\346\225\260\346\215\256.sql" deleted file mode 100644 index f443d8af879b31026e34d6fb8f5a2a45ff36ffe8..0000000000000000000000000000000000000000 --- "a/sql/PostgresSql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\346\270\205\347\220\206\346\227\247PG\345\220\221\351\207\217\346\225\260\346\215\256.sql" +++ /dev/null @@ -1,150 +0,0 @@ -/* - O2-O8 RAG 完整测试:按当前 15 份文档物理清理旧 PG 向量数据 - - 用途: - - MySQL 已经删除旧数据并重新上传了 15 份测试文档。 - - PostgreSQL 的 public.super_agent_document_embedding 和 public.super_agent_raptor_embedding - 还残留旧 document_id 的向量数据。 - - 本脚本只保留下面 15 个 document_id 对应的数据,其他 document_id 的数据全部物理删除。 - - 文档 ID 来源: - - 已按用户提供的 MySQL 查询从 super_agent_document 获取。 - - 查询条件为 status = 1 且 original_file_name in 15 份必传样例。 - - 执行方式: - PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d super_agent_pgvector \ - -f sql/PostgresSql/O2-O8-RAG完整测试清理旧PG向量数据.sql - - 注意: - - 本脚本只操作 PostgreSQL,不改 MySQL。 - - 本脚本是物理 DELETE,不是软删除。 - - super_agent_raptor_embedding 中 document_id = 0 的 dataset-level 旧数据也会被删除; - 因为它不属于这 15 个 document_id。 -*/ - -BEGIN; - -CREATE TEMP TABLE keep_document_ids ( - document_id BIGINT PRIMARY KEY, - file_name TEXT NOT NULL -) ON COMMIT DROP; - -INSERT INTO keep_document_ids (document_id, file_name) VALUES -(2296737919064432640, 'O2-provider-artifact验收样例.pdf'), -(2296737919064433640, 'O2-扫描OCR验收样例-图片型PDF.pdf'), -(2296737919064434525, 'O2-扫描OCR验收样例-文字截图.png'), -(2296738159582601310, 'O6多社区排序-客户数据访问控制别名B.md'), -(2296738125222865466, 'O6多社区排序-客户数据访问控制规范A.md'), -(2296738090863129667, 'O6多社区排序-生产发布回滚别名B.md'), -(2296738056503393982, 'O6多社区排序-生产发布回滚规范A.md'), -(2296738056503388553, 'O6跨文档图谱-审计系统别名说明B.md'), -(2296738022143652966, 'O6跨文档图谱-审计证据规范A.md'), -(2296737953424176918, '客户数据分级与访问控制管理制度.md'), -(2296737987783912016, '差旅与费用报销管理办法.md'), -(2296737919064435493, '星联智服全渠道客服平台上线与运营管理手册.md'), -(2296737953424172808, '核心业务系统故障应急响应预案.md'), -(2296737987783916947, '澄星智能新员工入职培训手册.md'), -(2296737919064439185, '生产环境发布与回滚操作规范.md'); - -/* ========================================================= - 1. 执行前预览 - ========================================================= */ - -SELECT - 'keep_document_ids' AS item, - COUNT(*) AS count -FROM keep_document_ids; - -SELECT - 'super_agent_document_embedding_before' AS item, - COUNT(*) AS total_rows, - COUNT(*) FILTER (WHERE document_id IN (SELECT document_id FROM keep_document_ids)) AS keep_rows, - COUNT(*) FILTER (WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)) AS delete_rows -FROM public.super_agent_document_embedding; - -SELECT - 'super_agent_raptor_embedding_before' AS item, - COUNT(*) AS total_rows, - COUNT(*) FILTER (WHERE document_id IN (SELECT document_id FROM keep_document_ids)) AS keep_rows, - COUNT(*) FILTER (WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)) AS delete_rows -FROM public.super_agent_raptor_embedding; - -SELECT - 'document_embedding_delete_by_document_id' AS item, - document_id, - COUNT(*) AS rows_to_delete -FROM public.super_agent_document_embedding -WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids) -GROUP BY document_id -ORDER BY document_id; - -SELECT - 'raptor_embedding_delete_by_document_id' AS item, - document_id, - COUNT(*) AS rows_to_delete -FROM public.super_agent_raptor_embedding -WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids) -GROUP BY document_id -ORDER BY document_id; - -/* ========================================================= - 2. 物理删除旧数据 - ========================================================= */ - -DELETE FROM public.super_agent_document_embedding -WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids); - -DELETE FROM public.super_agent_raptor_embedding -WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids); - -/* ========================================================= - 3. 删除后复核 - ========================================================= */ - -SELECT - 'super_agent_document_embedding_after' AS item, - COUNT(*) AS total_rows, - COUNT(*) FILTER (WHERE document_id IN (SELECT document_id FROM keep_document_ids)) AS keep_rows, - COUNT(*) FILTER (WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)) AS old_rows_remaining -FROM public.super_agent_document_embedding; - -SELECT - 'super_agent_raptor_embedding_after' AS item, - COUNT(*) AS total_rows, - COUNT(*) FILTER (WHERE document_id IN (SELECT document_id FROM keep_document_ids)) AS keep_rows, - COUNT(*) FILTER (WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)) AS old_rows_remaining -FROM public.super_agent_raptor_embedding; - -SELECT - 'document_embedding_keep_by_document_id' AS item, - e.document_id, - k.file_name, - COUNT(*) AS rows_kept -FROM public.super_agent_document_embedding e -JOIN keep_document_ids k ON k.document_id = e.document_id -GROUP BY e.document_id, k.file_name -ORDER BY k.file_name; - -SELECT - 'raptor_embedding_keep_by_document_id' AS item, - e.document_id, - k.file_name, - COUNT(*) AS rows_kept -FROM public.super_agent_raptor_embedding e -JOIN keep_document_ids k ON k.document_id = e.document_id -GROUP BY e.document_id, k.file_name -ORDER BY k.file_name; - -COMMIT; - -/* - 可选:删除后如果要让备份文件更小,建议执行 VACUUM。 - - 普通回收统计: - VACUUM (ANALYZE) public.super_agent_document_embedding; - VACUUM (ANALYZE) public.super_agent_raptor_embedding; - - 强制压缩物理文件,需低峰期执行,会锁表: - VACUUM FULL public.super_agent_document_embedding; - VACUUM FULL public.super_agent_raptor_embedding; -*/ diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/controller/BusinessChatController.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/controller/BusinessChatController.java index dd57f71de83df13aee1e731ac3c89fbd9ec1e3ec..5ab87cd04ed80840e9a5ba79c29933df20e3c643 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/controller/BusinessChatController.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/controller/BusinessChatController.java @@ -15,6 +15,7 @@ import org.javaup.ai.chatagent.model.KnowledgeDocumentOptionView; import org.javaup.ai.chatagent.model.RetrievalResultView; import org.javaup.ai.chatagent.model.StageBenchmarkView; import org.javaup.ai.chatagent.service.BusinessChatService; +import org.javaup.ai.manage.vo.KnowledgeBaseOptionVo; import org.javaup.ai.chatagent.vo.ConversationResetVo; import org.javaup.ai.chatagent.vo.ConversationSessionListVo; import org.javaup.ai.chatagent.vo.ConversationStopVo; @@ -49,6 +50,11 @@ public class BusinessChatController { return ApiResponse.ok(businessChatService.listKnowledgeDocumentOptions()); } + @PostMapping("/knowledge-base/options") + public ApiResponse> knowledgeBaseOptions() { + return ApiResponse.ok(businessChatService.listKnowledgeBaseOptions()); + } + @PostMapping("/session/stop") public ApiResponse stop(@Valid @RequestBody ConversationIdentityDto dto) { return ApiResponse.ok(businessChatService.stopConversation(dto.getConversationId())); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatDialogue.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatDialogue.java index b0b18dacfc63848e20cd5ed05ba6a41bb406a444..03f21bb225e8ddb6ce6b10991dd3fdc935ee084a 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatDialogue.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatDialogue.java @@ -40,4 +40,13 @@ public class SuperAgentChatDialogue extends BaseTableData { @TableField("selected_document_name") private String selectedDocumentName; + + @TableField("knowledge_base_selection_mode") + private String knowledgeBaseSelectionMode; + + @TableField("selected_knowledge_base_ids_json") + private String selectedKnowledgeBaseIdsJson; + + @TableField("selected_knowledge_base_names_json") + private String selectedKnowledgeBaseNamesJson; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatExchange.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatExchange.java index 3e5b4cbf1abe89e1c229e2f91a0d54df6b9b9054..ea5b3cd3bf0b425c7f0ae04992b0900bbbea6f5e 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatExchange.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatExchange.java @@ -61,4 +61,16 @@ public class SuperAgentChatExchange extends BaseTableData { @TableField("total_latency_ms") private Long totalResponseTimeMs; + + @TableField("knowledge_base_selection_mode") + private String knowledgeBaseSelectionMode; + + @TableField("selected_knowledge_base_ids_json") + private String selectedKnowledgeBaseIdsJson; + + @TableField("selected_knowledge_base_names_json") + private String selectedKnowledgeBaseNamesJson; + + @TableField("retrieval_config_snapshot_json") + private String retrievalConfigSnapshotJson; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatRetrievalResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatRetrievalResult.java index d81315e1cfaf7c1b7c81185881641060bd2b561f..4342ce110f83cd48fdabc9212be7d4e17d95e2e4 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatRetrievalResult.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatRetrievalResult.java @@ -97,6 +97,9 @@ public class SuperAgentChatRetrievalResult extends BaseTableData { @TableField("chunk_id") private Long chunkId; + @TableField("chunk_type") + private String chunkType; + @TableField("chunk_no") private Integer chunkNo; @@ -114,4 +117,19 @@ public class SuperAgentChatRetrievalResult extends BaseTableData { @TableField("chunk_char_count") private Integer chunkCharCount; + + @TableField("context_identity") + private String contextIdentity; + + @TableField("citation_identity") + private String citationIdentity; + + @TableField("citation_evidence_type") + private String citationEvidenceType; + + @TableField("context_only") + private Integer contextOnly; + + @TableField("source_evidence_resolved") + private Integer sourceEvidenceResolved; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/dto/ChatRequestDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/dto/ChatRequestDto.java index f96b2f5a7579704c4cc38ff1df9ccd2a482086e6..360ae78989723803c5fe1df4bb1ca62293b5bf40 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/dto/ChatRequestDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/dto/ChatRequestDto.java @@ -5,6 +5,8 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; + /** * @program: 企业级别深度设计 AI Agent。添加 阿星不是程序员 微信,添加时备注 super 来获取项目的完整资料 * @description: 数据传输对象 @@ -24,4 +26,8 @@ public class ChatRequestDto { private String chatMode; private String selectedDocumentId; + + private String knowledgeBaseSelectionMode; + + private List selectedKnowledgeBaseIds; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationExchangeView.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationExchangeView.java index baefc60c445be5ff0a6ab144b42a395f88cf0400..e9f07e15552a74c57466a04e7e0943192331ea3d 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationExchangeView.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationExchangeView.java @@ -32,6 +32,10 @@ public class ConversationExchangeView { private String errorMessage; private Long firstResponseTimeMs; private Long totalResponseTimeMs; + private String knowledgeBaseSelectionMode; + private List selectedKnowledgeBaseIds; + private List selectedKnowledgeBaseNames; + private String retrievalConfigSnapshotJson; private Date createTime; private Date editTime; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationSessionView.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationSessionView.java index e489145f9be9c3111d11bfe912935de0d5b37b50..771380487e608d23cc58fe5021484e7ef1476a62 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationSessionView.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationSessionView.java @@ -31,6 +31,9 @@ public class ConversationSessionView { private ChatQueryMode chatMode; private String selectedDocumentId; private String selectedDocumentName; + private String knowledgeBaseSelectionMode; + private List selectedKnowledgeBaseIds; + private List selectedKnowledgeBaseNames; private Instant createdAt; private Instant updatedAt; private List exchanges; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/KnowledgeDocumentOptionView.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/KnowledgeDocumentOptionView.java index 358f8b34191973266dba909928d8c870d2f54197..266c968e4b765ab3f395cb5048fba35ced0c2a71 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/KnowledgeDocumentOptionView.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/KnowledgeDocumentOptionView.java @@ -17,7 +17,6 @@ public class KnowledgeDocumentOptionView { private String documentId; private String documentName; - private String knowledgeScopeName; - private String businessCategory; - private String documentTags; + private String knowledgeBaseId; + private String knowledgeBaseName; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/RetrievalResultView.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/RetrievalResultView.java index 96cfe1ae9f9c9c28078086378b49340e7236af28..531a79fe2f0a3e047f3378179094d0821c45da75 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/RetrievalResultView.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/RetrievalResultView.java @@ -40,11 +40,17 @@ public class RetrievalResultView { private Long documentId; private String documentName; private Long chunkId; + private String chunkType; private Integer chunkNo; private Long parentBlockId; private Integer parentBlockNo; private String sectionPath; private String chunkTextPreview; private Integer chunkCharCount; + private String contextIdentity; + private String citationIdentity; + private String citationEvidenceType; + private boolean contextOnly; + private boolean sourceEvidenceResolved; private Instant createTime; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/SearchReference.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/SearchReference.java index 727afb814c10af603a456417c564951c62a08a11..eaadedc57ed7e8977d33ad7b12f7cb02852453b5 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/SearchReference.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/SearchReference.java @@ -2,6 +2,8 @@ package org.javaup.ai.chatagent.model; import lombok.Data; import lombok.NoArgsConstructor; +import org.javaup.ai.chatagent.rag.model.EvidenceIdentity; +import org.javaup.ai.chatagent.rag.support.EvidenceIdentityResolver; import java.util.List; @@ -29,8 +31,14 @@ public class SearchReference { private String documentName; + private Long knowledgeBaseId; + + private String knowledgeBaseName; + private Long chunkId; + private String chunkType; + private Long parentBlockId; private Integer parentBlockNo; @@ -57,10 +65,6 @@ public class SearchReference { private String toolName; - private String knowledgeScopeCode; - - private String knowledgeScopeName; - private Integer pageNo; private String pageRange; @@ -129,6 +133,8 @@ public class SearchReference { private Long kgEvidenceId; + private String kgEvidenceGroundingLevel; + private String kgGraphPath; private Integer kgHopCount; @@ -147,6 +153,8 @@ public class SearchReference { private String kgCrossDocumentCommunityKey; + private boolean kgCommunitySummaryOnly; + private Integer kgCrossDocumentCommunityEntityCount; private Integer kgCrossDocumentCommunityRelationGroupCount; @@ -179,6 +187,8 @@ public class SearchReference { private String raptorSummary; + private String raptorSourceStatus; + private String answerSegment; private String quoteText; @@ -191,6 +201,24 @@ public class SearchReference { private boolean citationRepaired; + private String finalSelectionReason; + + private String evidenceApplicabilityStatus; + + private String evidenceApplicabilityReason; + + private String evidenceRole; + + private String contextIdentity; + + private String citationIdentity; + + private String citationEvidenceType; + + private boolean contextOnly; + + private boolean sourceEvidenceResolved; + public SearchReference(String title, String url, String snippet) { this.sourceType = "WEB"; this.title = title; @@ -201,24 +229,13 @@ public class SearchReference { } public String uniqueKey() { - if (raptorNodeId != null && chunkId != null) { - return "RAPTOR:" + raptorNodeId + ":" + chunkId; - } - if (kgEvidenceId != null) { - return "GRAPH_RAG:" + kgEvidenceId; - } - if (tableId != null) { - return "TABLE:" + tableId - + ":" + (tableOperation == null ? "" : tableOperation) - + ":" + (tableMetricColumn == null ? "" : tableMetricColumn) - + ":" + (tableGroupByColumn == null ? "" : tableGroupByColumn) - + ":" + (snippet == null ? 0 : snippet.hashCode()); - } - if (parentBlockId != null) { - return "PARENT:" + parentBlockId; + EvidenceIdentity citation = EvidenceIdentityResolver.citationIdentity(this); + if (citation != null && citation.present()) { + return citation.value(); } - if (chunkId != null) { - return "DOCUMENT:" + chunkId; + EvidenceIdentity context = EvidenceIdentityResolver.contextIdentity(this); + if (context != null && context.present()) { + return context.value(); } if (url != null && !url.isBlank()) { return "WEB:" + url; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/config/ChatRagProperties.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/config/ChatRagProperties.java index 77fb74357c01a651f7b0b40536faade89d970e89..67336407dfe688d321fbc6ad97c12fd7a9353762 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/config/ChatRagProperties.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/config/ChatRagProperties.java @@ -25,9 +25,9 @@ public class ChatRagProperties { private int maxSubQuestions = 4; - private int vectorTopK = 8; + private int vectorTopK = 10; - private int keywordTopK = 8; + private int keywordTopK = 10; private int graphRagTopK = 5; @@ -49,9 +49,13 @@ public class ChatRagProperties { private double raptorSummaryQualityFloor = 0.42D; - private int candidateTopK = 10; + private int candidateTopK = 40; - private int finalTopK = 5; + private int rerankCandidateTopK = 24; + + private int reserveCandidateTopK = 30; + + private int finalTopK = 6; private double minVectorSimilarity = 0.45D; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/GraphThenEvidenceExecutor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/GraphThenEvidenceExecutor.java index 4da0f97c2b2fd05ef0e2620719e5040433331467..0161a5cbca1bc6b07f9846d7ad4ace98fe423970 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/GraphThenEvidenceExecutor.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/GraphThenEvidenceExecutor.java @@ -81,6 +81,8 @@ public class GraphThenEvidenceExecutor implements ConversationExecutor { taskInfo.traceRecorder().completeStage(graphStage, "结构图定位完成,但证据不满足约束。", Map.of( "targetSection", graphResult == null || graphResult.getTargetSection() == null ? "" : StrUtil.blankToDefault(graphResult.getTargetSection().displayTitle(), ""), "targetItemIndex", graphResult == null || graphResult.getTargetItem() == null || graphResult.getTargetItem().getItemIndex() == null ? "" : String.valueOf(graphResult.getTargetItem().getItemIndex()), + "graphThenEvidenceFailed", true, + "fallbackRecommendation", ExecutionMode.RETRIEVAL.name(), "notes", List.of("结构图未定位到满足条件的章节或编号项。") )); } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/RagChatExecutor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/RagChatExecutor.java index 9bc76877ddbd02f38b12038d0608f83fcf58f27f..543833b7570a676b21870b5eaf6bbfa35383dc11 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/RagChatExecutor.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/RagChatExecutor.java @@ -132,6 +132,9 @@ public class RagChatExecutor implements ConversationExecutor { item.put("pageNo", reference.getPageNo()); item.put("pageRange", StrUtil.blankToDefault(reference.getPageRange(), "")); item.put("bboxJson", StrUtil.blankToDefault(reference.getBboxJson(), "")); + item.put("finalSelectionReason", StrUtil.blankToDefault(reference.getFinalSelectionReason(), "")); + item.put("evidenceApplicabilityStatus", StrUtil.blankToDefault(reference.getEvidenceApplicabilityStatus(), "")); + item.put("evidenceApplicabilityReason", StrUtil.blankToDefault(reference.getEvidenceApplicabilityReason(), "")); item.put("tableId", reference.getTableId()); item.put("tableNo", reference.getTableNo()); item.put("tableTitle", StrUtil.blankToDefault(reference.getTableTitle(), "")); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerHistoryContext.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerHistoryContext.java index 7fffae751512ef4ff54c9332aa0dee7010ffdeb1..cdf9fd3aa6c8c1a9bab4f7f785d2179e3c73835e 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerHistoryContext.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerHistoryContext.java @@ -5,6 +5,9 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + /** * @program: 企业级别深度设计 AI Agent。添加 阿星不是程序员 微信,添加时备注 super 来获取项目的完整资料 * @description: 回答阶段最终使用的历史上下文 @@ -23,6 +26,11 @@ public class AnswerHistoryContext { private String recentContext; + @Builder.Default + private List evidenceAnchors = new ArrayList<>(); + + private String resolvedTopic; + private boolean followUpQuestion; private Integer totalBudget; @@ -32,6 +40,7 @@ public class AnswerHistoryContext { private Integer structuredBudget; public boolean isEmpty() { - return renderedText == null || renderedText.isBlank(); + return (renderedText == null || renderedText.isBlank()) + && (evidenceAnchors == null || evidenceAnchors.isEmpty()); } } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerPlan.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerPlan.java new file mode 100644 index 0000000000000000000000000000000000000000..7c23f6130b60f0d9d05b272c194c5c5a6c817672 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerPlan.java @@ -0,0 +1,28 @@ +package org.javaup.ai.chatagent.rag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AnswerPlan { + + @Builder.Default + private List requiredRoles = new ArrayList<>(); + + @Builder.Default + private List optionalRoles = new ArrayList<>(); + + private boolean requireExplicitEvidence; + + private boolean allowRoleFallback; + + private String instruction; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/CitationEvidenceType.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/CitationEvidenceType.java new file mode 100644 index 0000000000000000000000000000000000000000..37cc3b89a42f70f21834c29543a19f3259eb4431 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/CitationEvidenceType.java @@ -0,0 +1,9 @@ +package org.javaup.ai.chatagent.rag.model; + +public enum CitationEvidenceType { + CHUNK, + TABLE_CELL_OR_ROW, + KG_QUOTE_SOURCE, + RAPTOR_SOURCE_CHUNK, + CONTEXT_ONLY +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/ConversationExecutionPlan.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/ConversationExecutionPlan.java index 2528b9fc34c255644ff0e1905ff261e38dea9baa..3c05e37a28a4ca71088c1c4275060fa63c182601 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/ConversationExecutionPlan.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/ConversationExecutionPlan.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.javaup.enums.ChatQueryMode; +import org.javaup.enums.KnowledgeBaseSelectionMode; import java.time.LocalDate; import java.util.ArrayList; @@ -88,6 +89,20 @@ public class ConversationExecutionPlan { @Builder.Default private List retrievalTaskIds = new ArrayList<>(); + @Builder.Default + private KnowledgeBaseSelectionMode knowledgeBaseSelectionMode = KnowledgeBaseSelectionMode.NONE; + + @Builder.Default + private List selectedKnowledgeBaseIds = new ArrayList<>(); + + @Builder.Default + private List selectedKnowledgeBaseNames = new ArrayList<>(); + + @Builder.Default + private List allowedKnowledgeBaseDocumentIds = new ArrayList<>(); + + private RagRuntimeOptions ragRuntimeOptions; + private String clarificationReply; @Builder.Default diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/DocumentNavigationDecision.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/DocumentNavigationDecision.java index 061825c646894e0d7b30398303c471613a01d476..14fb64b6e10a7207bb48d55b1ed5ecda3dcc5bf2 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/DocumentNavigationDecision.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/DocumentNavigationDecision.java @@ -32,6 +32,8 @@ public class DocumentNavigationDecision { private QueryUnderstandingResult queryUnderstanding; + private StructureNavigationResult structureNavigationResult; + @Builder.Default private RetrievalIntent retrievalIntent = RetrievalIntent.GENERAL; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceAnchor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceAnchor.java new file mode 100644 index 0000000000000000000000000000000000000000..4de82850072715c402bec828cb879b2be342e699 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceAnchor.java @@ -0,0 +1,46 @@ +package org.javaup.ai.chatagent.rag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 上一轮最终引用形成的证据锚点,只用于追问指代和检索范围限定。 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EvidenceAnchor { + + private Long documentId; + + private String documentName; + + private Long taskId; + + private Long knowledgeBaseId; + + private String knowledgeBaseName; + + private Long structureNodeId; + + private String sectionPath; + + private String canonicalPath; + + private Integer itemIndex; + + private Long parentBlockId; + + private Long chunkId; + + private String sourceType; + + private String channel; + + private String snippet; + + private Double score; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceApplicabilityResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceApplicabilityResult.java new file mode 100644 index 0000000000000000000000000000000000000000..59589cd77e21efead35845d56a6f866b5ff06395 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceApplicabilityResult.java @@ -0,0 +1,47 @@ +package org.javaup.ai.chatagent.rag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EvidenceApplicabilityResult { + + public static final String APPLICABLE = "APPLICABLE"; + public static final String APPLICABLE_UNKNOWN = "APPLICABLE_UNKNOWN"; + public static final String NOT_APPLICABLE = "NOT_APPLICABLE"; + + private String status; + + private boolean applicable; + + private String reason; + + public static EvidenceApplicabilityResult applicable(String reason) { + return EvidenceApplicabilityResult.builder() + .status(APPLICABLE) + .applicable(true) + .reason(reason) + .build(); + } + + public static EvidenceApplicabilityResult unknown(String reason) { + return EvidenceApplicabilityResult.builder() + .status(APPLICABLE_UNKNOWN) + .applicable(true) + .reason(reason) + .build(); + } + + public static EvidenceApplicabilityResult notApplicable(String reason) { + return EvidenceApplicabilityResult.builder() + .status(NOT_APPLICABLE) + .applicable(false) + .reason(reason) + .build(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceIdentity.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceIdentity.java new file mode 100644 index 0000000000000000000000000000000000000000..88ba40952ebaa98ab4e7ab08336f5951b3e7dbad --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceIdentity.java @@ -0,0 +1,16 @@ +package org.javaup.ai.chatagent.rag.model; + +public record EvidenceIdentity(String value, CitationEvidenceType type, boolean citationCapable) { + + public static EvidenceIdentity citation(String value, CitationEvidenceType type) { + return new EvidenceIdentity(value, type, true); + } + + public static EvidenceIdentity context(String value) { + return new EvidenceIdentity(value, CitationEvidenceType.CONTEXT_ONLY, false); + } + + public boolean present() { + return value != null && !value.isBlank(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceRole.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceRole.java new file mode 100644 index 0000000000000000000000000000000000000000..ed35c88f5cecfc2c1bb77091e5063fed441a3e83 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceRole.java @@ -0,0 +1,15 @@ +package org.javaup.ai.chatagent.rag.model; + +public enum EvidenceRole { + SYMPTOM, + CAUSE, + HANDLING_STEP, + CHECK_ORDER, + THRESHOLD, + RESPONSIBILITY, + CONFIGURATION, + BOUNDARY, + RELATION, + SUMMARY, + GENERAL +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/QueryUnderstandingResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/QueryUnderstandingResult.java index df6998464409a00f4fc00953fe6c2664db8de1b2..433303db0556a9cfff068d22b52463287a8f6757 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/QueryUnderstandingResult.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/QueryUnderstandingResult.java @@ -26,14 +26,27 @@ public class QueryUnderstandingResult { @Builder.Default private List entities = new ArrayList<>(); + @Builder.Default + private List targetEntities = new ArrayList<>(); + + @Builder.Default + private List excludedEntities = new ArrayList<>(); + @Builder.Default private List sectionAnchors = new ArrayList<>(); + private StructureNavigationIntent structureNavigationIntent; + + @Builder.Default + private List expectedEvidenceRoles = new ArrayList<>(); + @Builder.Default private List tableOps = new ArrayList<>(); private boolean negativeBoundary; + private String answerExpectation; + private double confidence; @Builder.Default diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/RagRuntimeOptions.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/RagRuntimeOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..de22bb171c08a96a40452691aeee570ca8b31d62 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/RagRuntimeOptions.java @@ -0,0 +1,123 @@ +package org.javaup.ai.chatagent.rag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.javaup.ai.chatagent.rag.config.ChatRagProperties; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RagRuntimeOptions { + + private int vectorTopK; + + private int keywordTopK; + + private int graphRagTopK; + + private int graphRagMaxHops; + + private int raptorTopK; + + private int raptorSourceChunkTopK; + + private int candidateTopK; + + private int rerankCandidateTopK; + + private int reserveCandidateTopK; + + private int finalTopK; + + private double minVectorSimilarity; + + private double keywordRelativeScoreFloor; + + private boolean keywordChannelEnabled; + + private boolean tableChannelEnabled; + + private boolean graphRagChannelEnabled; + + private boolean raptorChannelEnabled; + + private HybridOptions hybrid; + + @Builder.Default + private List kbConfigConflictFields = new ArrayList<>(); + + public static RagRuntimeOptions from(ChatRagProperties properties) { + ChatRagProperties.HybridProperties hybridProperties = properties == null ? null : properties.getHybrid(); + return RagRuntimeOptions.builder() + .vectorTopK(properties == null ? 10 : properties.getVectorTopK()) + .keywordTopK(properties == null ? 10 : properties.getKeywordTopK()) + .graphRagTopK(properties == null ? 5 : properties.getGraphRagTopK()) + .graphRagMaxHops(properties == null ? 2 : properties.getGraphRagMaxHops()) + .raptorTopK(properties == null ? 5 : properties.getRaptorTopK()) + .raptorSourceChunkTopK(properties == null ? 3 : properties.getRaptorSourceChunkTopK()) + .candidateTopK(properties == null ? 40 : properties.getCandidateTopK()) + .rerankCandidateTopK(properties == null ? 24 : properties.getRerankCandidateTopK()) + .reserveCandidateTopK(properties == null ? 30 : properties.getReserveCandidateTopK()) + .finalTopK(properties == null ? 6 : properties.getFinalTopK()) + .minVectorSimilarity(properties == null ? 0.45D : properties.getMinVectorSimilarity()) + .keywordRelativeScoreFloor(properties == null ? 0.35D : properties.getKeywordRelativeScoreFloor()) + .keywordChannelEnabled(properties == null || properties.isKeywordChannelEnabled()) + .tableChannelEnabled(properties == null || properties.isTableChannelEnabled()) + .graphRagChannelEnabled(properties == null || properties.isGraphRagChannelEnabled()) + .raptorChannelEnabled(properties == null || properties.isRaptorChannelEnabled()) + .hybrid(HybridOptions.from(hybridProperties)) + .kbConfigConflictFields(new ArrayList<>()) + .build(); + } + + public static RagRuntimeOptions resolve(ConversationExecutionPlan plan, ChatRagProperties properties) { + return plan == null || plan.getRagRuntimeOptions() == null + ? from(properties) + : plan.getRagRuntimeOptions(); + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class HybridOptions { + + private double vectorWeight; + + private double keywordWeight; + + private double tableWeight; + + private double graphRagWeight; + + private double raptorWeight; + + private double rankWeight; + + private double originalScoreWeight; + + private double metadataBoostWeight; + + private double maxMetadataBoost; + + public static HybridOptions from(ChatRagProperties.HybridProperties properties) { + return HybridOptions.builder() + .vectorWeight(properties == null ? 1.0D : properties.getVectorWeight()) + .keywordWeight(properties == null ? 1.0D : properties.getKeywordWeight()) + .tableWeight(properties == null ? 1.2D : properties.getTableWeight()) + .graphRagWeight(properties == null ? 1.1D : properties.getGraphRagWeight()) + .raptorWeight(properties == null ? 1.05D : properties.getRaptorWeight()) + .rankWeight(properties == null ? 1.0D : properties.getRankWeight()) + .originalScoreWeight(properties == null ? 0.08D : properties.getOriginalScoreWeight()) + .metadataBoostWeight(properties == null ? 0.04D : properties.getMetadataBoostWeight()) + .maxMetadataBoost(properties == null ? 1.0D : properties.getMaxMetadataBoost()) + .build(); + } + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationIntent.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationIntent.java new file mode 100644 index 0000000000000000000000000000000000000000..ecd161372ac03d27980086b56299095a49565a2a --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationIntent.java @@ -0,0 +1,32 @@ +package org.javaup.ai.chatagent.rag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StructureNavigationIntent { + + @Builder.Default + private List operations = new ArrayList<>(); + + private Long anchorStructureNodeId; + + private String anchorSectionPath; + + private String anchorCanonicalPath; + + @Builder.Default + private List sectionAnchors = new ArrayList<>(); + + private double confidence; + + private String source; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationOperation.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationOperation.java new file mode 100644 index 0000000000000000000000000000000000000000..a7f8d1090d3e377c4314485a114a4d618cd091ad --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationOperation.java @@ -0,0 +1,18 @@ +package org.javaup.ai.chatagent.rag.model; + +public enum StructureNavigationOperation { + + CURRENT_SECTION, + + PARENT_SECTION, + + PREVIOUS_SIBLING, + + NEXT_SIBLING, + + DIRECT_CHILDREN, + + SECTION_WITH_SIBLINGS, + + SECTION_WITH_CHILDREN +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationResult.java new file mode 100644 index 0000000000000000000000000000000000000000..79c9e8c7a52fbe40d77dafd82686fdc6cef835e4 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationResult.java @@ -0,0 +1,36 @@ +package org.javaup.ai.chatagent.rag.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StructureNavigationResult { + + private Long documentId; + + private Long anchorNodeId; + + private SuperAgentDocumentStructureNode current; + + private SuperAgentDocumentStructureNode parent; + + private SuperAgentDocumentStructureNode previousSibling; + + private SuperAgentDocumentStructureNode nextSibling; + + @Builder.Default + private List directChildren = new ArrayList<>(); + + private boolean deterministic; + + private String missReason; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannel.java index ae646ec203ea416b1d0563e848b9332d059fe954..22329e8d35ec45cea28b3d8d7d1c8aa9ccacd22a 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannel.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannel.java @@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.rag.retrieve.channel; import cn.hutool.core.util.StrUtil; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; import org.javaup.ai.chatagent.rag.service.DocumentRetrieveRequestFactory; import org.javaup.ai.manage.model.DocumentRetrieveRequest; import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; @@ -52,33 +53,37 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { @Override public boolean supports(ConversationExecutionPlan plan) { return plan != null - && properties.isGraphRagChannelEnabled() + && RagRuntimeOptions.resolve(plan, properties).isGraphRagChannelEnabled() && !resolvedDocumentIds(plan).isEmpty(); } @Override public RetrievalChannelResult retrieve(String subQuestion, ConversationExecutionPlan plan) { - DocumentRetrieveRequest request = documentRetrieveRequestFactory.build(subQuestion, plan, properties.getGraphRagTopK()); + RagRuntimeOptions options = RagRuntimeOptions.resolve(plan, properties); + DocumentRetrieveRequest request = documentRetrieveRequestFactory.build(subQuestion, plan, options.getGraphRagTopK()); List results = graphRagSearchService.search( StrUtil.blankToDefault(request.getRetrievalQuery(), subQuestion), request.resolvedDocumentIds(), request.resolvedTaskIds(), - properties.getGraphRagTopK(), - properties.getGraphRagMaxHops() + options.getGraphRagTopK(), + options.getGraphRagMaxHops() ); if (results.isEmpty()) { return new RetrievalChannelResult(channelName(), List.of()); } - Map documentNames = resolveDocumentNames(); + Map documentDescriptors = resolveDocumentDescriptors(plan); List documents = results.stream() - .map(result -> toDocument(subQuestion, result, documentNames)) + .map(result -> toDocument(subQuestion, result, documentDescriptors)) .toList(); return new RetrievalChannelResult(channelName(), documents); } - private Document toDocument(String subQuestion, GraphRagSearchResult result, Map documentNames) { - String documentName = StrUtil.blankToDefault(documentNames.get(result.getDocumentId()), "文档图谱"); + private Document toDocument(String subQuestion, + GraphRagSearchResult result, + Map documentDescriptors) { + KnowledgeDocumentDescriptor descriptor = documentDescriptors.get(result.getDocumentId()); + String documentName = StrUtil.blankToDefault(descriptor == null ? null : descriptor.getDocumentName(), "文档图谱"); String text = renderEvidenceText(subQuestion, result); Map metadata = new LinkedHashMap<>(); metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, SOURCE_TYPE); @@ -86,6 +91,10 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { metadata.put(DocumentKnowledgeMetadataKeys.SCORE, result.getScore()); putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.DOCUMENT_ID, result.getDocumentId()); metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, documentName); + if (descriptor != null) { + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId()); + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, StrUtil.blankToDefault(descriptor.getKnowledgeBaseName(), "")); + } putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.TASK_ID, result.getTaskId()); if (!isCommunityReportResult(result)) { putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, result.getParentBlockId()); @@ -113,6 +122,7 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_EVIDENCE_COUNT, result.getRelationGroupEvidenceCount()); putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_DOCUMENT_COUNT, result.getRelationGroupDocumentCount()); putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID, result.getEvidenceId()); + metadata.put(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL, groundingLevel(result)); metadata.put(DocumentKnowledgeMetadataKeys.KG_GRAPH_PATH, StrUtil.blankToDefault(result.getGraphPath(), "")); putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_HOP_COUNT, result.getHopCount()); metadata.put(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE, StrUtil.blankToDefault(result.getQueryPlanSource(), "")); @@ -124,6 +134,7 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_COMMUNITY_ID, result.getCommunityId()); metadata.put(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_TITLE, StrUtil.blankToDefault(result.getCommunityTitle(), "")); metadata.put(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY, StrUtil.blankToDefault(result.getCommunitySummary(), "")); + metadata.put(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY, isCommunitySummaryOnly(result)); metadata.put(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY, StrUtil.blankToDefault(result.getCrossDocumentCommunityKey(), "")); putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_ENTITY_COUNT, result.getCrossDocumentCommunityEntityCount()); putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_RELATION_GROUP_COUNT, result.getCrossDocumentCommunityRelationGroupCount()); @@ -162,6 +173,9 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { } if (StrUtil.isNotBlank(result.getCommunitySummary())) { builder.append("社区报告:").append(result.getCommunitySummary()).append('\n'); + if (isCommunitySummaryOnly(result)) { + builder.append("社区报告边界:该候选缺少可回到原文 quote 的 KG evidence,只能作为背景线索,不能单独支撑具体事实结论。\n"); + } } if (StrUtil.isNotBlank(result.getNHopPath())) { builder.append("n-hop路径:").append(result.getNHopPath()).append('\n'); @@ -190,6 +204,42 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { return builder.toString().trim(); } + private String groundingLevel(GraphRagSearchResult result) { + if (result == null) { + return "NONE"; + } + boolean hasSourceQuote = hasSourceQuoteEvidence(result); + if (result.getRelationId() != null) { + if (!hasSourceQuote) { + return "RELATION_NO_QUOTE"; + } + String relationType = StrUtil.blankToDefault(result.getRelationType(), "") + .trim() + .toUpperCase(); + if ("RECORDS".equals(relationType) || "ASSOCIATED_WITH".equals(relationType) || "RELATED_TO".equals(relationType)) { + return "RELATION_WEAK_QUOTE"; + } + return "RELATION_STRONG_QUOTE"; + } + if (result.getEntityId() != null) { + return hasSourceQuote ? "ENTITY_QUOTE" : "ENTITY_NO_QUOTE"; + } + if (isCommunityReportResult(result)) { + return hasSourceQuote ? "COMMUNITY_SOURCE_QUOTE" : "COMMUNITY_SUMMARY_ONLY"; + } + return hasSourceQuote ? "SOURCE_QUOTE" : "NONE"; + } + + private boolean isCommunitySummaryOnly(GraphRagSearchResult result) { + return isCommunityReportResult(result) && !hasSourceQuoteEvidence(result); + } + + private boolean hasSourceQuoteEvidence(GraphRagSearchResult result) { + return result != null + && result.getEvidenceId() != null + && StrUtil.isNotBlank(result.getQuoteText()); + } + private Set resolveRelationTypes(GraphRagSearchResult result) { LinkedHashSet relationTypes = new LinkedHashSet<>(); addRelationType(relationTypes, result.getRelationType()); @@ -249,14 +299,21 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { return plan.getSelectedTaskId() == null ? List.of() : List.of(plan.getSelectedTaskId()); } - private Map resolveDocumentNames() { - Map documentNames = new LinkedHashMap<>(); - for (KnowledgeDocumentDescriptor descriptor : documentKnowledgeService.listRetrievableDocuments()) { + private Map resolveDocumentDescriptors(ConversationExecutionPlan plan) { + Map documentDescriptors = new LinkedHashMap<>(); + List documentIds = resolvedDocumentIds(plan); + List descriptors = plan == null + || plan.getSelectedKnowledgeBaseIds() == null + || plan.getSelectedKnowledgeBaseIds().isEmpty() + ? documentKnowledgeService.listRetrievableDocuments() + : documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(plan.getSelectedKnowledgeBaseIds()); + for (KnowledgeDocumentDescriptor descriptor : descriptors) { if (descriptor.getDocumentId() != null) { - documentNames.put(descriptor.getDocumentId(), descriptor.getDocumentName()); + documentDescriptors.put(descriptor.getDocumentId(), descriptor); } } - return documentNames; + documentDescriptors.keySet().retainAll(documentIds); + return documentDescriptors; } private void putIfNotNull(Map metadata, String key, Object value) { @@ -268,10 +325,10 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { private String documentId(GraphRagSearchResult result) { if (isCommunityReportResult(result)) { if (StrUtil.isNotBlank(result.getCrossDocumentCommunityKey())) { - return "graphrag-xcommunity-" + stableIdPart(result.getCrossDocumentCommunityKey()) + "-evidence-" + result.getEvidenceId(); + return "graphrag-xcommunity-" + stableIdPart(result.getCrossDocumentCommunityKey()) + "-evidence-" + stableEvidenceIdPart(result); } if (result.getCommunityId() != null) { - return "graphrag-community-" + result.getCommunityId() + "-evidence-" + result.getEvidenceId(); + return "graphrag-community-" + result.getCommunityId() + "-evidence-" + stableEvidenceIdPart(result); } } if (result.getEvidenceId() != null) { @@ -306,4 +363,11 @@ public class GraphRagRetrievalChannel implements RetrievalChannel { } return normalized.length() <= 80 ? normalized : normalized.substring(0, 80); } + + private String stableEvidenceIdPart(GraphRagSearchResult result) { + if (result != null && result.getEvidenceId() != null) { + return String.valueOf(result.getEvidenceId()); + } + return "summary"; + } } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/KeywordRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/KeywordRetrievalChannel.java index 7a5433daf875a8c96e7d29a6683326d279d3741a..806e8e2d0350b7c188944d78ccf6e66112556e97 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/KeywordRetrievalChannel.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/KeywordRetrievalChannel.java @@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.rag.retrieve.channel; import cn.hutool.core.collection.CollectionUtil; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; import org.javaup.ai.chatagent.rag.service.DocumentRetrieveRequestFactory; import org.javaup.ai.manage.service.DocumentKnowledgeService; import org.javaup.enums.RetrievalChannelEnum; @@ -40,14 +41,14 @@ public class KeywordRetrievalChannel implements RetrievalChannel { @Override public boolean supports(ConversationExecutionPlan plan) { - return properties.isKeywordChannelEnabled() + return RagRuntimeOptions.resolve(plan, properties).isKeywordChannelEnabled() && hasDocumentScope(plan); } @Override public RetrievalChannelResult retrieve(String subQuestion, ConversationExecutionPlan plan) { List documentList = documentKnowledgeService.keywordSearch( - documentRetrieveRequestFactory.build(subQuestion, plan, properties.getKeywordTopK()) + documentRetrieveRequestFactory.build(subQuestion, plan, RagRuntimeOptions.resolve(plan, properties).getKeywordTopK()) ); return new RetrievalChannelResult( diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannel.java index e6f06797b667dbe7bae2487ccdaefc9aa4d2f260..9b134ad5b1336b018aeb98e33c990bb1dafca074 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannel.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannel.java @@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.rag.retrieve.channel; import cn.hutool.core.util.StrUtil; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; import org.javaup.ai.manage.model.raptor.RaptorSearchResult; import org.javaup.ai.manage.service.DocumentKnowledgeService; @@ -20,6 +21,9 @@ import java.util.Map; public class RaptorRetrievalChannel implements RetrievalChannel { private static final String SOURCE_TYPE = "RAPTOR"; + private static final String SOURCE_STATUS_SOURCE_CHUNK = "SOURCE_CHUNK"; + private static final String SOURCE_STATUS_SOURCE_PARENT_BLOCK = "SOURCE_PARENT_BLOCK"; + private static final String SOURCE_STATUS_SUMMARY_ONLY = "SUMMARY_ONLY"; private final RaptorSearchService raptorSearchService; private final DocumentKnowledgeService documentKnowledgeService; @@ -41,58 +45,70 @@ public class RaptorRetrievalChannel implements RetrievalChannel { @Override public boolean supports(ConversationExecutionPlan plan) { return plan != null - && properties.isRaptorChannelEnabled() + && RagRuntimeOptions.resolve(plan, properties).isRaptorChannelEnabled() && !resolvedDocumentIds(plan).isEmpty(); } @Override public RetrievalChannelResult retrieve(String subQuestion, ConversationExecutionPlan plan) { + RagRuntimeOptions options = RagRuntimeOptions.resolve(plan, properties); List results = raptorSearchService.search( subQuestion, resolvedDocumentIds(plan), resolvedTaskIds(plan), - properties.getRaptorTopK(), - properties.getRaptorSourceChunkTopK() + options.getRaptorTopK(), + options.getRaptorSourceChunkTopK() ); if (results.isEmpty()) { return new RetrievalChannelResult(channelName(), List.of()); } - Map documentNames = resolveDocumentNames(); + Map documentDescriptors = resolveDocumentDescriptors(plan); List documents = results.stream() - .map(result -> toDocument(subQuestion, result, documentNames)) + .map(result -> toDocument(subQuestion, result, documentDescriptors)) .toList(); return new RetrievalChannelResult(channelName(), documents); } - private Document toDocument(String subQuestion, RaptorSearchResult result, Map documentNames) { - String documentName = StrUtil.blankToDefault(documentNames.get(result.getDocumentId()), "文档摘要树"); + private Document toDocument(String subQuestion, + RaptorSearchResult result, + Map documentDescriptors) { + KnowledgeDocumentDescriptor descriptor = documentDescriptors.get(result.getDocumentId()); + String documentName = StrUtil.blankToDefault(descriptor == null ? null : descriptor.getDocumentName(), "文档摘要树"); String text = renderEvidenceText(subQuestion, result); + String sourceStatus = resolveSourceStatus(result); Map metadata = new LinkedHashMap<>(); metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, SOURCE_TYPE); metadata.put(DocumentKnowledgeMetadataKeys.CHANNEL, channelName()); metadata.put(DocumentKnowledgeMetadataKeys.SCORE, result.getScore()); metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, result.getDocumentId()); metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, documentName); + if (descriptor != null) { + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId()); + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, StrUtil.blankToDefault(descriptor.getKnowledgeBaseName(), "")); + } metadata.put(DocumentKnowledgeMetadataKeys.TASK_ID, result.getTaskId()); - metadata.put(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, result.getParentBlockId()); - metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_ID, result.getChunkId()); - metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_NO, result.getChunkNo()); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, result.getParentBlockId()); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.CHUNK_ID, result.getChunkId()); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.CHUNK_NO, result.getChunkNo()); metadata.put(DocumentKnowledgeMetadataKeys.SECTION_PATH, StrUtil.blankToDefault(result.getSectionPath(), "")); putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PAGE_NO, result.getPageNo()); metadata.put(DocumentKnowledgeMetadataKeys.PAGE_RANGE, StrUtil.blankToDefault(result.getPageRange(), "")); metadata.put(DocumentKnowledgeMetadataKeys.BBOX_JSON, StrUtil.blankToDefault(result.getBboxJson(), "")); metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_BLOCK_IDS, StrUtil.blankToDefault(result.getSourceBlockIds(), "")); - metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "RAPTOR_SOURCE_CHUNK"); + metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, + SOURCE_STATUS_SUMMARY_ONLY.equals(sourceStatus) ? "RAPTOR_SUMMARY" : "RAPTOR_SOURCE_CHUNK"); metadata.put(DocumentKnowledgeMetadataKeys.TITLE, StrUtil.blankToDefault(result.getTitle(), result.getRaptorNodeTitle())); - metadata.put(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET, StrUtil.blankToDefault(result.getChunkText(), "")); + metadata.put(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET, + StrUtil.blankToDefault(result.getChunkText(), StrUtil.blankToDefault(result.getRaptorSummary(), ""))); metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID, result.getRaptorNodeId()); metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_TITLE, StrUtil.blankToDefault(result.getRaptorNodeTitle(), "")); metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_LEVEL, result.getRaptorNodeLevel()); metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_SUMMARY, StrUtil.blankToDefault(result.getRaptorSummary(), "")); + metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS, sourceStatus); return Document.builder() - .id("raptor-" + result.getRaptorNodeId() + "-" + result.getChunkId()) + .id(raptorDocumentId(result)) .text(text) .metadata(metadata) .score(result.getScore()) @@ -112,10 +128,43 @@ public class RaptorRetrievalChannel implements RetrievalChannel { if (result.getPageNo() != null) { builder.append("原文页码:").append(result.getPageNo()).append('\n'); } - builder.append("下钻原文:").append(StrUtil.blankToDefault(result.getChunkText(), "")).append('\n'); + String sourceStatus = resolveSourceStatus(result); + if (SOURCE_STATUS_SUMMARY_ONLY.equals(sourceStatus)) { + builder.append("下钻状态:未找到可引用 source chunk 或 ParentBlock,本证据仅作为摘要背景。\n"); + } + else if (SOURCE_STATUS_SOURCE_PARENT_BLOCK.equals(sourceStatus) && StrUtil.isBlank(result.getChunkText())) { + builder.append("下钻状态:已定位到 ParentBlock,但当前结果未携带 chunk 原文。\n"); + } + if (StrUtil.isNotBlank(result.getChunkText())) { + builder.append("下钻原文:").append(result.getChunkText()).append('\n'); + } return builder.toString().trim(); } + private String resolveSourceStatus(RaptorSearchResult result) { + String sourceStatus = StrUtil.blankToDefault(result.getSourceStatus(), ""); + if (StrUtil.isNotBlank(sourceStatus)) { + return sourceStatus; + } + if (result.getChunkId() != null) { + return SOURCE_STATUS_SOURCE_CHUNK; + } + if (result.getParentBlockId() != null) { + return SOURCE_STATUS_SOURCE_PARENT_BLOCK; + } + return SOURCE_STATUS_SUMMARY_ONLY; + } + + private String raptorDocumentId(RaptorSearchResult result) { + if (result.getChunkId() != null) { + return "raptor-" + result.getRaptorNodeId() + "-" + result.getChunkId(); + } + if (result.getParentBlockId() != null) { + return "raptor-" + result.getRaptorNodeId() + "-parent-" + result.getParentBlockId(); + } + return "raptor-" + result.getRaptorNodeId() + "-summary"; + } + private List resolvedDocumentIds(ConversationExecutionPlan plan) { if (plan.getRetrievalDocumentIds() != null && !plan.getRetrievalDocumentIds().isEmpty()) { return plan.getRetrievalDocumentIds(); @@ -130,14 +179,21 @@ public class RaptorRetrievalChannel implements RetrievalChannel { return plan.getSelectedTaskId() == null ? List.of() : List.of(plan.getSelectedTaskId()); } - private Map resolveDocumentNames() { - Map documentNames = new LinkedHashMap<>(); - for (KnowledgeDocumentDescriptor descriptor : documentKnowledgeService.listRetrievableDocuments()) { + private Map resolveDocumentDescriptors(ConversationExecutionPlan plan) { + Map documentDescriptors = new LinkedHashMap<>(); + List documentIds = resolvedDocumentIds(plan); + List descriptors = plan == null + || plan.getSelectedKnowledgeBaseIds() == null + || plan.getSelectedKnowledgeBaseIds().isEmpty() + ? documentKnowledgeService.listRetrievableDocuments() + : documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(plan.getSelectedKnowledgeBaseIds()); + for (KnowledgeDocumentDescriptor descriptor : descriptors) { if (descriptor.getDocumentId() != null) { - documentNames.put(descriptor.getDocumentId(), descriptor.getDocumentName()); + documentDescriptors.put(descriptor.getDocumentId(), descriptor); } } - return documentNames; + documentDescriptors.keySet().retainAll(documentIds); + return documentDescriptors; } private void putIfNotNull(Map metadata, String key, Object value) { diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannel.java index 9aebd1492f0ebdd53d65b0deba469db22ee24c14..b157a86e2dad814cbe96e8fabfcbe4f6db58cfdf 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannel.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannel.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; import org.javaup.ai.chatagent.rag.model.DocumentTableQueryPlan; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; import org.javaup.ai.chatagent.rag.service.DocumentTableQueryPlanner; import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; import org.javaup.ai.manage.model.table.DocumentTableDescriptor; @@ -50,7 +51,7 @@ public class TableRetrievalChannel implements RetrievalChannel { @Override public boolean supports(ConversationExecutionPlan plan) { return plan != null - && properties.isTableChannelEnabled() + && RagRuntimeOptions.resolve(plan, properties).isTableChannelEnabled() && !resolvedDocumentIds(plan).isEmpty(); } @@ -67,15 +68,16 @@ public class TableRetrievalChannel implements RetrievalChannel { DocumentTableQueryPlan planned = queryPlan.get(); DocumentTableQueryResult result = tableStructureService.query(planned.getQuery()); - Document document = buildEvidenceDocument(subQuestion, planned, result, resolveDocumentNames()); + Document document = buildEvidenceDocument(subQuestion, planned, result, resolveDocumentDescriptors(plan)); return new RetrievalChannelResult(channelName(), List.of(document)); } private Document buildEvidenceDocument(String subQuestion, DocumentTableQueryPlan queryPlan, DocumentTableQueryResult result, - Map documentNames) { - String documentName = StrUtil.blankToDefault(documentNames.get(result.getDocumentId()), "文档表格"); + Map documentDescriptors) { + KnowledgeDocumentDescriptor descriptor = documentDescriptors.get(result.getDocumentId()); + String documentName = StrUtil.blankToDefault(descriptor == null ? null : descriptor.getDocumentName(), "文档表格"); String text = renderEvidenceText(subQuestion, queryPlan, result); Map metadata = new LinkedHashMap<>(); metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, SOURCE_TYPE); @@ -83,6 +85,10 @@ public class TableRetrievalChannel implements RetrievalChannel { metadata.put(DocumentKnowledgeMetadataKeys.SCORE, TABLE_QUERY_SCORE); metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, result.getDocumentId()); metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, documentName); + if (descriptor != null) { + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId()); + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, StrUtil.blankToDefault(descriptor.getKnowledgeBaseName(), "")); + } metadata.put(DocumentKnowledgeMetadataKeys.TASK_ID, result.getTaskId()); metadata.put(DocumentKnowledgeMetadataKeys.SECTION_PATH, StrUtil.blankToDefault(result.getSectionPath(), "")); putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PAGE_NO, result.getPageNo()); @@ -165,14 +171,21 @@ public class TableRetrievalChannel implements RetrievalChannel { return plan.getSelectedTaskId() == null ? List.of() : List.of(plan.getSelectedTaskId()); } - private Map resolveDocumentNames() { - Map documentNames = new LinkedHashMap<>(); - for (KnowledgeDocumentDescriptor descriptor : documentKnowledgeService.listRetrievableDocuments()) { + private Map resolveDocumentDescriptors(ConversationExecutionPlan plan) { + Map documentDescriptors = new LinkedHashMap<>(); + List documentIds = resolvedDocumentIds(plan); + List descriptors = plan == null + || plan.getSelectedKnowledgeBaseIds() == null + || plan.getSelectedKnowledgeBaseIds().isEmpty() + ? documentKnowledgeService.listRetrievableDocuments() + : documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(plan.getSelectedKnowledgeBaseIds()); + for (KnowledgeDocumentDescriptor descriptor : descriptors) { if (descriptor.getDocumentId() != null) { - documentNames.put(descriptor.getDocumentId(), descriptor.getDocumentName()); + documentDescriptors.put(descriptor.getDocumentId(), descriptor); } } - return documentNames; + documentDescriptors.keySet().retainAll(documentIds); + return documentDescriptors; } private void putIfNotNull(Map metadata, String key, Object value) { diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/VectorRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/VectorRetrievalChannel.java index 7c3adfdf2725f086d56657cd35ec2a10388a206d..fa4e83eccd7fac210f5b51ace05949af7575d383 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/VectorRetrievalChannel.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/VectorRetrievalChannel.java @@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.rag.retrieve.channel; import cn.hutool.core.collection.CollectionUtil; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; import org.javaup.ai.chatagent.rag.service.DocumentRetrieveRequestFactory; import org.javaup.ai.manage.service.DocumentKnowledgeService; import org.javaup.enums.RetrievalChannelEnum; @@ -47,7 +48,7 @@ public class VectorRetrievalChannel implements RetrievalChannel { public RetrievalChannelResult retrieve(String subQuestion, ConversationExecutionPlan plan) { List documentList = documentKnowledgeService.vectorSearch( - documentRetrieveRequestFactory.build(subQuestion, plan, properties.getVectorTopK()) + documentRetrieveRequestFactory.build(subQuestion, plan, RagRuntimeOptions.resolve(plan, properties).getVectorTopK()) ); return new RetrievalChannelResult( channelName(), documentList diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssembler.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssembler.java index acad7a0c72e80036203e3a2747bfbf6c87b55276..ef50b5f7a3707378647de014a612700dd12b254c 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssembler.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssembler.java @@ -3,10 +3,13 @@ package org.javaup.ai.chatagent.rag.service; import cn.hutool.core.util.StrUtil; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.AnswerHistoryContext; +import org.javaup.ai.chatagent.rag.model.EvidenceAnchor; import org.javaup.ai.chatagent.rag.model.QueryType; import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; import org.springframework.stereotype.Service; +import java.util.List; + /** * @program: 企业级别深度设计 AI Agent。添加 阿星不是程序员 微信,添加时备注 super 来获取项目的完整资料 * @description: 服务层 @@ -29,28 +32,40 @@ public class AnswerHistoryContextAssembler { public AnswerHistoryContext assemble(String question, String answerRecentTranscript, QueryUnderstandingResult queryUnderstanding) { + return assemble(question, answerRecentTranscript, queryUnderstanding, List.of()); + } + + public AnswerHistoryContext assemble(String question, + String answerRecentTranscript, + QueryUnderstandingResult queryUnderstanding, + List recentEvidenceAnchors) { String normalizedQuestion = safeText(question); String recentUserContext = extractRecentUserQuestions(answerRecentTranscript); int totalBudget = Math.max(1, properties.getAnswerHistoryMaxChars()); boolean hasRecentContext = StrUtil.isNotBlank(recentUserContext); - boolean followUpQuestion = looksLikeFollowUpQuestion(normalizedQuestion, hasRecentContext, queryUnderstanding); + boolean followUpQuestion = looksLikeFollowUpQuestion(normalizedQuestion, queryUnderstanding); + List anchors = safeAnchors(recentEvidenceAnchors); - if (!followUpQuestion || !hasRecentContext) { + if (!followUpQuestion || (!hasRecentContext && anchors.isEmpty())) { return emptyContext(totalBudget, followUpQuestion); } String recentPart = renderRecentContext(recentUserContext, totalBudget); - if (recentPart.isBlank()) { + String structuredPart = renderStructuredContext(anchors, totalBudget - recentPart.length()); + String renderedText = joinNonBlank(structuredPart, recentPart); + if (renderedText.isBlank() && anchors.isEmpty()) { return emptyContext(totalBudget, followUpQuestion); } return AnswerHistoryContext.builder() - .renderedText(recentPart) - .structuredContext("") + .renderedText(renderedText) + .structuredContext(structuredPart) .recentContext(recentPart) + .evidenceAnchors(anchors) + .resolvedTopic(resolveTopic(anchors)) .followUpQuestion(followUpQuestion) .totalBudget(totalBudget) - .recentBudget(totalBudget) - .structuredBudget(0) + .recentBudget(recentPart.length()) + .structuredBudget(structuredPart.length()) .build(); } @@ -59,6 +74,8 @@ public class AnswerHistoryContextAssembler { .renderedText("") .structuredContext("") .recentContext("") + .evidenceAnchors(List.of()) + .resolvedTopic("") .followUpQuestion(followUpQuestion) .totalBudget(totalBudget) .recentBudget(0) @@ -89,9 +106,8 @@ public class AnswerHistoryContextAssembler { } private boolean looksLikeFollowUpQuestion(String normalizedQuestion, - boolean hasRecentContext, QueryUnderstandingResult queryUnderstanding) { - if (!hasRecentContext || StrUtil.isBlank(normalizedQuestion)) { + if (StrUtil.isBlank(normalizedQuestion)) { return false; } QueryType queryType = queryUnderstanding == null || queryUnderstanding.getQueryType() == null @@ -103,6 +119,62 @@ public class AnswerHistoryContextAssembler { return false; } + private List safeAnchors(List anchors) { + if (anchors == null || anchors.isEmpty()) { + return List.of(); + } + return anchors.stream() + .filter(anchor -> anchor != null && hasAnchorIdentity(anchor)) + .limit(5) + .toList(); + } + + private boolean hasAnchorIdentity(EvidenceAnchor anchor) { + return anchor.getDocumentId() != null + || anchor.getStructureNodeId() != null + || anchor.getParentBlockId() != null + || anchor.getChunkId() != null + || StrUtil.isNotBlank(anchor.getSectionPath()); + } + + private String renderStructuredContext(List anchors, int budget) { + if (anchors == null || anchors.isEmpty() || budget <= 0) { + return ""; + } + StringBuilder builder = new StringBuilder("上一轮可继承证据锚点(仅用于解析指代和限定范围,不作为事实证据):\n"); + for (EvidenceAnchor anchor : anchors) { + if (anchor == null) { + continue; + } + builder.append("- 文档: ").append(blankToDash(anchor.getDocumentName())).append('\n'); + appendAnchorField(builder, " 章节", anchor.getSectionPath()); + appendAnchorField(builder, " canonicalPath", anchor.getCanonicalPath()); + appendAnchorField(builder, " structureNodeId", anchor.getStructureNodeId()); + appendAnchorField(builder, " parentBlockId", anchor.getParentBlockId()); + appendAnchorField(builder, " chunkId", anchor.getChunkId()); + appendAnchorField(builder, " itemIndex", anchor.getItemIndex()); + String snippet = clipHead(anchor.getSnippet(), 300); + appendAnchorField(builder, " snippet", snippet); + } + return clipHead(builder.toString().trim(), budget); + } + + private void appendAnchorField(StringBuilder builder, String name, Object value) { + String text = value == null ? "" : String.valueOf(value).trim(); + if (text.isBlank()) { + return; + } + builder.append(name).append(": ").append(text).append('\n'); + } + + private String resolveTopic(List anchors) { + if (anchors == null || anchors.isEmpty()) { + return ""; + } + EvidenceAnchor anchor = anchors.get(0); + return StrUtil.blankToDefault(anchor.getSectionPath(), StrUtil.blankToDefault(anchor.getDocumentName(), "")); + } + private String renderRecentContext(String recentUserContext, int budget) { if (budget <= 0 || StrUtil.isBlank(recentUserContext)) { return ""; @@ -118,6 +190,29 @@ public class AnswerHistoryContextAssembler { return title + body; } + private String joinNonBlank(String first, String second) { + String left = safeText(first); + String right = safeText(second); + if (left.isBlank()) { + return right; + } + if (right.isBlank()) { + return left; + } + return left + "\n" + right; + } + + private String clipHead(String text, int maxChars) { + String normalized = safeText(text); + if (normalized.length() <= maxChars) { + return normalized; + } + if (maxChars <= 1) { + return ""; + } + return normalized.substring(0, maxChars - 1) + "…"; + } + private String clipTail(String text, int maxChars) { String normalized = safeText(text); if (normalized.length() <= maxChars) { @@ -130,6 +225,10 @@ public class AnswerHistoryContextAssembler { return "…" + normalized.substring(start); } + private String blankToDash(String text) { + return StrUtil.blankToDefault(text, "-"); + } + private String safeText(String text) { return text == null ? "" : text.trim(); } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerPlanService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerPlanService.java new file mode 100644 index 0000000000000000000000000000000000000000..d99bc338d06bce5738f22d31f52c52aa6a812b15 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerPlanService.java @@ -0,0 +1,61 @@ +package org.javaup.ai.chatagent.rag.service; + +import cn.hutool.core.util.StrUtil; +import org.javaup.ai.chatagent.rag.model.AnswerPlan; +import org.javaup.ai.chatagent.rag.model.EvidenceRole; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.springframework.stereotype.Service; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class AnswerPlanService { + + public AnswerPlan build(QueryUnderstandingResult understanding) { + List requiredRoles = normalizeRoles( + understanding == null ? null : understanding.getExpectedEvidenceRoles() + ); + if (requiredRoles.isEmpty()) { + return AnswerPlan.builder() + .requiredRoles(List.of()) + .optionalRoles(List.of(EvidenceRole.GENERAL)) + .requireExplicitEvidence(false) + .allowRoleFallback(true) + .instruction("") + .build(); + } + return AnswerPlan.builder() + .requiredRoles(requiredRoles) + .optionalRoles(List.of(EvidenceRole.GENERAL)) + .requireExplicitEvidence(true) + .allowRoleFallback(false) + .instruction(buildInstruction(requiredRoles)) + .build(); + } + + private List normalizeRoles(List roles) { + if (roles == null || roles.isEmpty()) { + return List.of(); + } + return roles.stream() + .filter(role -> role != null && role != EvidenceRole.GENERAL) + .collect(Collectors.toCollection(LinkedHashSet::new)) + .stream() + .limit(4) + .toList(); + } + + private String buildInstruction(List requiredRoles) { + String joined = requiredRoles.stream() + .map(Enum::name) + .collect(Collectors.joining(" / ")); + if (StrUtil.isBlank(joined)) { + return ""; + } + return "本轮问题期望证据角色:" + joined + "。\n" + + "只能用对应角色的证据回答对应问题;如果 final evidence 中没有这些角色,必须说明文档没有明确给出。\n" + + "背景证据不能替代对应角色证据。"; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestrator.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestrator.java index 0e2b20a4cbcb6e60fb94df4798c664c5d9930096..ba06f435903d151ed8993f4a218c01019b095d50 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestrator.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestrator.java @@ -9,22 +9,30 @@ import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.AnswerHistoryContext; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; import org.javaup.ai.chatagent.rag.model.DocumentNavigationDecision; +import org.javaup.ai.chatagent.rag.model.EvidenceAnchor; import org.javaup.ai.chatagent.rag.model.ExecutionMode; import org.javaup.ai.chatagent.rag.model.HistoryPlanningContext; import org.javaup.ai.chatagent.rag.model.QueryType; import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; import org.javaup.ai.chatagent.rag.model.RagRewriteResult; import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationResult; +import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode; import org.javaup.ai.chatagent.service.ConversationMemoryService; import org.javaup.ai.chatagent.service.ConversationTraceRecorder; import org.javaup.ai.chatagent.service.TaskInfo; import org.javaup.ai.chatagent.support.TimeSensitiveQueryHelper; import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; import org.javaup.ai.manage.model.route.DocumentRouteCandidate; +import org.javaup.ai.manage.model.route.KnowledgeRouteContext; import org.javaup.ai.manage.model.route.KnowledgeRouteDecision; import org.javaup.ai.manage.service.DocumentKnowledgeService; import org.javaup.ai.manage.service.KnowledgeRouteService; import org.javaup.enums.ChatQueryMode; +import org.javaup.enums.KnowledgeBaseSelectionMode; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; @@ -56,6 +64,8 @@ public class ChatPreparationOrchestrator { private final DocumentQuestionRouter documentQuestionRouter; private final KnowledgeRouteService knowledgeRouteService; private final DocumentKnowledgeService documentKnowledgeService; + private final ConversationEvidenceAnchorService conversationEvidenceAnchorService; + private final StructureNavigationResolver structureNavigationResolver; public ChatPreparationOrchestrator(ChatRagProperties properties, ConversationMemoryService conversationMemoryService, @@ -64,6 +74,32 @@ public class ChatPreparationOrchestrator { DocumentQuestionRouter documentQuestionRouter, KnowledgeRouteService knowledgeRouteService, DocumentKnowledgeService documentKnowledgeService) { + this(properties, conversationMemoryService, answerHistoryContextAssembler, chatQueryRewriteService, + documentQuestionRouter, knowledgeRouteService, documentKnowledgeService, null, null); + } + + public ChatPreparationOrchestrator(ChatRagProperties properties, + ConversationMemoryService conversationMemoryService, + AnswerHistoryContextAssembler answerHistoryContextAssembler, + ChatQueryRewriteService chatQueryRewriteService, + DocumentQuestionRouter documentQuestionRouter, + KnowledgeRouteService knowledgeRouteService, + DocumentKnowledgeService documentKnowledgeService, + ConversationEvidenceAnchorService conversationEvidenceAnchorService) { + this(properties, conversationMemoryService, answerHistoryContextAssembler, chatQueryRewriteService, + documentQuestionRouter, knowledgeRouteService, documentKnowledgeService, conversationEvidenceAnchorService, null); + } + + @Autowired + public ChatPreparationOrchestrator(ChatRagProperties properties, + ConversationMemoryService conversationMemoryService, + AnswerHistoryContextAssembler answerHistoryContextAssembler, + ChatQueryRewriteService chatQueryRewriteService, + DocumentQuestionRouter documentQuestionRouter, + KnowledgeRouteService knowledgeRouteService, + DocumentKnowledgeService documentKnowledgeService, + ConversationEvidenceAnchorService conversationEvidenceAnchorService, + StructureNavigationResolver structureNavigationResolver) { this.properties = properties; this.conversationMemoryService = conversationMemoryService; this.answerHistoryContextAssembler = answerHistoryContextAssembler; @@ -71,6 +107,8 @@ public class ChatPreparationOrchestrator { this.documentQuestionRouter = documentQuestionRouter; this.knowledgeRouteService = knowledgeRouteService; this.documentKnowledgeService = documentKnowledgeService; + this.conversationEvidenceAnchorService = conversationEvidenceAnchorService; + this.structureNavigationResolver = structureNavigationResolver; } public ConversationExecutionPlan prepare(TaskInfo taskInfo) { @@ -80,6 +118,7 @@ public class ChatPreparationOrchestrator { Long selectedDocumentId = taskInfo.selectedDocumentId(); String selectedDocumentName = taskInfo.selectedDocumentName(); Long selectedTaskId = taskInfo.selectedTaskId(); + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection = taskInfo.knowledgeBaseSelectionSnapshot(); LocalDate currentDate = taskInfo.currentDate(); String currentDateText = taskInfo.currentDateText(); ConversationTraceRecorder traceRecorder = taskInfo.traceRecorder(); @@ -111,10 +150,12 @@ public class ChatPreparationOrchestrator { HistoryPlanningContext historyPlanningContext = buildHistoryPlanningContext(memoryContext); String historySummary = buildPlanningHistory(memoryContext, historyPlanningContext); + List recentEvidenceAnchors = loadRecentEvidenceAnchors(conversationId); AnswerHistoryContext answerHistoryContext = buildAnswerHistoryContext( question, memoryContext == null ? "" : memoryContext.getAnswerRecentTranscript(), - null + null, + List.of() ); boolean requiresCurrentDateAnchoring = TimeSensitiveQueryHelper.requiresCurrentDateAnchoring(question); @@ -125,7 +166,7 @@ public class ChatPreparationOrchestrator { if (chatMode == ChatQueryMode.OPEN_CHAT) { ConversationExecutionPlan plan = basePlan(question, chatMode, memoryContext, historyPlanningContext, historySummary, answerHistoryContext, currentDate, currentDateText, - requiresCurrentDateAnchoring, requiresFreshSearch) + requiresCurrentDateAnchoring, requiresFreshSearch, knowledgeBaseSelection) .mode(ExecutionMode.REACT_AGENT) .build(); if (traceRecorder != null) { @@ -139,6 +180,12 @@ public class ChatPreparationOrchestrator { } return plan; } + if (selectionMode(knowledgeBaseSelection) == KnowledgeBaseSelectionMode.NONE) { + return basePlan(question, ChatQueryMode.OPEN_CHAT, memoryContext, historyPlanningContext, historySummary, answerHistoryContext, currentDate, currentDateText, + requiresCurrentDateAnchoring, requiresFreshSearch, knowledgeBaseSelection) + .mode(ExecutionMode.REACT_AGENT) + .build(); + } if (!properties.isEnabled()) { throw new IllegalStateException("当前文档问答模式未启用,请先开启聊天侧 RAG 编排"); @@ -185,9 +232,10 @@ public class ChatPreparationOrchestrator { List routedDocumentIds = routedDocumentId == null ? List.of() : List.of(routedDocumentId); List routedTaskIds = routedTaskId == null ? List.of() : List.of(routedTaskId); if (chatMode == ChatQueryMode.AUTO_DOCUMENT) { - KnowledgeRouteDecision routeDecision = knowledgeRouteService.route(question, rewriteQuestion); - knowledgeRouteService.recordAutoRoute(conversationId, taskInfo.exchangeId(), question, rewriteQuestion, routeDecision); - List candidateDocuments = selectAutoCandidates(routeDecision, question, rewriteQuestion); + KnowledgeRouteContext routeContext = buildRouteContext(question, rewriteQuestion, knowledgeBaseSelection); + KnowledgeRouteDecision routeDecision = knowledgeRouteService.route(routeContext); + knowledgeRouteService.recordAutoRoute(conversationId, taskInfo.exchangeId(), routeContext, routeDecision); + List candidateDocuments = selectAutoCandidates(routeDecision, question, rewriteQuestion, allowedDocuments(knowledgeBaseSelection)); boolean lowConfidenceMultiDocumentRetrieval = shouldAllowLowConfidenceMultiDocumentRetrieval(routeDecision, candidateDocuments); if (lowConfidenceMultiDocumentRetrieval) { log.info("自动知识路由低置信多文档候选进入检索: conversationId={}, confidence={}, candidateDocumentCount={}, threshold=[{}, {})", @@ -200,7 +248,7 @@ public class ChatPreparationOrchestrator { if (shouldAskClarification(routeDecision, candidateDocuments, lowConfidenceMultiDocumentRetrieval)) { recordAutoDocumentRouteTrace(traceRecorder, routeDecision, candidateDocuments, true, false, null); return basePlan(question, chatMode, memoryContext, historyPlanningContext, historySummary, answerHistoryContext, currentDate, currentDateText, - requiresCurrentDateAnchoring, requiresFreshSearch) + requiresCurrentDateAnchoring, requiresFreshSearch, knowledgeBaseSelection) .mode(ExecutionMode.CLARIFICATION) .rewriteQuestion(rewriteQuestion) .rewriteSubQuestions(rewriteSubQuestions) @@ -249,7 +297,8 @@ public class ChatPreparationOrchestrator { lowConfidenceMultiDocumentRetrieval); } else if (chatMode == ChatQueryMode.DOCUMENT) { - knowledgeRouteService.recordShadowRoute(conversationId, taskInfo.exchangeId(), selectedDocumentId, question, rewriteQuestion); + knowledgeRouteService.recordShadowRoute(conversationId, taskInfo.exchangeId(), selectedDocumentId, + buildRouteContext(question, rewriteQuestion, knowledgeBaseSelection)); } ConversationTraceRecorder.StageHandle routeStage = traceRecorder == null @@ -265,6 +314,11 @@ public class ChatPreparationOrchestrator { memoryContext == null ? "" : memoryContext.getAnswerRecentTranscript() ); QueryUnderstandingResult queryUnderstanding = navigationDecision == null ? null : navigationDecision.getQueryUnderstanding(); + StructureNavigationResult structureNavigationResult = resolveStructureNavigationResult( + navigationDecision, + routedDocumentId, + routedTaskId + ); if (traceRecorder != null) { traceRecorder.completeStage(routeStage, "执行路由完成。", Map.of( "executionMode", navigationDecision == null || navigationDecision.getExecutionMode() == null ? "" : navigationDecision.getExecutionMode().name(), @@ -276,6 +330,7 @@ public class ChatPreparationOrchestrator { ? RetrievalIntent.GENERAL.name() : navigationDecision.getRetrievalIntent().name(), "queryUnderstanding", buildQueryUnderstandingTrace(queryUnderstanding), + "structureNavigation", buildStructureNavigationTrace(structureNavigationResult), "navigationSummary", navigationDecision == null ? "" : StrUtil.blankToDefault(navigationDecision.getSummaryText(), "") )); } @@ -301,10 +356,18 @@ public class ChatPreparationOrchestrator { ? RetrievalIntent.GENERAL : navigationDecision.getRetrievalIntent(); QueryUnderstandingResult queryUnderstanding = navigationDecision == null ? null : navigationDecision.getQueryUnderstanding(); + List scopedEvidenceAnchors = filterEvidenceAnchors( + recentEvidenceAnchors, + chatMode, + routedDocumentId, + knowledgeBaseSelection + ); + appendAnchorHints(historyPlanningContext, scopedEvidenceAnchors); AnswerHistoryContext routedAnswerHistoryContext = buildAnswerHistoryContext( question, memoryContext == null ? "" : memoryContext.getAnswerRecentTranscript(), - queryUnderstanding + queryUnderstanding, + scopedEvidenceAnchors ); log.info("聊天编排完成: conversationId={}, chatMode={}, originalQuestion='{}', rewriteQuestion='{}', retrievalQuestion='{}', executionMode={}, retrievalIntent={}, targetSection='{}'", @@ -318,7 +381,7 @@ public class ChatPreparationOrchestrator { navigationDecision == null || navigationDecision.getStructureAnchor() == null ? "" : safeText(navigationDecision.getStructureAnchor().getTargetSectionHint())); return basePlan(question, chatMode, memoryContext, historyPlanningContext, historySummary, routedAnswerHistoryContext, currentDate, currentDateText, - requiresCurrentDateAnchoring, requiresFreshSearch) + requiresCurrentDateAnchoring, requiresFreshSearch, knowledgeBaseSelection) .mode(executionMode) .navigationDecision(navigationDecision) .queryUnderstanding(queryUnderstanding) @@ -336,6 +399,27 @@ public class ChatPreparationOrchestrator { .build(); } + private StructureNavigationResult resolveStructureNavigationResult(DocumentNavigationDecision navigationDecision, + Long routedDocumentId, + Long routedTaskId) { + if (structureNavigationResolver == null || navigationDecision == null || routedDocumentId == null) { + return null; + } + QueryUnderstandingResult queryUnderstanding = navigationDecision.getQueryUnderstanding(); + StructureNavigationIntent intent = queryUnderstanding == null ? null : queryUnderstanding.getStructureNavigationIntent(); + if (intent == null) { + return null; + } + StructureNavigationResult result = structureNavigationResolver.resolve( + routedDocumentId, + routedTaskId, + intent, + navigationDecision.getStructureAnchor() + ); + navigationDecision.setStructureNavigationResult(result); + return result; + } + private Map buildQueryUnderstandingTrace(QueryUnderstandingResult queryUnderstanding) { if (queryUnderstanding == null) { return Map.of(); @@ -344,15 +428,69 @@ public class ChatPreparationOrchestrator { snapshot.put("queryType", queryUnderstanding.getQueryType() == null ? "" : queryUnderstanding.getQueryType().name()); snapshot.put("channels", queryUnderstanding.getChannels() == null ? List.of() : queryUnderstanding.getChannels().stream().map(Enum::name).toList()); snapshot.put("entities", queryUnderstanding.getEntities() == null ? List.of() : queryUnderstanding.getEntities()); + snapshot.put("targetEntities", queryUnderstanding.getTargetEntities() == null ? List.of() : queryUnderstanding.getTargetEntities()); + snapshot.put("excludedEntities", queryUnderstanding.getExcludedEntities() == null ? List.of() : queryUnderstanding.getExcludedEntities()); snapshot.put("sectionAnchors", queryUnderstanding.getSectionAnchors() == null ? List.of() : queryUnderstanding.getSectionAnchors()); + snapshot.put("structureNavigationIntent", buildStructureNavigationIntentTrace(queryUnderstanding.getStructureNavigationIntent())); snapshot.put("tableOps", queryUnderstanding.getTableOps() == null ? List.of() : queryUnderstanding.getTableOps()); snapshot.put("negativeBoundary", queryUnderstanding.isNegativeBoundary()); + snapshot.put("answerExpectation", StrUtil.blankToDefault(queryUnderstanding.getAnswerExpectation(), "")); snapshot.put("confidence", queryUnderstanding.getConfidence()); snapshot.put("source", StrUtil.blankToDefault(queryUnderstanding.getSource(), "")); snapshot.put("reasons", queryUnderstanding.getReasons() == null ? List.of() : queryUnderstanding.getReasons()); return snapshot; } + private Map buildStructureNavigationIntentTrace(StructureNavigationIntent intent) { + if (intent == null) { + return Map.of(); + } + Map snapshot = new LinkedHashMap<>(); + snapshot.put("operations", intent.getOperations() == null ? List.of() : intent.getOperations().stream().map(Enum::name).toList()); + snapshot.put("anchorStructureNodeId", intent.getAnchorStructureNodeId() == null ? "" : String.valueOf(intent.getAnchorStructureNodeId())); + snapshot.put("anchorSectionPath", StrUtil.blankToDefault(intent.getAnchorSectionPath(), "")); + snapshot.put("anchorCanonicalPath", StrUtil.blankToDefault(intent.getAnchorCanonicalPath(), "")); + snapshot.put("sectionAnchors", intent.getSectionAnchors() == null ? List.of() : intent.getSectionAnchors()); + snapshot.put("confidence", intent.getConfidence()); + snapshot.put("source", StrUtil.blankToDefault(intent.getSource(), "")); + return snapshot; + } + + private Map buildStructureNavigationTrace(StructureNavigationResult result) { + if (result == null) { + return Map.of(); + } + Map snapshot = new LinkedHashMap<>(); + snapshot.put("documentId", result.getDocumentId() == null ? "" : String.valueOf(result.getDocumentId())); + snapshot.put("anchorNodeId", result.getAnchorNodeId() == null ? "" : String.valueOf(result.getAnchorNodeId())); + snapshot.put("current", buildStructureNodeTrace(result.getCurrent())); + snapshot.put("parent", buildStructureNodeTrace(result.getParent())); + snapshot.put("previous", buildStructureNodeTrace(result.getPreviousSibling())); + snapshot.put("next", buildStructureNodeTrace(result.getNextSibling())); + snapshot.put("directChildren", result.getDirectChildren() == null + ? List.of() + : result.getDirectChildren().stream().map(this::buildStructureNodeTrace).toList()); + snapshot.put("deterministic", result.isDeterministic()); + snapshot.put("missReason", StrUtil.blankToDefault(result.getMissReason(), "")); + return snapshot; + } + + private Map buildStructureNodeTrace(SuperAgentDocumentStructureNode node) { + if (node == null) { + return Map.of(); + } + Map snapshot = new LinkedHashMap<>(); + snapshot.put("nodeId", node.getId() == null ? "" : String.valueOf(node.getId())); + snapshot.put("nodeNo", node.getNodeNo() == null ? "" : String.valueOf(node.getNodeNo())); + snapshot.put("title", StrUtil.blankToDefault(node.getTitle(), "")); + snapshot.put("sectionPath", StrUtil.blankToDefault(node.getSectionPath(), "")); + snapshot.put("canonicalPath", StrUtil.blankToDefault(node.getCanonicalPath(), "")); + snapshot.put("parentNodeId", node.getParentNodeId() == null ? "" : String.valueOf(node.getParentNodeId())); + snapshot.put("prevSiblingNodeId", node.getPrevSiblingNodeId() == null ? "" : String.valueOf(node.getPrevSiblingNodeId())); + snapshot.put("nextSiblingNodeId", node.getNextSiblingNodeId() == null ? "" : String.valueOf(node.getNextSiblingNodeId())); + return snapshot; + } + private ConversationExecutionPlan.ConversationExecutionPlanBuilder basePlan(String question, ChatQueryMode chatMode, ConversationMemoryContext memoryContext, @@ -362,7 +500,8 @@ public class ChatPreparationOrchestrator { LocalDate currentDate, String currentDateText, boolean requiresCurrentDateAnchoring, - boolean requiresFreshSearch) { + boolean requiresFreshSearch, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { return ConversationExecutionPlan.builder() .chatMode(chatMode) .originalQuestion(question) @@ -385,6 +524,11 @@ public class ChatPreparationOrchestrator { .currentDateText(currentDateText) .requiresCurrentDateAnchoring(requiresCurrentDateAnchoring) .requiresFreshSearch(requiresFreshSearch) + .knowledgeBaseSelectionMode(selectionMode(knowledgeBaseSelection)) + .selectedKnowledgeBaseIds(selectedKnowledgeBaseIds(knowledgeBaseSelection)) + .selectedKnowledgeBaseNames(selectedKnowledgeBaseNames(knowledgeBaseSelection)) + .allowedKnowledgeBaseDocumentIds(allowedDocumentIds(knowledgeBaseSelection)) + .ragRuntimeOptions(knowledgeBaseSelection == null ? null : knowledgeBaseSelection.getRagRuntimeOptions()) .noEvidenceReply(properties.getNoEvidenceReply()); } @@ -468,7 +612,7 @@ public class ChatPreparationOrchestrator { .limit(5) .map(scope -> { Map item = new LinkedHashMap<>(); - item.put("scopeCode", StrUtil.blankToDefault(scope.getScopeCode(), "")); + item.put("scopeId", scope.getScopeId() == null ? "" : String.valueOf(scope.getScopeId())); item.put("scopeName", StrUtil.blankToDefault(scope.getScopeName(), "")); item.put("score", scope.getScore() == null ? "" : scope.getScore().toPlainString()); item.put("reason", StrUtil.blankToDefault(scope.getReason(), "")); @@ -485,8 +629,8 @@ public class ChatPreparationOrchestrator { .limit(5) .map(topic -> { Map item = new LinkedHashMap<>(); - item.put("scopeCode", StrUtil.blankToDefault(topic.getScopeCode(), "")); - item.put("topicCode", StrUtil.blankToDefault(topic.getTopicCode(), "")); + item.put("scopeId", topic.getScopeId() == null ? "" : String.valueOf(topic.getScopeId())); + item.put("topicId", topic.getTopicId() == null ? "" : String.valueOf(topic.getTopicId())); item.put("topicName", StrUtil.blankToDefault(topic.getTopicName(), "")); item.put("score", topic.getScore() == null ? "" : topic.getScore().toPlainString()); item.put("reason", StrUtil.blankToDefault(topic.getReason(), "")); @@ -506,10 +650,6 @@ public class ChatPreparationOrchestrator { item.put("documentId", StrUtil.blankToDefault(document.getDocumentId(), "")); item.put("documentName", StrUtil.blankToDefault(document.getDocumentName(), "")); item.put("lastIndexTaskId", StrUtil.blankToDefault(document.getLastIndexTaskId(), "")); - item.put("knowledgeScopeCode", StrUtil.blankToDefault(document.getKnowledgeScopeCode(), "")); - item.put("knowledgeScopeName", StrUtil.blankToDefault(document.getKnowledgeScopeName(), "")); - item.put("businessCategory", StrUtil.blankToDefault(document.getBusinessCategory(), "")); - item.put("documentTags", StrUtil.blankToDefault(document.getDocumentTags(), "")); item.put("score", document.getScore() == null ? "" : document.getScore().toPlainString()); item.put("reason", StrUtil.blankToDefault(document.getReason(), "")); return item; @@ -572,7 +712,96 @@ public class ChatPreparationOrchestrator { private AnswerHistoryContext buildAnswerHistoryContext(String question, String answerRecentTranscript, QueryUnderstandingResult queryUnderstanding) { - return answerHistoryContextAssembler.assemble(question, answerRecentTranscript, queryUnderstanding); + return buildAnswerHistoryContext(question, answerRecentTranscript, queryUnderstanding, List.of()); + } + + private AnswerHistoryContext buildAnswerHistoryContext(String question, + String answerRecentTranscript, + QueryUnderstandingResult queryUnderstanding, + List recentEvidenceAnchors) { + return answerHistoryContextAssembler.assemble(question, answerRecentTranscript, queryUnderstanding, recentEvidenceAnchors); + } + + private List loadRecentEvidenceAnchors(String conversationId) { + if (conversationEvidenceAnchorService == null || StrUtil.isBlank(conversationId)) { + return List.of(); + } + try { + return conversationEvidenceAnchorService.loadRecentEvidenceAnchors(conversationId, 5); + } + catch (RuntimeException exception) { + log.warn("加载上一轮 evidence anchor 失败: conversationId={}, message={}", + conversationId, + exception.getMessage(), + exception); + return List.of(); + } + } + + private List filterEvidenceAnchors(List anchors, + ChatQueryMode chatMode, + Long selectedDocumentId, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + if (anchors == null || anchors.isEmpty()) { + return List.of(); + } + if (chatMode == ChatQueryMode.DOCUMENT && selectedDocumentId != null) { + return anchors.stream() + .filter(anchor -> anchor != null && Objects.equals(anchor.getDocumentId(), selectedDocumentId)) + .toList(); + } + if (chatMode == ChatQueryMode.AUTO_DOCUMENT + && knowledgeBaseSelection != null + && knowledgeBaseSelection.getAllowedDocumentIds() != null + && !knowledgeBaseSelection.getAllowedDocumentIds().isEmpty()) { + return anchors.stream() + .filter(anchor -> anchor != null && anchor.getDocumentId() != null) + .filter(anchor -> knowledgeBaseSelection.getAllowedDocumentIds().contains(anchor.getDocumentId())) + .toList(); + } + return anchors; + } + + private void appendAnchorHints(HistoryPlanningContext historyPlanningContext, List anchors) { + if (historyPlanningContext == null || anchors == null || anchors.isEmpty()) { + return; + } + List hints = new ArrayList<>(historyPlanningContext.getQueryContextHints() == null + ? List.of() + : historyPlanningContext.getQueryContextHints()); + anchors.stream() + .map(this::anchorHint) + .filter(StrUtil::isNotBlank) + .limit(5) + .forEach(hints::add); + historyPlanningContext.setQueryContextHints(hints); + } + + private String anchorHint(EvidenceAnchor anchor) { + if (anchor == null) { + return ""; + } + StringBuilder builder = new StringBuilder(); + appendHintPart(builder, "documentId", anchor.getDocumentId()); + appendHintPart(builder, "sectionPath", anchor.getSectionPath()); + appendHintPart(builder, "structureNodeId", anchor.getStructureNodeId()); + appendHintPart(builder, "parentBlockId", anchor.getParentBlockId()); + appendHintPart(builder, "chunkId", anchor.getChunkId()); + return builder.toString().trim(); + } + + private void appendHintPart(StringBuilder builder, String name, Object value) { + if (value == null) { + return; + } + String text = String.valueOf(value).trim(); + if (text.isBlank()) { + return; + } + if (!builder.isEmpty()) { + builder.append("; "); + } + builder.append(name).append('=').append(text); } private String buildStructuredPlanningHistory(HistoryPlanningContext historyPlanningContext) { @@ -657,28 +886,36 @@ public class ChatPreparationOrchestrator { private List selectAutoCandidates(KnowledgeRouteDecision routeDecision, String question, - String rewriteQuestion) { + String rewriteQuestion, + List allowedDocuments) { if (routeDecision == null || routeDecision.getDocuments() == null || routeDecision.getDocuments().isEmpty()) { - return expandCandidatesByDocumentProfile(question, rewriteQuestion, 5); + return expandCandidatesByDocumentProfile(question, rewriteQuestion, allowedDocuments, 5); } int candidateLimit = routeDecision.getConfidence() != null && routeDecision.getConfidence().doubleValue() >= 0.80D ? 3 : 5; + List allowedDocumentIds = allowedDocuments == null + ? List.of() + : allowedDocuments.stream().map(KnowledgeDocumentDescriptor::getDocumentId).filter(Objects::nonNull).toList(); List candidates = routeDecision.getDocuments().stream() .filter(item -> StrUtil.isNotBlank(item.getDocumentId()) && StrUtil.isNotBlank(item.getLastIndexTaskId())) + .filter(item -> allowedDocumentIds.isEmpty() || allowedDocumentIds.contains(Long.valueOf(item.getDocumentId()))) .limit(candidateLimit) .toList(); if (candidates.isEmpty()) { - return expandCandidatesByDocumentProfile(question, rewriteQuestion, candidateLimit); + return expandCandidatesByDocumentProfile(question, rewriteQuestion, allowedDocuments, candidateLimit); } if (routeDecision.getConfidence() != null && routeDecision.getConfidence().doubleValue() < confidentDocumentThreshold()) { - return mergeCandidates(candidates, expandCandidatesByDocumentProfile(question, rewriteQuestion, candidateLimit), candidateLimit); + return mergeCandidates(candidates, expandCandidatesByDocumentProfile(question, rewriteQuestion, allowedDocuments, candidateLimit), candidateLimit); } return candidates; } private List expandCandidatesByDocumentProfile(String question, String rewriteQuestion, + List allowedDocuments, int limit) { - List descriptors = documentKnowledgeService.listRetrievableDocuments(); + List descriptors = allowedDocuments == null || allowedDocuments.isEmpty() + ? List.of() + : allowedDocuments; if (descriptors == null || descriptors.isEmpty()) { return List.of(); } @@ -693,16 +930,56 @@ public class ChatPreparationOrchestrator { String.valueOf(item.getDocumentId()), item.getDocumentName(), item.getLastIndexTaskId() == null ? "" : String.valueOf(item.getLastIndexTaskId()), - StrUtil.blankToDefault(item.getKnowledgeScopeCode(), ""), - StrUtil.blankToDefault(item.getKnowledgeScopeName(), ""), - StrUtil.blankToDefault(item.getBusinessCategory(), ""), - StrUtil.blankToDefault(item.getDocumentTags(), ""), BigDecimal.valueOf(descriptorRouteScore(item, queryTerms)).setScale(4, RoundingMode.HALF_UP), "低置信度时基于文档画像扩展候选范围" )) .toList(); } + private KnowledgeRouteContext buildRouteContext(String question, + String rewriteQuestion, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + return KnowledgeRouteContext.builder() + .question(question) + .rewriteQuestion(rewriteQuestion) + .knowledgeBaseSelectionMode(selectionMode(knowledgeBaseSelection)) + .selectedKnowledgeBaseIds(selectedKnowledgeBaseIds(knowledgeBaseSelection)) + .selectedKnowledgeBaseNames(selectedKnowledgeBaseNames(knowledgeBaseSelection)) + .allowedDocuments(allowedDocuments(knowledgeBaseSelection)) + .allowedDocumentIds(allowedDocumentIds(knowledgeBaseSelection)) + .build(); + } + + private KnowledgeBaseSelectionMode selectionMode(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + return knowledgeBaseSelection == null || knowledgeBaseSelection.getSelectionMode() == null + ? KnowledgeBaseSelectionMode.NONE + : knowledgeBaseSelection.getSelectionMode(); + } + + private List selectedKnowledgeBaseIds(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + return knowledgeBaseSelection == null || knowledgeBaseSelection.getSelectedKnowledgeBaseIds() == null + ? List.of() + : knowledgeBaseSelection.getSelectedKnowledgeBaseIds(); + } + + private List selectedKnowledgeBaseNames(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + return knowledgeBaseSelection == null || knowledgeBaseSelection.getSelectedKnowledgeBaseNames() == null + ? List.of() + : knowledgeBaseSelection.getSelectedKnowledgeBaseNames(); + } + + private List allowedDocuments(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + return knowledgeBaseSelection == null || knowledgeBaseSelection.getAllowedDocuments() == null + ? List.of() + : knowledgeBaseSelection.getAllowedDocuments(); + } + + private List allowedDocumentIds(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + return knowledgeBaseSelection == null || knowledgeBaseSelection.getAllowedDocumentIds() == null + ? List.of() + : knowledgeBaseSelection.getAllowedDocumentIds(); + } + private List mergeCandidates(List primary, List secondary, int limit) { @@ -735,8 +1012,7 @@ public class ChatPreparationOrchestrator { if (topScore == null || secondScore == null) { return false; } - return topScore.subtract(secondScore).doubleValue() <= 3D - && !Objects.equals(candidateDocuments.get(0).getKnowledgeScopeCode(), candidateDocuments.get(1).getKnowledgeScopeCode()); + return topScore.subtract(secondScore).doubleValue() <= 3D; } private boolean shouldAllowLowConfidenceMultiDocumentRetrieval(KnowledgeRouteDecision routeDecision, @@ -790,11 +1066,6 @@ public class ChatPreparationOrchestrator { .append(". 《") .append(StrUtil.blankToDefault(item.getDocumentName(), item.getDocumentId())) .append("》"); - if (StrUtil.isNotBlank(item.getKnowledgeScopeName()) || StrUtil.isNotBlank(item.getKnowledgeScopeCode())) { - builder.append("(") - .append(StrUtil.blankToDefault(item.getKnowledgeScopeName(), item.getKnowledgeScopeCode())) - .append(")"); - } builder.append('\n'); } builder.append("你可以直接回复文档名,或者改用“当前文档问答”模式明确指定文档。"); @@ -844,10 +1115,7 @@ public class ChatPreparationOrchestrator { private double descriptorRouteScore(KnowledgeDocumentDescriptor descriptor, List queryTerms) { String content = normalizeRouteExpansionText(String.join(" ", StrUtil.blankToDefault(descriptor.getDocumentName(), ""), - StrUtil.blankToDefault(descriptor.getKnowledgeScopeCode(), ""), - StrUtil.blankToDefault(descriptor.getKnowledgeScopeName(), ""), - StrUtil.blankToDefault(descriptor.getBusinessCategory(), ""), - StrUtil.blankToDefault(descriptor.getDocumentTags(), "") + StrUtil.blankToDefault(descriptor.getKnowledgeBaseName(), "") )); if (queryTerms == null || queryTerms.isEmpty() || content.isBlank()) { return 0D; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ConversationEvidenceAnchorService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ConversationEvidenceAnchorService.java new file mode 100644 index 0000000000000000000000000000000000000000..b81ff984ad1e1a55fdb46ddac2e7b998390d0309 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ConversationEvidenceAnchorService.java @@ -0,0 +1,97 @@ +package org.javaup.ai.chatagent.rag.service; + +import cn.hutool.core.util.StrUtil; +import org.javaup.ai.chatagent.model.ConversationExchangeView; +import org.javaup.ai.chatagent.model.SearchReference; +import org.javaup.ai.chatagent.rag.model.EvidenceAnchor; +import org.javaup.ai.chatagent.service.ConversationArchiveStore; +import org.javaup.enums.ChatTurnStatus; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * 从上一轮最终引用中抽取追问可继承的结构锚点。 + */ +@Service +public class ConversationEvidenceAnchorService { + + private final ConversationArchiveStore conversationArchiveStore; + + public ConversationEvidenceAnchorService(ConversationArchiveStore conversationArchiveStore) { + this.conversationArchiveStore = conversationArchiveStore; + } + + public List loadRecentEvidenceAnchors(String conversationId, int limit) { + if (StrUtil.isBlank(conversationId) || limit <= 0 || conversationArchiveStore == null) { + return List.of(); + } + List exchanges = conversationArchiveStore.listRecentExchanges(conversationId, 3); + if (exchanges == null || exchanges.isEmpty()) { + return List.of(); + } + List anchors = new ArrayList<>(); + for (ConversationExchangeView exchange : exchanges) { + if (exchange == null || !completed(exchange) || exchange.getReferences() == null || exchange.getReferences().isEmpty()) { + continue; + } + for (SearchReference reference : exchange.getReferences()) { + EvidenceAnchor anchor = fromReference(reference); + if (anchor == null) { + continue; + } + anchors.add(anchor); + if (anchors.size() >= limit) { + return anchors; + } + } + } + return anchors; + } + + private boolean completed(ConversationExchangeView exchange) { + return exchange.getStatus() == null || exchange.getStatus() == ChatTurnStatus.COMPLETED; + } + + private EvidenceAnchor fromReference(SearchReference reference) { + if (reference == null || !hasUsableAnchor(reference)) { + return null; + } + return EvidenceAnchor.builder() + .documentId(reference.getDocumentId()) + .documentName(reference.getDocumentName()) + .knowledgeBaseId(reference.getKnowledgeBaseId()) + .knowledgeBaseName(reference.getKnowledgeBaseName()) + .structureNodeId(reference.getStructureNodeId()) + .sectionPath(reference.getSectionPath()) + .canonicalPath(reference.getCanonicalPath()) + .itemIndex(reference.getItemIndex()) + .parentBlockId(reference.getParentBlockId()) + .chunkId(reference.getChunkId()) + .sourceType(reference.getSourceType()) + .channel(reference.getChannel()) + .snippet(clip(reference.getSnippet(), 300)) + .score(reference.getScore()) + .build(); + } + + private boolean hasUsableAnchor(SearchReference reference) { + return reference.getDocumentId() != null + || reference.getStructureNodeId() != null + || reference.getParentBlockId() != null + || reference.getChunkId() != null + || StrUtil.isNotBlank(reference.getSectionPath()); + } + + private String clip(String text, int maxChars) { + String normalized = StrUtil.blankToDefault(text, "").trim(); + if (normalized.length() <= maxChars) { + return normalized; + } + if (maxChars <= 1) { + return ""; + } + return normalized.substring(0, maxChars - 1) + "…"; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouter.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouter.java index 155788b18c3f08208fc5534969805ae09d16c159..441fc48d5a962fc398d4a31e7b59e2a842a17e19 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouter.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouter.java @@ -12,6 +12,8 @@ import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; import org.javaup.ai.chatagent.rag.model.RagRewriteResult; import org.javaup.ai.chatagent.rag.model.RetrievalQuestionPlan; import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationOperation; import org.javaup.ai.manage.model.graph.GraphSection; import org.javaup.ai.manage.service.DocumentNavigationIndexService; import org.javaup.ai.manage.service.DocumentStructureGraphService; @@ -92,6 +94,21 @@ public class DocumentQuestionRouter { RetrievalIntent retrievalIntent = detectRetrievalIntent(questionIntent, queryUnderstanding); GraphOnlyIntentDecision graphOnlyIntent = questionIntent.graphOnlyIntent(); boolean analyticQuestion = questionIntent.analytic(); + + DocumentNavigationAction structureNavigationAction = resolveStructureNavigationAction(queryUnderstanding); + if (structureNavigationAction != null && subQuestions.size() <= 1) { + GraphSection section = resolveSection(documentId, originalQuestion, rewrittenQuestion); + return buildDecision( + ExecutionMode.RETRIEVAL, + structureNavigationAction, + section, + null, + retrievalPlan, + queryUnderstanding, + RetrievalIntent.STRUCTURE, + "高置信结构导航走结构树确定性查询,结构结果作为检索上下文和观测信号。" + ); + } boolean singleQuestionGraphOnlyMatched = graphOnlyIntent.matched() && subQuestions.size() <= 1; if (singleQuestionGraphOnlyMatched) { @@ -110,7 +127,8 @@ public class DocumentQuestionRouter { Integer itemIndex = resolveExplicitItemIndex(routeText); boolean itemLookupMatched = itemIndex != null || questionIntent.itemLookup(); - boolean shouldUseGraphThenEvidence = itemLookupMatched && !analyticQuestion; + boolean shouldUseGraphThenEvidence = itemLookupMatched + && shouldUseGraphThenEvidence(routeText, itemIndex, queryUnderstanding); if (shouldUseGraphThenEvidence) { GraphSection section = resolveSection(documentId, originalQuestion, rewrittenQuestion); return buildDecision( @@ -121,7 +139,7 @@ public class DocumentQuestionRouter { retrievalPlan, queryUnderstanding, retrievalIntent, - "编号项或步骤型问题走图定位取证" + "高置信结构导航编号项问题走图定位取证" ); } @@ -218,6 +236,36 @@ public class DocumentQuestionRouter { return primaryRetrievalIntent(queryUnderstanding); } + private DocumentNavigationAction resolveStructureNavigationAction(QueryUnderstandingResult queryUnderstanding) { + if (queryUnderstanding == null || queryUnderstanding.getQueryType() != QueryType.STRUCTURE_NAVIGATION) { + return null; + } + if (confidence(queryUnderstanding) < 0.65D) { + return null; + } + StructureNavigationIntent intent = queryUnderstanding.getStructureNavigationIntent(); + if (intent == null || intent.getOperations() == null || intent.getOperations().isEmpty()) { + return null; + } + List operations = intent.getOperations(); + if (operations.contains(StructureNavigationOperation.SECTION_WITH_CHILDREN) + || operations.contains(StructureNavigationOperation.DIRECT_CHILDREN)) { + return DocumentNavigationAction.CHILD_SECTION_DESCEND; + } + if (operations.contains(StructureNavigationOperation.SECTION_WITH_SIBLINGS) + || operations.contains(StructureNavigationOperation.PREVIOUS_SIBLING) + || operations.contains(StructureNavigationOperation.NEXT_SIBLING)) { + return DocumentNavigationAction.SECTION_ADJACENCY_LOOKUP; + } + if (operations.contains(StructureNavigationOperation.PARENT_SECTION)) { + return DocumentNavigationAction.ANCESTOR_SECTION_RETURN; + } + if (operations.contains(StructureNavigationOperation.CURRENT_SECTION)) { + return DocumentNavigationAction.FRESH_TOPIC; + } + return null; + } + private RetrievalIntent primaryRetrievalIntent(QueryUnderstandingResult queryUnderstanding) { if (queryUnderstanding == null) { return RetrievalIntent.GENERAL; @@ -344,6 +392,24 @@ public class DocumentQuestionRouter { return false; } + private boolean shouldUseGraphThenEvidence(String routeText, + Integer itemIndex, + QueryUnderstandingResult queryUnderstanding) { + if (itemIndex == null || queryUnderstanding == null) { + return false; + } + QueryType queryType = queryUnderstanding.getQueryType() == null + ? QueryType.DOCUMENT_QA + : queryUnderstanding.getQueryType(); + if (queryType != QueryType.STRUCTURE_NAVIGATION) { + return false; + } + if (confidence(queryUnderstanding) < 0.72D) { + return false; + } + return hasExplicitSectionAnchor(routeText) || hasSectionAnchor(queryUnderstanding); + } + private GraphOnlyIntentDecision detectGraphOnlyIntentByControlledPlan(String question, QueryUnderstandingResult queryUnderstanding) { if (asksOutlineByAnchors(queryUnderstanding)) { @@ -745,6 +811,10 @@ public class DocumentQuestionRouter { if (queryUnderstanding.getSectionAnchors() != null) { queryUnderstanding.getSectionAnchors().forEach(item -> addHint(hints, item)); } + StructureNavigationIntent structureIntent = queryUnderstanding.getStructureNavigationIntent(); + if (structureIntent != null && structureIntent.getSectionAnchors() != null) { + structureIntent.getSectionAnchors().forEach(item -> addHint(hints, item)); + } } if (section != null) { addHint(hints, section.displayTitle()); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentRetrieveRequestFactory.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentRetrieveRequestFactory.java index d91d30df4f89bda82c637c93326a8da8a5075283..3295c6a9f618dbfa800bcb2042d9888deb3cf2d7 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentRetrieveRequestFactory.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentRetrieveRequestFactory.java @@ -114,8 +114,6 @@ public class DocumentRetrieveRequestFactory { return DocumentRetrieveFilters.builder().build(); } LinkedHashSet documentNameHints = new LinkedHashSet<>(); - LinkedHashSet businessCategoryHints = new LinkedHashSet<>(); - LinkedHashSet documentTagHints = new LinkedHashSet<>(); LinkedHashSet sectionPathHints = new LinkedHashSet<>(); LinkedHashSet yearHints = new LinkedHashSet<>(); @@ -131,12 +129,10 @@ public class DocumentRetrieveRequestFactory { } } - collectQueryUnderstandingHints(queryUnderstanding, documentNameHints, businessCategoryHints, documentTagHints, sectionPathHints); + collectQueryUnderstandingHints(queryUnderstanding, documentNameHints, sectionPathHints); return DocumentRetrieveFilters.builder() .documentNameHints(new ArrayList<>(documentNameHints)) - .businessCategoryHints(new ArrayList<>(businessCategoryHints)) - .documentTagHints(new ArrayList<>(documentTagHints)) .sectionPathHints(new ArrayList<>(sectionPathHints)) .yearHints(new ArrayList<>(yearHints)) .build(); @@ -144,8 +140,6 @@ public class DocumentRetrieveRequestFactory { private void collectQueryUnderstandingHints(QueryUnderstandingResult queryUnderstanding, LinkedHashSet documentNameHints, - LinkedHashSet businessCategoryHints, - LinkedHashSet documentTagHints, LinkedHashSet sectionPathHints) { if (queryUnderstanding == null) { return; @@ -155,11 +149,7 @@ public class DocumentRetrieveRequestFactory { .filter(StrUtil::isNotBlank) .map(String::trim) .limit(8) - .forEach(entity -> { - documentNameHints.add(entity); - businessCategoryHints.add(entity); - documentTagHints.add(entity); - }); + .forEach(documentNameHints::add); } if (queryUnderstanding.getSectionAnchors() != null) { queryUnderstanding.getSectionAnchors().stream() diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentTableQueryPlanner.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentTableQueryPlanner.java index fe1d8760b4b9dd30afd45338aea5d98476b415bf..6c170dc2cf3ce0d4f437c7ea5ba957e99a581cce 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentTableQueryPlanner.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentTableQueryPlanner.java @@ -300,8 +300,20 @@ public class DocumentTableQueryPlanner { } DocumentTableQuery.Operation operation = DocumentTableQuery.Operation.COUNT; Optional metricColumn = resolveMetricColumn(question, table); - Optional groupColumn = Optional.empty(); - List filters = List.of(); + Optional groupColumn = resolveGroupColumn(question, table, metricColumn.orElse(null)); + List filters = resolveFilters(question, table); + + String normalizedQuestion = normalize(question); + boolean aggregateCue = containsAny(normalizedQuestion, List.of("合计", "总计", "求和", "总和", "sum")); + if (aggregateCue && metricColumn.isPresent()) { + operation = DocumentTableQuery.Operation.SUM; + } + if (groupColumn.isPresent() && aggregateCue && metricColumn.isPresent()) { + operation = DocumentTableQuery.Operation.GROUP_SUM; + } + else if (groupColumn.isPresent() && containsAny(normalizedQuestion, List.of("数量", "个数", "多少", "统计"))) { + operation = DocumentTableQuery.Operation.GROUP_COUNT; + } if (operation == DocumentTableQuery.Operation.COUNT && groupColumn.isPresent()) { operation = DocumentTableQuery.Operation.GROUP_COUNT; @@ -335,7 +347,9 @@ public class DocumentTableQueryPlanner { } private boolean mentionsKnownTableSignal(String question, List tables) { - return tables.stream() + String normalizedQuestion = normalize(question); + boolean operationCue = containsAny(normalizedQuestion, List.of("合计", "总计", "求和", "总和", "统计", "数量", "个数", "多少", "最大", "最小", "sum", "count")); + return operationCue && tables.stream() .filter(table -> table != null) .anyMatch(table -> textMentionScore(question, table.getTitle()) > 0 || textMentionScore(question, table.getSectionPath()) > 0 @@ -355,6 +369,77 @@ public class DocumentTableQueryPlanner { .findFirst()); } + private Optional resolveGroupColumn(String question, + DocumentTableDescriptor table, + DocumentTableDescriptor.Column metricColumn) { + String normalizedQuestion = normalize(question); + if (!containsAny(normalizedQuestion, List.of("按", "分别", "分组", "各", "每"))) { + return Optional.empty(); + } + return table.getColumns().stream() + .filter(column -> metricColumn == null || !StrUtil.equals(column.getColumnName(), metricColumn.getColumnName())) + .filter(column -> !"NUMBER".equalsIgnoreCase(StrUtil.blankToDefault(column.getValueType(), ""))) + .map(column -> new ColumnScore(column, columnMentionScore(question, column))) + .filter(item -> item.score() > 0) + .max(Comparator.comparingInt(ColumnScore::score)) + .map(ColumnScore::column); + } + + private List resolveFilters(String question, DocumentTableDescriptor table) { + String normalizedQuestion = normalize(question); + List filters = new ArrayList<>(); + for (DocumentTableDescriptor.Column column : table.getColumns()) { + if (column == null || StrUtil.isBlank(column.getColumnName())) { + continue; + } + String normalizedColumn = normalize(column.getColumnName()); + if (normalizedColumn.isBlank()) { + continue; + } + for (String marker : List.of("为", "是", "等于")) { + int start = normalizedQuestion.indexOf(normalizedColumn + marker); + if (start < 0) { + continue; + } + int valueStart = start + normalizedColumn.length() + normalize(marker).length(); + String value = extractFilterValue(normalizedQuestion.substring(valueStart), table); + if (StrUtil.isNotBlank(value)) { + filters.add(DocumentTableQuery.Filter.builder() + .column(column.getColumnName()) + .operator(DocumentTableQuery.Operator.EQ) + .value(value) + .build()); + break; + } + } + } + return filters; + } + + private String extractFilterValue(String normalizedRemainder, DocumentTableDescriptor table) { + if (StrUtil.isBlank(normalizedRemainder)) { + return ""; + } + String value = normalizedRemainder; + for (DocumentTableDescriptor.Column column : table.getColumns()) { + String normalizedColumn = normalize(column.getColumnName()); + if (StrUtil.isBlank(normalizedColumn)) { + continue; + } + int index = value.indexOf(normalizedColumn); + if (index > 0) { + value = value.substring(0, index); + } + } + for (String cue : List.of("的", "数量", "个数", "多少", "合计", "总计", "求和", "总和", "统计", "sum", "count")) { + int index = value.indexOf(normalize(cue)); + if (index > 0) { + value = value.substring(0, index); + } + } + return value.trim(); + } + private int scorePlan(String question, DocumentTableDescriptor table, DocumentTableQuery query) { int score = 0; score += textMentionScore(question, table.getTitle()) * 3; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityService.java new file mode 100644 index 0000000000000000000000000000000000000000..c2c45bedc2eec72d466688a800cadf2d3716791d --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityService.java @@ -0,0 +1,158 @@ +package org.javaup.ai.chatagent.rag.service; + +import cn.hutool.core.util.StrUtil; +import org.javaup.ai.chatagent.rag.model.EvidenceRole; +import org.javaup.ai.chatagent.rag.model.EvidenceApplicabilityResult; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.springframework.ai.document.Document; +import org.springframework.stereotype.Service; + +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * 判断候选证据是否适用于当前 query understanding 的目标实体。 + */ +@Service +public class EvidenceApplicabilityService { + + private final EvidenceRoleClassifier evidenceRoleClassifier; + + public EvidenceApplicabilityService() { + this(new EvidenceRoleClassifier()); + } + + public EvidenceApplicabilityService(EvidenceRoleClassifier evidenceRoleClassifier) { + this.evidenceRoleClassifier = evidenceRoleClassifier == null ? new EvidenceRoleClassifier() : evidenceRoleClassifier; + } + + public EvidenceApplicabilityResult evaluate(QueryUnderstandingResult understanding, Document document) { + if (understanding == null || document == null) { + return EvidenceApplicabilityResult.unknown("missing understanding or evidence"); + } + EvidenceApplicabilityResult roleResult = evaluateExpectedRole(understanding, document); + if (roleResult != null && !roleResult.isApplicable()) { + return roleResult; + } + List targets = normalizedTerms(understanding.getTargetEntities()); + if (targets.isEmpty()) { + return roleResult == null ? EvidenceApplicabilityResult.unknown("target entity is empty") : roleResult; + } + String evidenceText = normalizedEvidenceText(document); + boolean targetSupported = targets.stream().anyMatch(evidenceText::contains); + if (targetSupported) { + return EvidenceApplicabilityResult.applicable("target entity supported by evidence"); + } + + List excluded = normalizedTerms(understanding.getExcludedEntities()); + boolean excludedOnly = !excluded.isEmpty() && excluded.stream().anyMatch(evidenceText::contains); + if (excludedOnly) { + return EvidenceApplicabilityResult.notApplicable("evidence only supports excluded entity"); + } + + if (understanding.isNegativeBoundary() || explicitEvidenceRequired(understanding)) { + return EvidenceApplicabilityResult.notApplicable("target entity is not explicitly supported by evidence"); + } + return EvidenceApplicabilityResult.unknown("target entity not found in evidence"); + } + + private EvidenceApplicabilityResult evaluateExpectedRole(QueryUnderstandingResult understanding, Document document) { + List expectedRoles = normalizeExpectedRoles(understanding.getExpectedEvidenceRoles()); + EvidenceRole actualRole = evidenceRoleClassifier.classify(document); + if (document.getMetadata() != null) { + document.getMetadata().put(DocumentKnowledgeMetadataKeys.EVIDENCE_ROLE, actualRole.name()); + if (!expectedRoles.isEmpty()) { + document.getMetadata().put( + DocumentKnowledgeMetadataKeys.EXPECTED_EVIDENCE_ROLES, + expectedRoles.stream().map(Enum::name).toList() + ); + } + } + if (expectedRoles.isEmpty()) { + return null; + } + if (actualRole == EvidenceRole.GENERAL) { + return EvidenceApplicabilityResult.unknown("expected evidence role is " + roleText(expectedRoles) + " but evidence role is GENERAL"); + } + if (expectedRoles.contains(actualRole)) { + return EvidenceApplicabilityResult.applicable("evidence role matched: " + actualRole.name()); + } + return EvidenceApplicabilityResult.notApplicable( + "evidence role mismatch: expected " + roleText(expectedRoles) + ", actual " + actualRole.name() + ); + } + + private List normalizeExpectedRoles(List roles) { + if (roles == null || roles.isEmpty()) { + return List.of(); + } + return roles.stream() + .filter(role -> role != null && role != EvidenceRole.GENERAL) + .distinct() + .limit(4) + .toList(); + } + + private String roleText(List roles) { + return roles == null || roles.isEmpty() + ? "GENERAL" + : String.join("/", roles.stream().map(Enum::name).toList()); + } + + private boolean explicitEvidenceRequired(QueryUnderstandingResult understanding) { + return "EXPLICIT_EVIDENCE_REQUIRED".equalsIgnoreCase(StrUtil.blankToDefault(understanding.getAnswerExpectation(), "")); + } + + private List normalizedTerms(List terms) { + if (terms == null || terms.isEmpty()) { + return List.of(); + } + return terms.stream() + .map(this::normalize) + .filter(term -> term.length() >= 2) + .distinct() + .limit(8) + .toList(); + } + + private String normalizedEvidenceText(Document document) { + List values = new ArrayList<>(); + if (document.getMetadata() != null) { + Map metadata = document.getMetadata(); + add(values, metadata.get(DocumentKnowledgeMetadataKeys.KG_ENTITY_NAME)); + add(values, metadata.get(DocumentKnowledgeMetadataKeys.KG_CANONICAL_ENTITY_NAME)); + add(values, metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATED_ENTITY_NAME)); + add(values, metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ENTITIES)); + add(values, metadata.get(DocumentKnowledgeMetadataKeys.TITLE)); + add(values, metadata.get(DocumentKnowledgeMetadataKeys.SECTION_PATH)); + add(values, metadata.get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH)); + add(values, metadata.get(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME)); + } + add(values, document.getText()); + return normalize(String.join(" ", values)); + } + + private void add(List values, Object value) { + if (value == null) { + return; + } + String text = String.valueOf(value); + if (StrUtil.isNotBlank(text)) { + values.add(text); + } + } + + private String normalize(String value) { + if (value == null) { + return ""; + } + return Normalizer.normalize(value, Normalizer.Form.NFKC) + .replaceAll("[\\s>`*#_\\-,,。;;::()()“”\"'\\[\\]{}]+", "") + .toLowerCase(Locale.ROOT) + .trim(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceRoleClassifier.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceRoleClassifier.java new file mode 100644 index 0000000000000000000000000000000000000000..65150219b29ba7b819b16decaeea8d7238a6b669 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceRoleClassifier.java @@ -0,0 +1,36 @@ +package org.javaup.ai.chatagent.rag.service; + +import org.javaup.ai.chatagent.rag.model.EvidenceRole; +import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.springframework.ai.document.Document; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class EvidenceRoleClassifier { + + /** + * Evidence role must come from controlled structured metadata. Do not infer it + * from section titles, question text, or document content with contains rules. + */ + public EvidenceRole classify(Document evidence) { + if (evidence == null || evidence.getMetadata() == null) { + return EvidenceRole.GENERAL; + } + return readStructuredRole(evidence.getMetadata()); + } + + private EvidenceRole readStructuredRole(Map metadata) { + Object raw = metadata.get(DocumentKnowledgeMetadataKeys.EVIDENCE_ROLE); + if (raw == null) { + return EvidenceRole.GENERAL; + } + try { + return EvidenceRole.valueOf(String.valueOf(raw).trim().toUpperCase()); + } + catch (IllegalArgumentException exception) { + return EvidenceRole.GENERAL; + } + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicy.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicy.java new file mode 100644 index 0000000000000000000000000000000000000000..d99acebcc743be03a28ca8a3f5df8bc47057ffe1 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicy.java @@ -0,0 +1,1019 @@ +package org.javaup.ai.chatagent.rag.service; + +import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.QueryType; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; +import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.chatagent.rag.support.EvidenceIdentityResolver; +import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.javaup.enums.RetrievalChannelEnum; +import org.springframework.ai.document.Document; + +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; + +/** + * 最终证据预算策略。只使用结构化 metadata 和受控查询理解结果,不读取业务词。 + */ +public class FinalEvidenceSelectionPolicy { + + public static final String RESERVE_TOP_RANK = "TOP_RANK"; + public static final String RESERVE_SAME_SECTION_BODY = "SAME_SECTION_BODY"; + public static final String RESERVE_STRUCTURE_ANCHOR = "STRUCTURE_ANCHOR"; + public static final String RESERVE_STRUCTURE_ANCHOR_BODY = "STRUCTURE_ANCHOR_BODY"; + public static final String RESERVE_STRUCTURE_DESCENDANT_BODY = "STRUCTURE_DESCENDANT_BODY"; + public static final String RESERVE_STRUCTURE_NAVIGATION_CURRENT = "STRUCTURE_NAVIGATION_CURRENT"; + public static final String RESERVE_STRUCTURE_NAVIGATION_PARENT = "STRUCTURE_NAVIGATION_PARENT"; + public static final String RESERVE_STRUCTURE_NAVIGATION_SIBLING = "STRUCTURE_NAVIGATION_SIBLING"; + public static final String RESERVE_STRUCTURE_NAVIGATION_CHILD = "STRUCTURE_NAVIGATION_CHILD"; + public static final String RESERVE_ROUTE_CANDIDATE_SOURCE = "ROUTE_CANDIDATE_SOURCE"; + public static final String RESERVE_MULTI_DOC_DIVERSITY = "MULTI_DOC_DIVERSITY_RESERVE"; + public static final String RESERVE_GRAPH_RAG_QUOTE = "GRAPH_RAG_QUOTE"; + public static final String RESERVE_RAPTOR_SOURCE_CHUNK = "RAPTOR_SOURCE_CHUNK"; + public static final String SELECTED_TOP_RANK = "SELECTED_TOP_RANK"; + public static final String SELECTED_SAME_SECTION_BODY = "SELECTED_SAME_SECTION_BODY"; + public static final String SELECTED_STRUCTURE_ANCHOR = "SELECTED_STRUCTURE_ANCHOR"; + public static final String SELECTED_STRUCTURE_ANCHOR_BODY = "SELECTED_STRUCTURE_ANCHOR_BODY"; + public static final String SELECTED_STRUCTURE_DESCENDANT_BODY = "SELECTED_STRUCTURE_DESCENDANT_BODY"; + public static final String SELECTED_STRUCTURE_NAVIGATION_CURRENT = "SELECTED_STRUCTURE_NAVIGATION_CURRENT"; + public static final String SELECTED_STRUCTURE_NAVIGATION_PARENT = "SELECTED_STRUCTURE_NAVIGATION_PARENT"; + public static final String SELECTED_STRUCTURE_NAVIGATION_SIBLING = "SELECTED_STRUCTURE_NAVIGATION_SIBLING"; + public static final String SELECTED_STRUCTURE_NAVIGATION_CHILD = "SELECTED_STRUCTURE_NAVIGATION_CHILD"; + public static final String SELECTED_ROUTE_CANDIDATE_RESERVE = "SELECTED_ROUTE_CANDIDATE_RESERVE"; + public static final String SELECTED_MULTI_DOC_DIVERSITY_RESERVE = "SELECTED_MULTI_DOC_DIVERSITY_RESERVE"; + public static final String REPLACED_TITLE_ONLY_WITH_BODY = "REPLACED_TITLE_ONLY_WITH_BODY"; + public static final String SELECTED_GRAPH_RAG_QUOTE = "SELECTED_GRAPH_RAG_QUOTE"; + public static final String SELECTED_RAPTOR_SOURCE_CHUNK = "SELECTED_RAPTOR_SOURCE_CHUNK"; + + private final ChatRagProperties properties; + + public FinalEvidenceSelectionPolicy(ChatRagProperties properties) { + this.properties = properties; + } + + public List select(List rerankedCandidates, ConversationExecutionPlan plan) { + if (rerankedCandidates == null || rerankedCandidates.isEmpty()) { + return List.of(); + } + int finalTopK = Math.max(runtimeOptions(plan).getFinalTopK(), 0); + if (finalTopK <= 0) { + return List.of(); + } + + List selected = new ArrayList<>(rerankedCandidates.stream() + .limit(finalTopK) + .toList()); + selected.forEach(this::markInitialSelection); + reserveStructureAnchorBodyCandidates(rerankedCandidates, selected); + if (rerankedCandidates.size() <= finalTopK) { + return selected; + } + reserveSameSectionBody(rerankedCandidates, selected); + reserveStructureAnchor(rerankedCandidates, selected, plan); + reserveRouteCandidateSourceEvidence(rerankedCandidates, selected, plan); + reserveRaptorSourceEvidence(rerankedCandidates, finalTopK, selected, plan); + reserveGraphRagEvidence(rerankedCandidates, finalTopK, selected, plan); + return selected; + } + + private void reserveStructureAnchorBodyCandidates(List candidates, List selected) { + List bodyCandidates = candidates.stream() + .filter(this::isStructureAnchorBodyCandidate) + .filter(this::isBodyEvidence) + .sorted(Comparator.comparingDouble(this::structureAnchorBodyPriority).reversed()) + .toList(); + if (bodyCandidates.isEmpty()) { + return; + } + for (Document bodyCandidate : bodyCandidates) { + Document alreadySelected = findSameCitationEvidence(selected, bodyCandidate); + String reserveType = structureAnchorReserveType(bodyCandidate); + if (alreadySelected != null) { + markReserve(alreadySelected, reserveType); + continue; + } + int replaceIndex = firstReplaceableTitleOrSummaryOnlyIndex(selected); + boolean replacesTitleOrSummary = replaceIndex >= 0; + if (replaceIndex < 0) { + replaceIndex = weakestReplaceableEvidenceIndex(selected); + } + if (replaceIndex < 0) { + return; + } + String reason = replacesTitleOrSummary ? REPLACED_TITLE_ONLY_WITH_BODY : selectedReasonCode(reserveType); + markReserve(bodyCandidate, reserveType, reason); + selected.set(replaceIndex, bodyCandidate); + } + } + + private void reserveSameSectionBody(List candidates, List selected) { + List selectedTitles = selected.stream() + .filter(this::isTitleEvidence) + .toList(); + if (selectedTitles.isEmpty()) { + return; + } + for (Document title : selectedTitles) { + Document body = candidates.stream() + .filter(candidate -> !containsSameCitationEvidence(selected, candidate)) + .filter(this::isBodyEvidence) + .filter(candidate -> sameStructureAnchor(title, candidate)) + .max(Comparator.comparingDouble(this::finalDocumentScore)) + .orElse(null); + if (body == null) { + continue; + } + replaceWeakestEvidence(selected, body, RESERVE_SAME_SECTION_BODY); + } + } + + private void reserveStructureAnchor(List candidates, + List selected, + ConversationExecutionPlan plan) { + List anchors = sectionAnchors(plan); + if (anchors.isEmpty()) { + return; + } + Document anchored = candidates.stream() + .filter(candidate -> !containsSameCitationEvidence(selected, candidate)) + .filter(this::isBodyEvidence) + .filter(candidate -> matchesAnySectionAnchor(candidate, anchors)) + .max(Comparator.comparingDouble(this::finalDocumentScore)) + .orElse(null); + if (anchored != null) { + replaceWeakestEvidence(selected, anchored, RESERVE_STRUCTURE_ANCHOR); + } + } + + private void reserveGraphRagEvidence(List rerankedCandidates, + int finalTopK, + List selected, + ConversationExecutionPlan plan) { + boolean preferCrossDocumentCommunity = shouldReserveCrossDocumentCommunityEvidence(plan); + if (selected.stream().anyMatch(document -> isRequiredGraphRagReserveCandidate(document, plan, preferCrossDocumentCommunity))) { + return; + } + Document graphRagReserve = selectGraphRagReserveCandidate( + rerankedCandidates, + finalTopK, + selected, + plan, + preferCrossDocumentCommunity + ); + if (graphRagReserve == null && preferCrossDocumentCommunity) { + graphRagReserve = selectGraphRagReserveCandidate(rerankedCandidates, finalTopK, selected, plan, false); + } + if (graphRagReserve != null) { + replaceGraphRagSummaryOnlyOrWeakestEvidence(selected, graphRagReserve, RESERVE_GRAPH_RAG_QUOTE); + } + } + + private void reserveRouteCandidateSourceEvidence(List candidates, + List selected, + ConversationExecutionPlan plan) { + if (!isMultiDocumentAutoRetrieval(plan) || candidates == null || candidates.isEmpty()) { + return; + } + List reserves = candidates.stream() + .filter(this::isRouteCandidateSourceReserve) + .filter(EvidenceIdentityResolver::isCitationCapable) + .filter(candidate -> !containsSameCitationEvidence(selected, candidate)) + .sorted(Comparator.comparingDouble(this::finalDocumentScore).reversed()) + .toList(); + if (reserves.isEmpty()) { + return; + } + for (Document reserve : reserves) { + if (containsSameCitationEvidence(selected, reserve)) { + continue; + } + int replaceIndex = firstReplaceableContextOnlyOrSummaryIndex(selected); + if (replaceIndex < 0) { + replaceIndex = weakestReplaceableEvidenceIndex(selected); + } + if (replaceIndex < 0) { + return; + } + markReserve(reserve, RESERVE_ROUTE_CANDIDATE_SOURCE); + selected.set(replaceIndex, reserve); + } + } + + private void reserveRaptorSourceEvidence(List rerankedCandidates, + int finalTopK, + List selected, + ConversationExecutionPlan plan) { + if (selected.stream().anyMatch(this::isRaptorSourceCandidate)) { + return; + } + Document raptorReserve = rerankedCandidates.stream() + .skip(finalTopK) + .filter(this::isRaptorSourceCandidate) + .filter(candidate -> !containsSameCitationEvidence(selected, candidate)) + .max(Comparator.comparingDouble(document -> raptorEvidenceBudgetPriority(document, plan))) + .orElse(null); + if (raptorReserve != null) { + replaceSummaryOnlyOrWeakestEvidence(selected, raptorReserve, RESERVE_RAPTOR_SOURCE_CHUNK); + } + } + + private Document selectGraphRagReserveCandidate(List rerankedCandidates, + int finalTopK, + List selected, + ConversationExecutionPlan plan, + boolean crossDocumentCommunityOnly) { + return rerankedCandidates.stream() + .skip(finalTopK) + .filter(document -> isGraphRagReserveCandidate(document, plan)) + .filter(document -> !crossDocumentCommunityOnly || isGraphRagCrossDocumentCommunityReserveCandidate(document, plan)) + .filter(candidate -> !containsSameCitationEvidence(selected, candidate)) + .max(Comparator.comparingDouble(document -> graphRagEvidenceBudgetPriority(document, plan))) + .orElse(null); + } + + private void replaceWeakestEvidence(List selected, + Document reserve, + String reserveType) { + if (selected == null || selected.isEmpty() || reserve == null) { + return; + } + if (containsSameCitationEvidence(selected, reserve)) { + return; + } + int replaceIndex = weakestReplaceableEvidenceIndex(selected); + if (replaceIndex < 0) { + return; + } + markReserve(reserve, reserveType); + selected.set(replaceIndex, reserve); + } + + private void replaceSummaryOnlyOrWeakestEvidence(List selected, + Document reserve, + String reserveType) { + if (selected == null || selected.isEmpty() || reserve == null || containsSameCitationEvidence(selected, reserve)) { + return; + } + int replaceIndex = firstReplaceableRaptorSummaryOnlyIndex(selected); + if (replaceIndex < 0) { + replaceIndex = weakestReplaceableEvidenceIndex(selected); + } + if (replaceIndex < 0) { + return; + } + markReserve(reserve, reserveType); + selected.set(replaceIndex, reserve); + } + + private void replaceGraphRagSummaryOnlyOrWeakestEvidence(List selected, + Document reserve, + String reserveType) { + if (selected == null || selected.isEmpty() || reserve == null || containsSameCitationEvidence(selected, reserve)) { + return; + } + int replaceIndex = firstReplaceableGraphRagCommunitySummaryOnlyIndex(selected); + if (replaceIndex < 0) { + replaceIndex = weakestReplaceableEvidenceIndex(selected); + } + if (replaceIndex < 0) { + return; + } + markReserve(reserve, reserveType); + selected.set(replaceIndex, reserve); + } + + private int firstReplaceableRaptorSummaryOnlyIndex(List selected) { + for (int index = 0; index < selected.size(); index++) { + Document document = selected.get(index); + if (!isProtectedReserve(document) && isRaptorSummaryOnly(document)) { + return index; + } + } + return -1; + } + + private int firstReplaceableGraphRagCommunitySummaryOnlyIndex(List selected) { + for (int index = 0; index < selected.size(); index++) { + Document document = selected.get(index); + if (!isProtectedReserve(document) && isGraphRagCommunitySummaryOnly(document)) { + return index; + } + } + return -1; + } + + private int firstReplaceableTitleOrSummaryOnlyIndex(List selected) { + for (int index = 0; index < selected.size(); index++) { + Document document = selected.get(index); + if (isProtectedReserve(document)) { + continue; + } + if (isTitleEvidence(document) || isRaptorSummaryOnly(document) || isGraphRagCommunitySummaryOnly(document)) { + return index; + } + } + return -1; + } + + private int firstReplaceableContextOnlyOrSummaryIndex(List selected) { + for (int index = 0; index < selected.size(); index++) { + Document document = selected.get(index); + if (isProtectedReserve(document)) { + continue; + } + if (EvidenceIdentityResolver.isContextOnly(document) + || isTitleEvidence(document) + || isRaptorSummaryOnly(document) + || isGraphRagCommunitySummaryOnly(document)) { + return index; + } + } + return -1; + } + + private int weakestReplaceableEvidenceIndex(List selected) { + int replaceIndex = -1; + double weakestScore = Double.MAX_VALUE; + for (int index = 0; index < selected.size(); index++) { + Document document = selected.get(index); + if (isProtectedReserve(document)) { + continue; + } + double score = finalDocumentScore(document); + if (score < weakestScore) { + weakestScore = score; + replaceIndex = index; + } + } + return replaceIndex >= 0 ? replaceIndex : selected.size() - 1; + } + + private boolean isProtectedReserve(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + String reserveType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE)); + return RESERVE_SAME_SECTION_BODY.equals(reserveType) + || RESERVE_STRUCTURE_ANCHOR.equals(reserveType) + || RESERVE_STRUCTURE_ANCHOR_BODY.equals(reserveType) + || RESERVE_STRUCTURE_DESCENDANT_BODY.equals(reserveType) + || isStructureNavigationReserveType(reserveType) + || RESERVE_ROUTE_CANDIDATE_SOURCE.equals(reserveType) + || RESERVE_MULTI_DOC_DIVERSITY.equals(reserveType) + || RESERVE_GRAPH_RAG_QUOTE.equals(reserveType) + || RESERVE_RAPTOR_SOURCE_CHUNK.equals(reserveType); + } + + private void markReserve(Document document, String reserveType) { + if (document == null || document.getMetadata() == null) { + return; + } + document.getMetadata().put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, selectedReasonCode(reserveType)); + document.getMetadata().putIfAbsent(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, reserveType); + } + + private void markInitialSelection(Document document) { + String reserveType = safeText(document == null || document.getMetadata() == null + ? null + : document.getMetadata().get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE)); + if (isStructureNavigationReserveType(reserveType)) { + markReserve(document, reserveType, selectedReasonCode(reserveType)); + return; + } + if (RESERVE_ROUTE_CANDIDATE_SOURCE.equals(reserveType) || RESERVE_MULTI_DOC_DIVERSITY.equals(reserveType)) { + markReserve(document, reserveType, selectedReasonCode(reserveType)); + return; + } + markReserve(document, RESERVE_TOP_RANK); + } + + private void markReserve(Document document, String reserveType, String reasonCode) { + if (document == null || document.getMetadata() == null) { + return; + } + document.getMetadata().put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, reasonCode); + document.getMetadata().put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, reserveType); + } + + private String selectedReasonCode(String reserveType) { + return switch (reserveType) { + case RESERVE_SAME_SECTION_BODY -> SELECTED_SAME_SECTION_BODY; + case RESERVE_STRUCTURE_ANCHOR -> SELECTED_STRUCTURE_ANCHOR; + case RESERVE_STRUCTURE_ANCHOR_BODY -> SELECTED_STRUCTURE_ANCHOR_BODY; + case RESERVE_STRUCTURE_DESCENDANT_BODY -> SELECTED_STRUCTURE_DESCENDANT_BODY; + case RESERVE_STRUCTURE_NAVIGATION_CURRENT -> SELECTED_STRUCTURE_NAVIGATION_CURRENT; + case RESERVE_STRUCTURE_NAVIGATION_PARENT -> SELECTED_STRUCTURE_NAVIGATION_PARENT; + case RESERVE_STRUCTURE_NAVIGATION_SIBLING -> SELECTED_STRUCTURE_NAVIGATION_SIBLING; + case RESERVE_STRUCTURE_NAVIGATION_CHILD -> SELECTED_STRUCTURE_NAVIGATION_CHILD; + case RESERVE_ROUTE_CANDIDATE_SOURCE -> SELECTED_ROUTE_CANDIDATE_RESERVE; + case RESERVE_MULTI_DOC_DIVERSITY -> SELECTED_MULTI_DOC_DIVERSITY_RESERVE; + case RESERVE_GRAPH_RAG_QUOTE -> SELECTED_GRAPH_RAG_QUOTE; + case RESERVE_RAPTOR_SOURCE_CHUNK -> SELECTED_RAPTOR_SOURCE_CHUNK; + default -> SELECTED_TOP_RANK; + }; + } + + private boolean isStructureNavigationReserveType(String reserveType) { + return RESERVE_STRUCTURE_NAVIGATION_CURRENT.equals(reserveType) + || RESERVE_STRUCTURE_NAVIGATION_PARENT.equals(reserveType) + || RESERVE_STRUCTURE_NAVIGATION_SIBLING.equals(reserveType) + || RESERVE_STRUCTURE_NAVIGATION_CHILD.equals(reserveType); + } + + private boolean isTitleEvidence(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + String chunkType = normalizeSimple(safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE))); + if ("title".equals(chunkType) || "heading".equals(chunkType)) { + return true; + } + String nodeType = normalizeSimple(safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_TYPE))); + if ("title".equals(nodeType) || "heading".equals(nodeType)) { + return true; + } + String text = safeText(document.getText()); + return text.length() <= 120 && (text.startsWith("#") || text.matches("^\\d+(\\.\\d+){1,5}\\s+\\S.*$")); + } + + private boolean isBodyEvidence(Document document) { + if (document == null) { + return false; + } + if (isTechnicalWrapperEvidence(document)) { + return false; + } + if (isTitleEvidence(document)) { + return false; + } + return safeText(document.getText()).length() >= 12; + } + + private boolean isStructureAnchorBodyCandidate(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + Map metadata = document.getMetadata(); + if (!booleanMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_RAW_BODY))) { + return false; + } + if (!EvidenceIdentityResolver.isCitationCapable(document)) { + return false; + } + String reserveType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE)); + String channel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL)); + Object bypass = metadata.get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW); + return "STRUCTURE_ANCHOR_BODY_CANDIDATE".equalsIgnoreCase(reserveType) + || "structure-anchor".equalsIgnoreCase(channel) + || Boolean.TRUE.equals(bypass) + || Boolean.parseBoolean(String.valueOf(bypass)); + } + + private boolean isRouteCandidateSourceReserve(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + String reserveType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE)); + return RESERVE_ROUTE_CANDIDATE_SOURCE.equals(reserveType); + } + + private boolean isMultiDocumentAutoRetrieval(ConversationExecutionPlan plan) { + return plan != null + && plan.getRetrievalDocumentIds() != null + && plan.getRetrievalDocumentIds().stream().filter(Objects::nonNull).distinct().limit(2).count() > 1; + } + + private boolean isTechnicalWrapperEvidence(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + Map metadata = document.getMetadata(); + String chunkType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE)); + if ("RAPTOR_SUMMARY".equalsIgnoreCase(chunkType)) { + return true; + } + if (isRaptorSummaryOnly(document) || isGraphRagCommunitySummaryOnly(document)) { + return true; + } + String channel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL)); + String sourceType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.SOURCE_TYPE)); + if ("graph-rag".equalsIgnoreCase(channel) || "GRAPH_RAG".equalsIgnoreCase(sourceType)) { + return !hasGraphRagSourceQuote(document); + } + String text = safeText(document.getText()).trim(); + return text.startsWith("[GraphRAG") || text.startsWith("[RAPTOR"); + } + + private String structureAnchorReserveType(Document document) { + if (document == null || document.getMetadata() == null) { + return RESERVE_STRUCTURE_ANCHOR_BODY; + } + String matchType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE)); + return "CANONICAL_DESCENDANT".equalsIgnoreCase(matchType) + ? RESERVE_STRUCTURE_DESCENDANT_BODY + : RESERVE_STRUCTURE_ANCHOR_BODY; + } + + private double structureAnchorBodyPriority(Document document) { + if (document == null || document.getMetadata() == null) { + return 0D; + } + double priority = finalDocumentScore(document) + 2D; + String matchType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE)); + if ("NODE_ID".equalsIgnoreCase(matchType)) { + priority += 0.6D; + } + else if ("CANONICAL_EXACT".equalsIgnoreCase(matchType)) { + priority += 0.45D; + } + else if ("CANONICAL_DESCENDANT".equalsIgnoreCase(matchType)) { + priority += 0.25D; + } + if (isMeaningfulMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID))) { + priority += 0.3D; + } + return priority; + } + + private boolean sameStructureAnchor(Document left, Document right) { + if (left == null || right == null || left.getMetadata() == null || right.getMetadata() == null) { + return false; + } + Long leftDocumentId = longMetadataValue(left.getMetadata().get(DocumentKnowledgeMetadataKeys.DOCUMENT_ID)); + Long rightDocumentId = longMetadataValue(right.getMetadata().get(DocumentKnowledgeMetadataKeys.DOCUMENT_ID)); + if (leftDocumentId != null && rightDocumentId != null && !Objects.equals(leftDocumentId, rightDocumentId)) { + return false; + } + Long leftNodeId = longMetadataValue(left.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID)); + Long rightNodeId = longMetadataValue(right.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID)); + if (leftNodeId != null && rightNodeId != null && Objects.equals(leftNodeId, rightNodeId)) { + return true; + } + String leftCanonical = normalizedAnchor(left.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH)); + String rightCanonical = normalizedAnchor(right.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH)); + if (!leftCanonical.isBlank() && leftCanonical.equals(rightCanonical)) { + return true; + } + String leftSection = normalizedAnchor(left.getMetadata().get(DocumentKnowledgeMetadataKeys.SECTION_PATH)); + String rightSection = normalizedAnchor(right.getMetadata().get(DocumentKnowledgeMetadataKeys.SECTION_PATH)); + return !leftSection.isBlank() && leftSection.equals(rightSection); + } + + private boolean matchesAnySectionAnchor(Document document, List anchors) { + if (document == null || document.getMetadata() == null || anchors == null || anchors.isEmpty()) { + return false; + } + String section = normalizedAnchor(document.getMetadata().get(DocumentKnowledgeMetadataKeys.SECTION_PATH)); + String canonical = normalizedAnchor(document.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH)); + String title = normalizedAnchor(document.getMetadata().get(DocumentKnowledgeMetadataKeys.TITLE)); + return anchors.stream() + .map(this::normalizedAnchor) + .filter(anchor -> !anchor.isBlank()) + .anyMatch(anchor -> equalsOrContains(section, anchor) + || equalsOrContains(canonical, anchor) + || equalsOrContains(title, anchor)); + } + + private boolean equalsOrContains(String value, String anchor) { + if (value == null || value.isBlank() || anchor == null || anchor.isBlank()) { + return false; + } + return value.equals(anchor) || value.contains(anchor) || anchor.contains(value); + } + + private List sectionAnchors(ConversationExecutionPlan plan) { + QueryUnderstandingResult queryUnderstanding = plan == null ? null : plan.getQueryUnderstanding(); + if (queryUnderstanding == null || queryUnderstanding.getSectionAnchors() == null) { + return List.of(); + } + return queryUnderstanding.getSectionAnchors(); + } + + private boolean containsSameCitationEvidence(List documents, Document candidate) { + return documents != null + && documents.stream().anyMatch(document -> sameCitationEvidence(document, candidate)); + } + + private Document findSameCitationEvidence(List documents, Document candidate) { + if (documents == null || candidate == null) { + return null; + } + return documents.stream() + .filter(document -> sameCitationEvidence(document, candidate)) + .findFirst() + .orElse(null); + } + + private boolean sameCitationEvidence(Document left, Document right) { + if (left == null || right == null) { + return false; + } + if (Objects.equals(left.getId(), right.getId())) { + return true; + } + return EvidenceIdentityResolver.sameCitationEvidence(left, right); + } + + private boolean isRequiredGraphRagReserveCandidate(Document document, + ConversationExecutionPlan plan, + boolean preferCrossDocumentCommunity) { + if (!isGraphRagReserveCandidate(document, plan)) { + return false; + } + return !preferCrossDocumentCommunity || isGraphRagCrossDocumentCommunityReserveCandidate(document, plan); + } + + private boolean isGraphRagReserveCandidate(Document document, ConversationExecutionPlan plan) { + if (document == null || document.getMetadata() == null || !isGraphRagMetadata(document.getMetadata())) { + return false; + } + Map metadata = document.getMetadata(); + if (isGraphRagCommunitySummaryOnly(document)) { + return false; + } + if (isGraphRagCommunityReportReserveCandidate(document, plan)) { + return true; + } + boolean hasRelationEvidence = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID)) + && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)) + && hasGraphRagSourceQuote(document); + if (!hasRelationEvidence) { + return false; + } + if (!hasGraphRagRelationGroundingContext(metadata)) { + return false; + } + Double qualityScore = numericMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE)); + return qualityScore == null || qualityScore >= 0.55D; + } + + private boolean isGraphRagCrossDocumentCommunityReserveCandidate(Document document, ConversationExecutionPlan plan) { + if (!isGraphRagCommunityReportReserveCandidate(document, plan)) { + return false; + } + Map metadata = document.getMetadata(); + return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY)); + } + + private boolean isGraphRagCommunityReportReserveCandidate(Document document, ConversationExecutionPlan plan) { + if (document == null || document.getMetadata() == null || !isGraphRagMetadata(document.getMetadata()) + || !shouldReserveCrossDocumentCommunityEvidence(plan)) { + return false; + } + Map metadata = document.getMetadata(); + if (!isGraphRagCommunityReportCandidate(metadata)) { + return false; + } + if (isGraphRagCommunitySummaryOnly(document)) { + return false; + } + Integer communityDocumentCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_DOCUMENT_COUNT)); + if (communityDocumentCount != null && communityDocumentCount < 2) { + return false; + } + boolean grounded = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)) + && hasGraphRagSourceQuote(document) + && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY)); + if (!grounded) { + return false; + } + Double qualityScore = numericMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE)); + return qualityScore == null || qualityScore >= 0.55D; + } + + private boolean isRaptorSourceCandidate(Document document) { + if (document == null || document.getMetadata() == null || !isRaptorMetadata(document.getMetadata())) { + return false; + } + if (isRaptorSummaryOnly(document)) { + return false; + } + Map metadata = document.getMetadata(); + return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID)); + } + + private boolean isRaptorSummaryOnly(Document document) { + if (document == null || document.getMetadata() == null || !isRaptorMetadata(document.getMetadata())) { + return false; + } + Map metadata = document.getMetadata(); + String sourceStatus = safeText(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS)); + if ("SUMMARY_ONLY".equalsIgnoreCase(sourceStatus)) { + return true; + } + String chunkType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE)); + if ("RAPTOR_SUMMARY".equalsIgnoreCase(chunkType)) { + return true; + } + return !isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID)) + && !isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID)); + } + + private double raptorEvidenceBudgetPriority(Document document, ConversationExecutionPlan plan) { + if (document == null || document.getMetadata() == null) { + return 0D; + } + Map metadata = document.getMetadata(); + double priority = finalDocumentScore(document); + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID))) { + priority += 1.0D; + } + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID))) { + priority += 0.55D; + } + if (isSuggestedChannel(RetrievalChannelEnum.RAPTOR.getName(), plan) + || resolveRetrievalIntent(plan) == RetrievalIntent.RAPTOR) { + priority += 0.25D; + } + Integer nodeLevel = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_LEVEL)); + if (nodeLevel != null && nodeLevel <= 1) { + priority += 0.08D; + } + return priority; + } + + private boolean isGraphRagCommunityReportCandidate(Map metadata) { + if (metadata == null) { + return false; + } + boolean hasCommunityIdentity = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_ID)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY)); + if (!hasCommunityIdentity) { + return false; + } + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_ENTITY_ID))) { + return false; + } + return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_TITLE)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY)); + } + + private boolean shouldReserveCrossDocumentCommunityEvidence(ConversationExecutionPlan plan) { + if (plan == null) { + return false; + } + RetrievalIntent intent = resolveRetrievalIntent(plan); + if (intent == RetrievalIntent.GRAPH_RAG || intent == RetrievalIntent.RAPTOR) { + return true; + } + QueryUnderstandingResult queryUnderstanding = plan.getQueryUnderstanding(); + if (queryUnderstanding == null) { + return false; + } + QueryType queryType = queryUnderstanding.getQueryType(); + if (queryType == QueryType.GRAPH_RELATION || queryType == QueryType.GLOBAL_SUMMARY) { + return true; + } + List channels = queryUnderstanding.getChannels(); + return channels != null + && (channels.contains(RetrievalIntent.GRAPH_RAG) || channels.contains(RetrievalIntent.RAPTOR)); + } + + private double graphRagEvidenceBudgetPriority(Document document, ConversationExecutionPlan plan) { + if (document == null || document.getMetadata() == null) { + return 0D; + } + Map metadata = document.getMetadata(); + double priority = finalDocumentScore(document); + if (isGraphRagCommunitySummaryOnly(document)) { + priority -= 2.0D; + } + if (hasGraphRagSourceQuote(document)) { + priority += 0.75D; + } + if (isGraphRagCommunityReportReserveCandidate(document, plan)) { + priority += 1.35D; + Integer communityDocumentCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_DOCUMENT_COUNT)); + Integer communityEvidenceCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_EVIDENCE_COUNT)); + Integer communityRelationGroupCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_RELATION_GROUP_COUNT)); + if (communityDocumentCount != null && communityDocumentCount > 1) { + priority += Math.min(0.36D, communityDocumentCount * 0.08D); + } + if (communityEvidenceCount != null && communityEvidenceCount > 1) { + priority += Math.min(0.30D, communityEvidenceCount * 0.04D); + } + if (communityRelationGroupCount != null && communityRelationGroupCount > 1) { + priority += Math.min(0.24D, communityRelationGroupCount * 0.04D); + } + } + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID)) + && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)) + && hasGraphRagSourceQuote(document)) { + priority += 1.10D; + } + String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL)); + if ("RELATION_STRONG_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 0.55D; + } + else if ("RELATION_WEAK_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 0.25D; + } + else if (groundingLevel.toUpperCase(Locale.ROOT).startsWith("RELATION_")) { + priority += 0.15D; + } + else if ("COMMUNITY_SOURCE_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 0.12D; + } + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH))) { + priority += 0.55D; + } + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE))) { + priority += 0.45D; + } + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ANSWER_TYPES))) { + priority += 0.20D; + } + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ENTITIES))) { + priority += 0.16D; + } + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY))) { + priority += 0.35D; + } + if (isSuggestedChannel(RetrievalChannelEnum.GRAPH_RAG.getName(), plan)) { + priority += 0.08D; + } + Double qualityScore = numericMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE)); + if (qualityScore != null) { + priority += Math.min(0.45D, Math.max(0D, qualityScore) * 0.45D); + } + return priority; + } + + private boolean hasGraphRagRelationGroundingContext(Map metadata) { + if (metadata == null) { + return false; + } + String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL)); + return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY)) + || groundingLevel.toUpperCase(Locale.ROOT).startsWith("RELATION_"); + } + + private boolean isGraphRagCommunitySummaryOnly(Document document) { + if (document == null || document.getMetadata() == null || !isGraphRagMetadata(document.getMetadata())) { + return false; + } + Map metadata = document.getMetadata(); + Object summaryOnly = metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY); + if (summaryOnly instanceof Boolean bool) { + return bool; + } + if (summaryOnly != null && Boolean.parseBoolean(String.valueOf(summaryOnly))) { + return true; + } + String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL)); + if ("COMMUNITY_SUMMARY_ONLY".equalsIgnoreCase(groundingLevel)) { + return true; + } + return isGraphRagCommunityReportCandidate(metadata) && !hasGraphRagSourceQuote(document); + } + + private boolean hasGraphRagSourceQuote(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + Object originalSnippet = document.getMetadata().get(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET); + return isMeaningfulMetadataValue(originalSnippet) + || (isMeaningfulMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)) + && isMeaningfulMetadataValue(document.getText())); + } + + private boolean isSuggestedChannel(String channelName, ConversationExecutionPlan plan) { + QueryUnderstandingResult queryUnderstanding = plan == null ? null : plan.getQueryUnderstanding(); + if (queryUnderstanding == null || queryUnderstanding.getChannels() == null || queryUnderstanding.getChannels().isEmpty()) { + return false; + } + RetrievalIntent channelIntent = channelIntent(channelName); + return channelIntent != null && queryUnderstanding.getChannels().contains(channelIntent); + } + + private RetrievalIntent channelIntent(String channelName) { + if (RetrievalChannelEnum.TABLE.getName().equals(channelName)) { + return RetrievalIntent.TABLE; + } + if (RetrievalChannelEnum.GRAPH_RAG.getName().equals(channelName)) { + return RetrievalIntent.GRAPH_RAG; + } + if (RetrievalChannelEnum.RAPTOR.getName().equals(channelName)) { + return RetrievalIntent.RAPTOR; + } + if (RetrievalChannelEnum.VECTOR.getName().equals(channelName) + || RetrievalChannelEnum.KEYWORD.getName().equals(channelName)) { + return RetrievalIntent.GENERAL; + } + return null; + } + + private RetrievalIntent resolveRetrievalIntent(ConversationExecutionPlan plan) { + return plan == null || plan.getRetrievalIntent() == null ? RetrievalIntent.GENERAL : plan.getRetrievalIntent(); + } + + private boolean isGraphRagMetadata(Map metadata) { + String channel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL)); + String sourceType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.SOURCE_TYPE)); + return RetrievalChannelEnum.GRAPH_RAG.getName().equals(channel) + || "GRAPH_RAG".equalsIgnoreCase(sourceType) + || metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID) != null + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CANONICAL_ENTITY_KEY)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY)) + || metadata.get(DocumentKnowledgeMetadataKeys.KG_ENTITY_ID) != null + || metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID) != null; + } + + private boolean isRaptorMetadata(Map metadata) { + if (metadata == null) { + return false; + } + String channel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL)); + String sourceType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.SOURCE_TYPE)); + return RetrievalChannelEnum.RAPTOR.getName().equals(channel) + || "RAPTOR".equalsIgnoreCase(sourceType) + || metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID) != null; + } + + private boolean isMeaningfulMetadataValue(Object value) { + if (value == null) { + return false; + } + return !(value instanceof String text) || !text.isBlank(); + } + + private boolean booleanMetadataValue(Object value) { + if (value instanceof Boolean bool) { + return bool; + } + if (value == null) { + return false; + } + return Boolean.parseBoolean(String.valueOf(value)); + } + + private double finalDocumentScore(Document document) { + if (document == null) { + return 0D; + } + Double score = numericMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.SCORE)); + if (score != null) { + return score; + } + return document.getScore() == null ? 0D : document.getScore(); + } + + private RagRuntimeOptions runtimeOptions(ConversationExecutionPlan plan) { + return RagRuntimeOptions.resolve(plan, properties); + } + + private Integer integerMetadataValue(Object value) { + Double number = numericMetadataValue(value); + return number == null ? null : number.intValue(); + } + + private Long longMetadataValue(Object value) { + Double number = numericMetadataValue(value); + return number == null ? null : number.longValue(); + } + + private Double numericMetadataValue(Object value) { + if (value instanceof Number number) { + return number.doubleValue(); + } + if (value == null) { + return null; + } + try { + return Double.parseDouble(String.valueOf(value)); + } + catch (NumberFormatException exception) { + return null; + } + } + + private String normalizedAnchor(Object value) { + return normalizeSimple(safeText(value) + .replaceAll("[\\s>`*#_\\-,,。;;::()()“”\"'\\[\\]{}]+", "")); + } + + private String normalizeSimple(String value) { + if (value == null) { + return ""; + } + return Normalizer.normalize(value, Normalizer.Form.NFKC) + .trim() + .toLowerCase(Locale.ROOT); + } + + private String safeText(Object value) { + return value == null ? "" : String.valueOf(value).trim(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolver.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..4cb841aae3d2aa577f5541c07599e6def409549f --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolver.java @@ -0,0 +1,261 @@ +package org.javaup.ai.chatagent.rag.service; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +@Slf4j +@Service +public class KnowledgeBaseRuntimeConfigResolver { + + private final ChatRagProperties properties; + private final ObjectMapper objectMapper; + + public KnowledgeBaseRuntimeConfigResolver(ChatRagProperties properties) { + this.properties = properties; + this.objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public RagRuntimeOptions resolve(List knowledgeBases) { + RagRuntimeOptions options = RagRuntimeOptions.from(properties); + List configs = knowledgeBases == null + ? List.of() + : knowledgeBases.stream() + .filter(Objects::nonNull) + .map(this::parseConfig) + .toList(); + if (configs.isEmpty()) { + return options; + } + if (configs.size() == 1) { + applySingle(options, configs.get(0)); + return options; + } + applyMerged(options, configs); + return options; + } + + private void applySingle(RagRuntimeOptions options, RuntimeConfig config) { + applyIfPresent(config.getVectorTopK(), options::setVectorTopK); + applyIfPresent(config.getKeywordTopK(), options::setKeywordTopK); + applyIfPresent(config.getGraphRagTopK(), options::setGraphRagTopK); + applyIfPresent(config.getGraphRagMaxHops(), options::setGraphRagMaxHops); + applyIfPresent(config.getRaptorTopK(), options::setRaptorTopK); + applyIfPresent(config.getRaptorSourceChunkTopK(), options::setRaptorSourceChunkTopK); + applyIfPresent(config.getCandidateTopK(), options::setCandidateTopK); + applyIfPresent(config.getRerankCandidateTopK(), options::setRerankCandidateTopK); + applyIfPresent(config.getReserveCandidateTopK(), options::setReserveCandidateTopK); + applyIfPresent(config.getFinalTopK(), options::setFinalTopK); + applyIfPresent(config.getMinVectorSimilarity(), options::setMinVectorSimilarity); + applyIfPresent(config.getKeywordRelativeScoreFloor(), options::setKeywordRelativeScoreFloor); + applyIfPresent(config.getKeywordChannelEnabled(), options::setKeywordChannelEnabled); + applyIfPresent(config.getTableChannelEnabled(), options::setTableChannelEnabled); + applyIfPresent(config.getGraphRagChannelEnabled(), options::setGraphRagChannelEnabled); + applyIfPresent(config.getRaptorChannelEnabled(), options::setRaptorChannelEnabled); + applySingleHybrid(options.getHybrid(), config.getHybrid()); + } + + private void applySingleHybrid(RagRuntimeOptions.HybridOptions options, HybridConfig config) { + if (config == null || options == null) { + return; + } + applyIfPresent(config.getVectorWeight(), options::setVectorWeight); + applyIfPresent(config.getKeywordWeight(), options::setKeywordWeight); + applyIfPresent(config.getTableWeight(), options::setTableWeight); + applyIfPresent(config.getGraphRagWeight(), options::setGraphRagWeight); + applyIfPresent(config.getRaptorWeight(), options::setRaptorWeight); + applyIfPresent(config.getRankWeight(), options::setRankWeight); + applyIfPresent(config.getOriginalScoreWeight(), options::setOriginalScoreWeight); + applyIfPresent(config.getMetadataBoostWeight(), options::setMetadataBoostWeight); + applyIfPresent(config.getMaxMetadataBoost(), options::setMaxMetadataBoost); + } + + private void applyMerged(RagRuntimeOptions options, List configs) { + List conflicts = new ArrayList<>(); + mergeField(configs, RuntimeConfig::getVectorTopK, options::setVectorTopK, "vectorTopK", conflicts); + mergeField(configs, RuntimeConfig::getKeywordTopK, options::setKeywordTopK, "keywordTopK", conflicts); + mergeField(configs, RuntimeConfig::getGraphRagTopK, options::setGraphRagTopK, "graphRagTopK", conflicts); + mergeField(configs, RuntimeConfig::getGraphRagMaxHops, options::setGraphRagMaxHops, "graphRagMaxHops", conflicts); + mergeField(configs, RuntimeConfig::getRaptorTopK, options::setRaptorTopK, "raptorTopK", conflicts); + mergeField(configs, RuntimeConfig::getRaptorSourceChunkTopK, options::setRaptorSourceChunkTopK, "raptorSourceChunkTopK", conflicts); + mergeField(configs, RuntimeConfig::getCandidateTopK, options::setCandidateTopK, "candidateTopK", conflicts); + mergeField(configs, RuntimeConfig::getRerankCandidateTopK, options::setRerankCandidateTopK, "rerankCandidateTopK", conflicts); + mergeField(configs, RuntimeConfig::getReserveCandidateTopK, options::setReserveCandidateTopK, "reserveCandidateTopK", conflicts); + mergeField(configs, RuntimeConfig::getFinalTopK, options::setFinalTopK, "finalTopK", conflicts); + mergeField(configs, RuntimeConfig::getMinVectorSimilarity, options::setMinVectorSimilarity, "minVectorSimilarity", conflicts); + mergeField(configs, RuntimeConfig::getKeywordRelativeScoreFloor, options::setKeywordRelativeScoreFloor, "keywordRelativeScoreFloor", conflicts); + mergeField(configs, RuntimeConfig::getKeywordChannelEnabled, options::setKeywordChannelEnabled, "keywordChannelEnabled", conflicts); + mergeField(configs, RuntimeConfig::getTableChannelEnabled, options::setTableChannelEnabled, "tableChannelEnabled", conflicts); + mergeField(configs, RuntimeConfig::getGraphRagChannelEnabled, options::setGraphRagChannelEnabled, "graphRagChannelEnabled", conflicts); + mergeField(configs, RuntimeConfig::getRaptorChannelEnabled, options::setRaptorChannelEnabled, "raptorChannelEnabled", conflicts); + + mergeHybridField(configs, HybridConfig::getVectorWeight, options.getHybrid()::setVectorWeight, "hybrid.vectorWeight", conflicts); + mergeHybridField(configs, HybridConfig::getKeywordWeight, options.getHybrid()::setKeywordWeight, "hybrid.keywordWeight", conflicts); + mergeHybridField(configs, HybridConfig::getTableWeight, options.getHybrid()::setTableWeight, "hybrid.tableWeight", conflicts); + mergeHybridField(configs, HybridConfig::getGraphRagWeight, options.getHybrid()::setGraphRagWeight, "hybrid.graphRagWeight", conflicts); + mergeHybridField(configs, HybridConfig::getRaptorWeight, options.getHybrid()::setRaptorWeight, "hybrid.raptorWeight", conflicts); + mergeHybridField(configs, HybridConfig::getRankWeight, options.getHybrid()::setRankWeight, "hybrid.rankWeight", conflicts); + mergeHybridField(configs, HybridConfig::getOriginalScoreWeight, options.getHybrid()::setOriginalScoreWeight, "hybrid.originalScoreWeight", conflicts); + mergeHybridField(configs, HybridConfig::getMetadataBoostWeight, options.getHybrid()::setMetadataBoostWeight, "hybrid.metadataBoostWeight", conflicts); + mergeHybridField(configs, HybridConfig::getMaxMetadataBoost, options.getHybrid()::setMaxMetadataBoost, "hybrid.maxMetadataBoost", conflicts); + + options.setKbConfigConflictFields(new ArrayList<>(new LinkedHashSet<>(conflicts))); + } + + private void mergeHybridField(List configs, + Function getter, + java.util.function.Consumer setter, + String field, + List conflicts) { + mergeField(configs, + config -> config.getHybrid() == null ? null : getter.apply(config.getHybrid()), + setter, + field, + conflicts); + } + + private void mergeField(List configs, + Function getter, + java.util.function.Consumer setter, + String field, + List conflicts) { + List values = configs.stream().map(getter).toList(); + if (values.stream().allMatch(Objects::isNull)) { + return; + } + if (values.stream().anyMatch(Objects::isNull)) { + conflicts.add(field); + return; + } + T first = values.get(0); + boolean same = values.stream().allMatch(value -> Objects.equals(first, value)); + if (same) { + setter.accept(first); + } + else { + conflicts.add(field); + } + } + + private RuntimeConfig parseConfig(SuperAgentKnowledgeBase knowledgeBase) { + RuntimeConfig merged = new RuntimeConfig(); + mergeInto(merged, parseJson(knowledgeBase.getRetrievalConfigJson(), knowledgeBase)); + mergeInto(merged, parseJson(knowledgeBase.getGraphRagConfigJson(), knowledgeBase)); + mergeInto(merged, parseJson(knowledgeBase.getRaptorConfigJson(), knowledgeBase)); + return merged; + } + + private RuntimeConfig parseJson(String rawJson, SuperAgentKnowledgeBase knowledgeBase) { + if (StrUtil.isBlank(rawJson)) { + return new RuntimeConfig(); + } + try { + return objectMapper.readValue(rawJson, RuntimeConfig.class); + } + catch (JsonProcessingException | RuntimeException exception) { + log.warn("知识库 RAG 配置 JSON 解析失败,将忽略该段配置: knowledgeBaseId={}, knowledgeBaseName={}", + knowledgeBase == null ? null : knowledgeBase.getId(), + knowledgeBase == null ? "" : knowledgeBase.getBaseName(), + exception); + return new RuntimeConfig(); + } + } + + private void mergeInto(RuntimeConfig target, RuntimeConfig source) { + if (source == null) { + return; + } + copyIfPresent(source.getVectorTopK(), target::setVectorTopK); + copyIfPresent(source.getKeywordTopK(), target::setKeywordTopK); + copyIfPresent(source.getGraphRagTopK(), target::setGraphRagTopK); + copyIfPresent(source.getGraphRagMaxHops(), target::setGraphRagMaxHops); + copyIfPresent(source.getRaptorTopK(), target::setRaptorTopK); + copyIfPresent(source.getRaptorSourceChunkTopK(), target::setRaptorSourceChunkTopK); + copyIfPresent(source.getCandidateTopK(), target::setCandidateTopK); + copyIfPresent(source.getRerankCandidateTopK(), target::setRerankCandidateTopK); + copyIfPresent(source.getReserveCandidateTopK(), target::setReserveCandidateTopK); + copyIfPresent(source.getFinalTopK(), target::setFinalTopK); + copyIfPresent(source.getMinVectorSimilarity(), target::setMinVectorSimilarity); + copyIfPresent(source.getKeywordRelativeScoreFloor(), target::setKeywordRelativeScoreFloor); + copyIfPresent(source.getKeywordChannelEnabled(), target::setKeywordChannelEnabled); + copyIfPresent(source.getTableChannelEnabled(), target::setTableChannelEnabled); + copyIfPresent(source.getGraphRagChannelEnabled(), target::setGraphRagChannelEnabled); + copyIfPresent(source.getRaptorChannelEnabled(), target::setRaptorChannelEnabled); + if (source.getHybrid() != null) { + if (target.getHybrid() == null) { + target.setHybrid(new HybridConfig()); + } + mergeHybridInto(target.getHybrid(), source.getHybrid()); + } + } + + private void mergeHybridInto(HybridConfig target, HybridConfig source) { + copyIfPresent(source.getVectorWeight(), target::setVectorWeight); + copyIfPresent(source.getKeywordWeight(), target::setKeywordWeight); + copyIfPresent(source.getTableWeight(), target::setTableWeight); + copyIfPresent(source.getGraphRagWeight(), target::setGraphRagWeight); + copyIfPresent(source.getRaptorWeight(), target::setRaptorWeight); + copyIfPresent(source.getRankWeight(), target::setRankWeight); + copyIfPresent(source.getOriginalScoreWeight(), target::setOriginalScoreWeight); + copyIfPresent(source.getMetadataBoostWeight(), target::setMetadataBoostWeight); + copyIfPresent(source.getMaxMetadataBoost(), target::setMaxMetadataBoost); + } + + private void applyIfPresent(T value, java.util.function.Consumer setter) { + copyIfPresent(value, setter); + } + + private void copyIfPresent(T value, java.util.function.Consumer setter) { + if (value != null) { + setter.accept(value); + } + } + + @Data + public static class RuntimeConfig { + private Integer vectorTopK; + private Integer keywordTopK; + private Integer graphRagTopK; + private Integer graphRagMaxHops; + private Integer raptorTopK; + private Integer raptorSourceChunkTopK; + private Integer candidateTopK; + private Integer rerankCandidateTopK; + private Integer reserveCandidateTopK; + private Integer finalTopK; + private Double minVectorSimilarity; + private Double keywordRelativeScoreFloor; + private Boolean keywordChannelEnabled; + private Boolean tableChannelEnabled; + private Boolean graphRagChannelEnabled; + private Boolean raptorChannelEnabled; + private HybridConfig hybrid; + } + + @Data + public static class HybridConfig { + private Double vectorWeight; + private Double keywordWeight; + private Double tableWeight; + private Double graphRagWeight; + private Double raptorWeight; + private Double rankWeight; + private Double originalScoreWeight; + private Double metadataBoostWeight; + private Double maxMetadataBoost; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingService.java index 1c86eb7837f9433e530b7fe7cad7566e94fce3b3..69e5a6df15bc643396f2942c4f8f6c2ec6954eed 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingService.java @@ -4,9 +4,12 @@ import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; +import org.javaup.ai.chatagent.rag.model.EvidenceRole; import org.javaup.ai.chatagent.rag.model.QueryType; import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationOperation; import org.javaup.ai.chatagent.service.ObservedChatModelService; import org.javaup.ai.prompt.PromptTemplateNames; import org.javaup.ai.prompt.PromptTemplateService; @@ -37,6 +40,7 @@ public class QueryUnderstandingService { private static final Pattern QUOTED_TEXT_PATTERN = Pattern.compile("[“\"']([^”\"']{2,40})[”\"']"); private static final double ADVISOR_CONFIDENCE_THRESHOLD = 0.72D; + private static final double STRUCTURE_NAVIGATION_CONFIDENCE_THRESHOLD = 0.65D; private final ObjectProvider observedChatModelServiceProvider; private final ObjectProvider promptTemplateServiceProvider; @@ -106,9 +110,14 @@ public class QueryUnderstandingService { .queryType(parseQueryType(root.path("queryType").asText(""))) .channels(parseChannels(root.path("channels"))) .entities(readStringArray(root.path("entities"), 8)) + .targetEntities(readStringArray(root.path("targetEntities"), 8)) + .excludedEntities(readStringArray(root.path("excludedEntities"), 8)) .sectionAnchors(readStringArray(root.path("sectionAnchors"), 8)) + .structureNavigationIntent(parseStructureNavigationIntent(root.path("structureNavigationIntent"))) + .expectedEvidenceRoles(parseEvidenceRoles(root.path("expectedEvidenceRoles"))) .tableOps(readStringArray(root.path("tableOps"), 8)) .negativeBoundary(root.path("negativeBoundary").asBoolean(false)) + .answerExpectation(root.path("answerExpectation").asText("")) .confidence(normalizeConfidence(root.path("confidence").asDouble(0D))) .reasons(readStringArray(root.path("reasons"), 8)) .source("llm-query-understanding") @@ -137,6 +146,16 @@ public class QueryUnderstandingService { channels.add(RetrievalIntent.GENERAL); } QueryType effectiveType = highConfidence ? queryType : fallback == null ? QueryType.DOCUMENT_QA : fallback.getQueryType(); + double effectiveConfidence = highConfidence + ? confidence + : fallback == null ? confidence : normalizeConfidence(fallback.getConfidence()); + List sectionAnchors = mergeStrings(fallback == null ? null : fallback.getSectionAnchors(), advised.getSectionAnchors(), 8); + StructureNavigationIntent structureNavigationIntent = selectStructureNavigationIntent(effectiveType, effectiveConfidence, advised, fallback, sectionAnchors); + List expectedEvidenceRoles = mergeRoles( + fallback == null ? null : fallback.getExpectedEvidenceRoles(), + advised.getExpectedEvidenceRoles(), + 4 + ); List reasons = new ArrayList<>(); if (advised.getReasons() != null) { reasons.addAll(advised.getReasons()); @@ -148,10 +167,15 @@ public class QueryUnderstandingService { .queryType(effectiveType == null ? QueryType.DOCUMENT_QA : effectiveType) .channels(new ArrayList<>(channels)) .entities(limitStrings(advised.getEntities(), 8)) - .sectionAnchors(mergeStrings(fallback == null ? null : fallback.getSectionAnchors(), advised.getSectionAnchors(), 8)) + .targetEntities(limitStrings(advised.getTargetEntities(), 8)) + .excludedEntities(limitStrings(advised.getExcludedEntities(), 8)) + .sectionAnchors(sectionAnchors) + .structureNavigationIntent(structureNavigationIntent) + .expectedEvidenceRoles(expectedEvidenceRoles) .tableOps(limitStrings(advised.getTableOps(), 8)) .negativeBoundary(advised.isNegativeBoundary()) - .confidence(confidence) + .answerExpectation(StrUtil.blankToDefault(advised.getAnswerExpectation(), "NORMAL_QA")) + .confidence(effectiveConfidence) .reasons(limitStrings(reasons, 10)) .source(StrUtil.blankToDefault(advised.getSource(), "query-understanding")); return builder.build(); @@ -164,6 +188,10 @@ public class QueryUnderstandingService { boolean strictStructureNavigation = !hasMultipleSubQuestions && looksStrictStructureNavigation(normalized); boolean outline = !hasMultipleSubQuestions && looksOutlineNavigation(normalized); boolean explicitTableQuery = !hasMultipleSubQuestions && looksExplicitTableQuery(normalized); + List structureOperations = determineStructureOperations(normalized, strictStructureNavigation, outline); + if (outline && anchors.isEmpty()) { + anchors = mergeStrings(anchors, extractOutlineAnchors(normalized), 8); + } QueryType queryType = explicitTableQuery ? QueryType.TABLE_QUERY : strictStructureNavigation || outline @@ -183,6 +211,16 @@ public class QueryUnderstandingService { .queryType(queryType) .channels(new ArrayList<>(channels)) .sectionAnchors(anchors) + .structureNavigationIntent(structureOperations.isEmpty() + ? null + : StructureNavigationIntent.builder() + .operations(structureOperations) + .sectionAnchors(anchors) + .confidence(strictStructureNavigation || outline ? 0.86D : 0.55D) + .source("java-deterministic-fallback") + .build()) + .expectedEvidenceRoles(List.of()) + .answerExpectation("NORMAL_QA") .confidence(strictStructureNavigation || outline || explicitTableQuery ? 0.86D : 0.55D) .reasons(reasons) .source("java-deterministic-fallback") @@ -210,6 +248,25 @@ public class QueryUnderstandingService { return containsAny(normalized, List.of("包含哪些章节", "都包含哪些章节", "有哪些章节", "有哪些小节", "包含哪些小节", "章节列表", "展开目录")); } + private List determineStructureOperations(String question, + boolean strictStructureNavigation, + boolean outline) { + if (outline) { + return List.of(StructureNavigationOperation.SECTION_WITH_CHILDREN); + } + if (!strictStructureNavigation) { + return List.of(); + } + String normalized = safeText(question); + if (containsAny(normalized, List.of("上一节", "下一节", "前一节", "后一节", "上一章", "下一章", "相邻章节", "同一一级章节"))) { + return List.of(StructureNavigationOperation.SECTION_WITH_SIBLINGS); + } + if (containsAny(normalized, List.of("属于哪个章节", "哪个章节", "哪个小节", "哪一节", "哪一章", "章节位置"))) { + return List.of(StructureNavigationOperation.PARENT_SECTION); + } + return List.of(StructureNavigationOperation.CURRENT_SECTION); + } + private boolean looksExplicitTableQuery(String question) { String normalized = safeText(question); if (normalized.isBlank()) { @@ -238,6 +295,100 @@ public class QueryUnderstandingService { return anchors.stream().limit(8).toList(); } + private List extractOutlineAnchors(String text) { + String cleaned = safeText(text) + .replace("都包含哪些章节", "") + .replace("包含哪些章节", "") + .replace("都有哪些章节", "") + .replace("有哪些章节", "") + .replace("包含哪些小节", "") + .replace("有哪些小节", "") + .replace("章节列表", "") + .replace("展开目录", "") + .replace("?", "") + .replace("?", "") + .trim(); + return StrUtil.isBlank(cleaned) ? List.of() : List.of(cleaned); + } + + private StructureNavigationIntent parseStructureNavigationIntent(JsonNode node) { + if (node == null || !node.isObject()) { + return null; + } + List operations = parseStructureOperations(node.path("operations")); + return StructureNavigationIntent.builder() + .operations(operations) + .anchorStructureNodeId(node.path("anchorStructureNodeId").isNumber() ? node.path("anchorStructureNodeId").asLong() : null) + .anchorSectionPath(node.path("anchorSectionPath").asText("")) + .anchorCanonicalPath(node.path("anchorCanonicalPath").asText("")) + .sectionAnchors(readStringArray(node.path("sectionAnchors"), 8)) + .confidence(normalizeConfidence(node.path("confidence").asDouble(0D))) + .source("llm-query-understanding") + .build(); + } + + private List parseStructureOperations(JsonNode node) { + if (node == null || !node.isArray()) { + return List.of(); + } + LinkedHashSet operations = new LinkedHashSet<>(); + for (JsonNode item : node) { + String normalized = item.asText("").trim().toUpperCase(Locale.ROOT); + try { + operations.add(StructureNavigationOperation.valueOf(normalized)); + } + catch (IllegalArgumentException ignored) { + // 丢弃未知结构导航操作,Java 主链路只接受白名单枚举。 + } + } + return new ArrayList<>(operations); + } + + private StructureNavigationIntent selectStructureNavigationIntent(QueryType effectiveType, + double confidence, + QueryUnderstandingResult advised, + QueryUnderstandingResult fallback, + List sectionAnchors) { + if (effectiveType != QueryType.STRUCTURE_NAVIGATION || confidence < STRUCTURE_NAVIGATION_CONFIDENCE_THRESHOLD) { + return null; + } + StructureNavigationIntent advisedIntent = advised == null ? null : advised.getStructureNavigationIntent(); + if (isValidStructureNavigationIntent(advisedIntent)) { + return normalizeStructureNavigationIntent(advisedIntent, sectionAnchors, "llm-query-understanding"); + } + StructureNavigationIntent fallbackIntent = fallback == null ? null : fallback.getStructureNavigationIntent(); + if (isValidStructureNavigationIntent(fallbackIntent)) { + return normalizeStructureNavigationIntent(fallbackIntent, sectionAnchors, "java-deterministic-fallback"); + } + return null; + } + + private boolean isValidStructureNavigationIntent(StructureNavigationIntent intent) { + return intent != null && intent.getOperations() != null && !intent.getOperations().isEmpty(); + } + + private StructureNavigationIntent normalizeStructureNavigationIntent(StructureNavigationIntent intent, + List sectionAnchors, + String fallbackSource) { + List operations = intent.getOperations().stream() + .filter(operation -> operation != null) + .distinct() + .limit(4) + .toList(); + if (operations.isEmpty()) { + return null; + } + return StructureNavigationIntent.builder() + .operations(operations) + .anchorStructureNodeId(intent.getAnchorStructureNodeId()) + .anchorSectionPath(StrUtil.blankToDefault(intent.getAnchorSectionPath(), "")) + .anchorCanonicalPath(StrUtil.blankToDefault(intent.getAnchorCanonicalPath(), "")) + .sectionAnchors(mergeStrings(sectionAnchors, intent.getSectionAnchors(), 8)) + .confidence(normalizeConfidence(intent.getConfidence())) + .source(StrUtil.blankToDefault(intent.getSource(), fallbackSource)) + .build(); + } + private QueryType parseQueryType(String raw) { String normalized = StrUtil.blankToDefault(raw, "").trim().toUpperCase(Locale.ROOT); try { @@ -269,6 +420,26 @@ public class QueryUnderstandingService { return new ArrayList<>(channels); } + private List parseEvidenceRoles(JsonNode node) { + if (node == null || !node.isArray()) { + return List.of(); + } + LinkedHashSet roles = new LinkedHashSet<>(); + for (JsonNode item : node) { + String normalized = item.asText("").trim().toUpperCase(Locale.ROOT); + try { + EvidenceRole role = EvidenceRole.valueOf(normalized); + if (role != EvidenceRole.GENERAL) { + roles.add(role); + } + } + catch (IllegalArgumentException ignored) { + // 丢弃未知证据角色,Java 主链路只接受白名单枚举。 + } + } + return new ArrayList<>(roles); + } + private List readStringArray(JsonNode node, int limit) { if (node == null || !node.isArray()) { return List.of(); @@ -297,6 +468,21 @@ public class QueryUnderstandingService { return values.stream().limit(limit).toList(); } + private List mergeRoles(List first, List second, int limit) { + LinkedHashSet values = new LinkedHashSet<>(); + if (first != null) { + first.stream() + .filter(role -> role != null && role != EvidenceRole.GENERAL) + .forEach(values::add); + } + if (second != null) { + second.stream() + .filter(role -> role != null && role != EvidenceRole.GENERAL) + .forEach(values::add); + } + return values.stream().limit(limit).toList(); + } + private List limitStrings(List values, int limit) { if (values == null || values.isEmpty()) { return List.of(); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairService.java index eef7b709b43b3297192cacbd49873bc6d3e87dbc..1a5ad679ce56cc89e7cd613e48940cbdc58f67e5 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairService.java @@ -4,6 +4,8 @@ import cn.hutool.core.util.StrUtil; import lombok.extern.slf4j.Slf4j; import org.javaup.ai.chatagent.model.SearchReference; import org.javaup.ai.chatagent.model.trace.ConversationTraceStageCode; +import org.javaup.ai.chatagent.rag.model.EvidenceIdentity; +import org.javaup.ai.chatagent.rag.support.EvidenceIdentityResolver; import org.javaup.ai.chatagent.service.ConversationTraceRecorder; import org.javaup.ai.ragtools.client.RagToolsClient; import org.javaup.ai.ragtools.model.RagToolsCitationRepairRequest; @@ -24,6 +26,9 @@ public class RagCitationRepairService { private static final double MIN_SCORE = 0.18D; private static final int MAX_TRACE_CANDIDATES = 24; private static final int MAX_TRACE_CITATIONS = 24; + private static final String NOT_APPLICABLE_STATUS = "NOT_APPLICABLE"; + private static final String FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY = "FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY"; + private static final String LEGACY_NOT_APPLICABLE_REASON = "NOT_APPLICABLE_TO_TARGET_ENTITY"; private final RagToolsClient ragToolsClient; @@ -41,8 +46,12 @@ public class RagCitationRepairService { List documentReferences = references.stream() .filter(this::isRepairableDocumentReference) + .filter(reference -> !isEvidenceNotApplicable(reference)) .toList(); - if (documentReferences.isEmpty()) { + List applicabilityFilteredReferences = references.stream() + .filter(this::isEvidenceNotApplicable) + .toList(); + if (documentReferences.isEmpty() && applicabilityFilteredReferences.isEmpty()) { return references; } @@ -54,26 +63,32 @@ public class RagCitationRepairService { "正在修复回答句与原文证据的引用关系。", Map.of( "candidateReferenceCount", references.size(), - "documentReferenceCount", documentReferences.size() + "documentReferenceCount", documentReferences.size(), + "applicabilityFilteredReferenceCount", applicabilityFilteredReferences.size() ) ); try { - RagToolsCitationRepairRequest request = buildRequest(answer, documentReferences); - RagToolsCitationRepairResponse response = ragToolsClient.repairCitations(request); - if (response == null) { - throw new IllegalStateException("rag-tools citation repair 返回空响应"); + List citations = List.of(); + if (!documentReferences.isEmpty()) { + RagToolsCitationRepairRequest request = buildRequest(answer, documentReferences); + RagToolsCitationRepairResponse response = ragToolsClient.repairCitations(request); + if (response == null) { + throw new IllegalStateException("rag-tools citation repair 返回空响应"); + } + citations = response.getCitations() == null ? List.of() : response.getCitations(); } - List repairedReferences = applyRepairResults(references, documentReferences, response.getCitations()); + List repairedReferences = applyRepairResults(references, documentReferences, citations); if (traceRecorder != null) { traceRecorder.completeStage( citationStage, "引用修复完成。", - buildRepairTraceSnapshot(references, documentReferences, response.getCitations(), repairedReferences) + buildRepairTraceSnapshot(references, documentReferences, applicabilityFilteredReferences, citations, repairedReferences) ); } - log.info("引用修复完成: candidateReferenceCount={}, documentReferenceCount={}, repairedReferenceCount={}", + log.info("引用修复完成: candidateReferenceCount={}, documentReferenceCount={}, applicabilityFilteredReferenceCount={}, repairedReferenceCount={}", references.size(), documentReferences.size(), + applicabilityFilteredReferences.size(), repairedReferences.size()); return repairedReferences; } @@ -132,6 +147,7 @@ public class RagCitationRepairService { List repairedReferences = new ArrayList<>(); allReferences.stream() .filter(reference -> !isRepairableDocumentReference(reference)) + .filter(reference -> !isEvidenceNotApplicable(reference)) .forEach(repairedReferences::add); repairedReferences.addAll(repairedDocumentMap.values()); return repairedReferences; @@ -162,16 +178,37 @@ public class RagCitationRepairService { if (StrUtil.isNotBlank(citation.getSectionPath())) { reference.setSectionPath(citation.getSectionPath()); } + refreshEvidenceIdentity(reference); + } + + private void refreshEvidenceIdentity(SearchReference reference) { + EvidenceIdentity citationIdentity = EvidenceIdentityResolver.citationIdentity(reference); + EvidenceIdentity contextIdentity = EvidenceIdentityResolver.contextIdentity(reference); + if (citationIdentity != null && citationIdentity.present()) { + reference.setCitationIdentity(citationIdentity.value()); + reference.setCitationEvidenceType(citationIdentity.type().name()); + reference.setSourceEvidenceResolved(true); + reference.setContextOnly(false); + } + else { + reference.setCitationIdentity(""); + reference.setCitationEvidenceType("CONTEXT_ONLY"); + reference.setSourceEvidenceResolved(false); + reference.setContextOnly(true); + } + reference.setContextIdentity(contextIdentity == null || !contextIdentity.present() ? "" : contextIdentity.value()); } private Map buildEvidenceMetadata(SearchReference reference) { Map metadata = new LinkedHashMap<>(); metadata.put("referenceId", StrUtil.blankToDefault(reference.getReferenceId(), "")); + metadata.put("finalSelectionReason", StrUtil.blankToDefault(reference.getFinalSelectionReason(), "")); + metadata.put("evidenceApplicabilityStatus", StrUtil.blankToDefault(reference.getEvidenceApplicabilityStatus(), "")); + metadata.put("evidenceApplicabilityReason", StrUtil.blankToDefault(reference.getEvidenceApplicabilityReason(), "")); metadata.put("chunkNo", reference.getChunkNo()); + metadata.put("chunkType", StrUtil.blankToDefault(reference.getChunkType(), "")); metadata.put("parentBlockNo", reference.getParentBlockNo()); metadata.put("sourceBlockIds", StrUtil.blankToDefault(reference.getSourceBlockIds(), "")); - metadata.put("knowledgeScopeCode", StrUtil.blankToDefault(reference.getKnowledgeScopeCode(), "")); - metadata.put("knowledgeScopeName", StrUtil.blankToDefault(reference.getKnowledgeScopeName(), "")); metadata.put("channel", StrUtil.blankToDefault(reference.getChannel(), "")); metadata.put("tableId", reference.getTableId()); metadata.put("tableNo", reference.getTableNo()); @@ -193,6 +230,7 @@ public class RagCitationRepairService { private Map buildRepairTraceSnapshot(List allReferences, List documentReferences, + List applicabilityFilteredReferences, List citations, List repairedReferences) { List safeCitations = citations == null ? List.of() : citations; @@ -200,17 +238,21 @@ public class RagCitationRepairService { Map snapshot = new LinkedHashMap<>(); snapshot.put("candidateReferenceCount", allReferences == null ? 0 : allReferences.size()); snapshot.put("documentReferenceCount", documentReferences == null ? 0 : documentReferences.size()); + snapshot.put("applicabilityFilteredReferenceCount", applicabilityFilteredReferences == null ? 0 : applicabilityFilteredReferences.size()); snapshot.put("matchedCitationCount", safeCitations.size()); snapshot.put("repairedReferenceCount", repairedReferences == null ? 0 : repairedReferences.size()); snapshot.put("repairedDocumentReferenceCount", countDocumentReferences(repairedReferences)); - snapshot.put("removedDocumentReferenceCount", Math.max(0, (documentReferences == null ? 0 : documentReferences.size()) - countDocumentReferences(repairedReferences))); + snapshot.put("removedDocumentReferenceCount", Math.max(0, + (documentReferences == null ? 0 : documentReferences.size()) + + (applicabilityFilteredReferences == null ? 0 : applicabilityFilteredReferences.size()) + - countDocumentReferences(repairedReferences))); snapshot.put("minScore", MIN_SCORE); snapshot.put("maxSegments", MAX_SEGMENTS); snapshot.put("maxMatchesPerSegment", MAX_MATCHES_PER_SEGMENT); snapshot.put("candidateEvidences", buildCandidateEvidenceTrace(documentReferences)); snapshot.put("matchedCitations", buildMatchedCitationTrace(safeCitations, documentReferences)); snapshot.put("finalCitations", finalCitations); - snapshot.put("removedCandidates", buildRemovedCandidateTrace(documentReferences, safeCitations)); + snapshot.put("removedCandidates", buildRemovedCandidateTrace(documentReferences, applicabilityFilteredReferences, safeCitations)); snapshot.put("citations", finalCitations); return snapshot; } @@ -267,24 +309,40 @@ public class RagCitationRepairService { } private List> buildRemovedCandidateTrace(List documentReferences, + List applicabilityFilteredReferences, List citations) { List matchedEvidenceIds = (citations == null ? List.of() : citations).stream() .filter(citation -> citation != null && StrUtil.isNotBlank(citation.getEvidenceId())) .map(RagToolsCitationRepairResponse.Result::getEvidenceId) .toList(); - return (documentReferences == null ? List.of() : documentReferences).stream() - .filter(reference -> !matchedEvidenceIds.contains(evidenceId(reference))) + List> removed = new ArrayList<>(); + (applicabilityFilteredReferences == null ? List.of() : applicabilityFilteredReferences).stream() .limit(MAX_TRACE_CANDIDATES) - .map(reference -> { + .forEach(reference -> { + Map item = baseReferenceTrace(reference); + item.put("evidenceId", evidenceId(reference)); + item.put("candidateText", StrUtil.blankToDefault(reference.getSnippet(), "")); + item.put("repairedBefore", reference.isCitationRepaired()); + item.put("repairedAfter", false); + item.put("filteredReason", FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY); + removed.add(item); + }); + if (removed.size() >= MAX_TRACE_CANDIDATES) { + return removed; + } + (documentReferences == null ? List.of() : documentReferences).stream() + .filter(reference -> !matchedEvidenceIds.contains(evidenceId(reference))) + .limit(MAX_TRACE_CANDIDATES - removed.size()) + .forEach(reference -> { Map item = baseReferenceTrace(reference); item.put("evidenceId", evidenceId(reference)); item.put("candidateText", StrUtil.blankToDefault(reference.getSnippet(), "")); item.put("repairedBefore", reference.isCitationRepaired()); item.put("repairedAfter", false); item.put("filteredReason", "未达到 citation repair 语义匹配阈值或该答案句已有更高分证据"); - return item; - }) - .toList(); + removed.add(item); + }); + return removed; } private List> buildCitationTrace(List references) { @@ -322,9 +380,18 @@ public class RagCitationRepairService { item.put("referenceId", StrUtil.blankToDefault(reference.getReferenceId(), "")); item.put("sourceType", StrUtil.blankToDefault(reference.getSourceType(), "")); item.put("channel", StrUtil.blankToDefault(reference.getChannel(), "")); + item.put("finalSelectionReason", StrUtil.blankToDefault(reference.getFinalSelectionReason(), "")); + item.put("evidenceApplicabilityStatus", StrUtil.blankToDefault(reference.getEvidenceApplicabilityStatus(), "")); + item.put("evidenceApplicabilityReason", StrUtil.blankToDefault(reference.getEvidenceApplicabilityReason(), "")); + item.put("contextIdentity", StrUtil.blankToDefault(reference.getContextIdentity(), "")); + item.put("citationIdentity", StrUtil.blankToDefault(reference.getCitationIdentity(), "")); + item.put("citationEvidenceType", StrUtil.blankToDefault(reference.getCitationEvidenceType(), "")); + item.put("contextOnly", reference.isContextOnly()); + item.put("sourceEvidenceResolved", reference.isSourceEvidenceResolved()); item.put("documentId", reference.getDocumentId()); item.put("documentName", StrUtil.blankToDefault(reference.getDocumentName(), reference.getTitle())); item.put("chunkId", reference.getChunkId()); + item.put("chunkType", StrUtil.blankToDefault(reference.getChunkType(), "")); item.put("chunkNo", reference.getChunkNo()); item.put("parentBlockId", reference.getParentBlockId()); item.put("parentBlockNo", reference.getParentBlockNo()); @@ -346,16 +413,29 @@ public class RagCitationRepairService { } private int countDocumentReferences(List references) { - return (int) references.stream().filter(this::isRepairableDocumentReference).count(); + return (int) references.stream() + .filter(this::isRepairableDocumentReference) + .filter(reference -> !isEvidenceNotApplicable(reference)) + .count(); } private boolean isRepairableDocumentReference(SearchReference reference) { return reference != null - && ("DOCUMENT".equalsIgnoreCase(StrUtil.blankToDefault(reference.getSourceType(), "")) - || "DOCUMENT_TABLE".equalsIgnoreCase(StrUtil.blankToDefault(reference.getSourceType(), ""))) + && EvidenceIdentityResolver.citationIdentity(reference) != null && StrUtil.isNotBlank(reference.getSnippet()); } + private boolean isEvidenceNotApplicable(SearchReference reference) { + if (reference == null) { + return false; + } + String status = StrUtil.blankToDefault(reference.getEvidenceApplicabilityStatus(), ""); + String reason = StrUtil.blankToDefault(reference.getFinalSelectionReason(), ""); + return NOT_APPLICABLE_STATUS.equalsIgnoreCase(status) + || FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY.equalsIgnoreCase(reason) + || LEGACY_NOT_APPLICABLE_REASON.equalsIgnoreCase(reason); + } + private String evidenceId(SearchReference reference) { if (StrUtil.isNotBlank(reference.getReferenceId())) { return reference.getReferenceId(); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagPromptAssemblyService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagPromptAssemblyService.java index e2400b474f581a15296669b239e57038e0b3530f..01236e296cee41a24b2fd743b4eb03cb04a42d07 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagPromptAssemblyService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagPromptAssemblyService.java @@ -3,8 +3,11 @@ package org.javaup.ai.chatagent.rag.service; import cn.hutool.core.util.StrUtil; import org.javaup.ai.chatagent.model.SearchReference; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.AnswerPlan; import org.javaup.ai.chatagent.rag.model.AnswerHistoryContext; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.EvidenceApplicabilityResult; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; import org.javaup.ai.chatagent.rag.model.RagPromptAssemblyResult; import org.javaup.ai.chatagent.rag.model.RagRetrievalContext; import org.javaup.ai.chatagent.rag.model.SubQuestionEvidence; @@ -29,11 +32,14 @@ public class RagPromptAssemblyService { private final ChatRagProperties properties; private final PromptTemplateService promptTemplateService; + private final AnswerPlanService answerPlanService; public RagPromptAssemblyService(ChatRagProperties properties, - PromptTemplateService promptTemplateService) { + PromptTemplateService promptTemplateService, + AnswerPlanService answerPlanService) { this.properties = properties; this.promptTemplateService = promptTemplateService; + this.answerPlanService = answerPlanService == null ? new AnswerPlanService() : answerPlanService; } public String buildSystemPrompt() { @@ -53,6 +59,7 @@ public class RagPromptAssemblyService { Math.max(0, properties.getPerSubQuestionEvidenceMaxChars()) ); Set renderedReferenceKeys = new LinkedHashSet<>(); + String evidenceBlocks = buildEvidenceBlocks(context, renderedReferenceKeys, promptBudget); String userPrompt = promptTemplateService.render(PromptTemplateNames.RAG_ANSWER_USER, Map.of( "currentDate", StrUtil.blankToDefault(plan.getCurrentDateText(), ""), "originalQuestion", StrUtil.blankToDefault(plan.getOriginalQuestion(), ""), @@ -62,8 +69,16 @@ public class RagPromptAssemblyService { "historyContext", buildHistoryContext(plan), "hasSubQuestions", hasSubQuestions(plan), "subQuestions", buildSubQuestions(plan), - "evidenceBlocks", buildEvidenceBlocks(context, renderedReferenceKeys, promptBudget) + "evidenceBlocks", evidenceBlocks )); + String boundaryInstruction = buildAnswerBoundaryInstruction(plan, context); + if (StrUtil.isNotBlank(boundaryInstruction)) { + userPrompt = boundaryInstruction + "\n\n" + userPrompt; + } + String roleInstruction = buildAnswerRoleInstruction(plan); + if (StrUtil.isNotBlank(roleInstruction)) { + userPrompt = roleInstruction + "\n\n" + userPrompt; + } return new RagPromptAssemblyResult( buildSystemPrompt(), userPrompt, @@ -181,6 +196,24 @@ public class RagPromptAssemblyService { } private String buildDocumentReferenceBlock(SearchReference reference) { + String snippet = trimSnippet(reference.getSnippet(), 1100); + if (StrUtil.isNotBlank(reference.getEvidenceRole())) { + snippet = "【证据角色】证据角色:" + reference.getEvidenceRole() + "\n" + snippet; + } + if (EvidenceApplicabilityResult.NOT_APPLICABLE.equals(reference.getEvidenceApplicabilityStatus())) { + snippet = "【证据适用性】这条证据不适用于当前目标对象,只能作为相似但不适用的线索。原因:" + + StrUtil.blankToDefault(reference.getEvidenceApplicabilityReason(), "-") + + "\n" + + snippet; + } + if ("SUMMARY_ONLY".equalsIgnoreCase(StrUtil.blankToDefault(reference.getRaptorSourceStatus(), ""))) { + snippet = "【RAPTOR 摘要边界】这条证据只命中层级摘要,未下钻到 source chunk 或 ParentBlock;只能作为背景线索,不能单独支撑具体事实结论。\n" + + snippet; + } + if (reference.isKgCommunitySummaryOnly()) { + snippet = "【GraphRAG 社区摘要边界】这条证据只命中社区摘要,缺少可回到原文 quote 的 KG evidence;只能作为背景线索,不能单独支撑具体事实结论。\n" + + snippet; + } return promptTemplateService.render(PromptTemplateNames.RAG_ANSWER_DOCUMENT_REFERENCE, Map.of( "referenceId", StrUtil.blankToDefault(reference.getReferenceId(), ""), "documentName", StrUtil.blankToDefault( @@ -188,10 +221,52 @@ public class RagPromptAssemblyService { "文档来源" ), "sectionPath", StrUtil.blankToDefault(reference.getSectionPath(), "未识别"), - "snippet", trimSnippet(reference.getSnippet(), 1100) + "snippet", snippet )) + "\n\n"; } + private String buildAnswerRoleInstruction(ConversationExecutionPlan plan) { + AnswerPlan answerPlan = answerPlanService.build(plan == null ? null : plan.getQueryUnderstanding()); + return answerPlan == null ? "" : StrUtil.blankToDefault(answerPlan.getInstruction(), ""); + } + + private String buildAnswerBoundaryInstruction(ConversationExecutionPlan plan, RagRetrievalContext context) { + QueryUnderstandingResult understanding = plan == null ? null : plan.getQueryUnderstanding(); + boolean explicitBoundary = understanding != null + && (understanding.isNegativeBoundary() + || "EXPLICIT_EVIDENCE_REQUIRED".equalsIgnoreCase(StrUtil.blankToDefault(understanding.getAnswerExpectation(), ""))); + boolean hasNotApplicableEvidence = hasNotApplicableEvidence(context); + if (!explicitBoundary && !hasNotApplicableEvidence) { + return ""; + } + String targetText = understanding == null || understanding.getTargetEntities() == null || understanding.getTargetEntities().isEmpty() + ? "" + : String.join("、", understanding.getTargetEntities()); + String excludedText = understanding == null || understanding.getExcludedEntities() == null || understanding.getExcludedEntities().isEmpty() + ? "" + : String.join("、", understanding.getExcludedEntities()); + StringBuilder builder = new StringBuilder(); + builder.append("回答边界要求:如果证据只支持相似对象或被用户排除的对象,而没有支持当前目标对象,必须回答文档没有明确给出,不得把相似对象的步骤、原因或结论套用到当前目标对象。"); + if (StrUtil.isNotBlank(targetText)) { + builder.append("\n当前目标对象:").append(targetText); + } + if (StrUtil.isNotBlank(excludedText)) { + builder.append("\n用户排除对象:").append(excludedText); + } + return builder.toString(); + } + + private boolean hasNotApplicableEvidence(RagRetrievalContext context) { + if (context == null || context.getSubQuestionEvidenceList() == null) { + return false; + } + return context.getSubQuestionEvidenceList().stream() + .filter(evidence -> evidence != null && evidence.getReferences() != null) + .flatMap(evidence -> evidence.getReferences().stream()) + .anyMatch(reference -> reference != null + && EvidenceApplicabilityResult.NOT_APPLICABLE.equals(reference.getEvidenceApplicabilityStatus())); + } + private String trimSnippet(String snippet, int maxChars) { if (StrUtil.isBlank(snippet)) { return ""; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagRetrievalEngine.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagRetrievalEngine.java index 11f700053b0d3c0ebe4663291ce2cbbb0b866ca6..73f1d6c7dbdab1bdf24144143f8f3fda89d45d71 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagRetrievalEngine.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagRetrievalEngine.java @@ -6,18 +6,29 @@ import org.javaup.ai.chatagent.model.RetrievalResultView; import org.javaup.ai.chatagent.model.SearchReference; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.ConversationStructureAnchor; +import org.javaup.ai.chatagent.rag.model.DocumentNavigationAction; +import org.javaup.ai.chatagent.rag.model.DocumentNavigationDecision; +import org.javaup.ai.chatagent.rag.model.EvidenceApplicabilityResult; import org.javaup.ai.chatagent.rag.model.QueryType; import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; import org.javaup.ai.chatagent.rag.model.RagRetrievalContext; import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationResult; import org.javaup.ai.chatagent.rag.model.SubQuestionChannelTrace; import org.javaup.ai.chatagent.rag.model.SubQuestionEvidence; import org.javaup.ai.chatagent.rag.retrieve.channel.RetrievalChannel; import org.javaup.ai.chatagent.rag.retrieve.channel.RetrievalChannelResult; +import org.javaup.ai.chatagent.rag.support.EvidenceIdentityResolver; import org.javaup.ai.chatagent.rag.support.SearchReferenceMapper; import org.javaup.ai.chatagent.service.ConversationTraceRecorder; +import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.ai.manage.model.StructureAnchoredEvidenceRequest; +import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode; import org.javaup.ai.manage.service.DocumentKnowledgeService; import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.javaup.enums.ChatQueryMode; import org.javaup.enums.RetrievalChannelEnum; import org.springframework.ai.document.Document; import org.springframework.beans.factory.annotation.Qualifier; @@ -52,27 +63,24 @@ import java.util.concurrent.TimeoutException; public class RagRetrievalEngine { private static final int RRF_K = 60; - private static final Set GRAPH_RAG_ACTION_RELATION_TYPES = Set.of( - "APPROVES", - "RESPONSIBLE_FOR", - "EXECUTES", - "REVOKES", - "OWNS", - "MANAGES", - "OPERATES", - "MAINTAINS" - ); - private static final Set GRAPH_RAG_WEAK_RELATION_TYPES = Set.of( - "RECORDS", - "ASSOCIATED_WITH", - "RELATED_TO" - ); + private static final String FILTERED_BY_VECTOR_GATE = "FILTERED_BY_VECTOR_GATE"; + private static final String FILTERED_BY_KEYWORD_RELATIVE_SCORE = "FILTERED_BY_KEYWORD_RELATIVE_SCORE"; + private static final String FILTERED_BY_CHANNEL_GATE = "FILTERED_BY_CHANNEL_GATE"; + private static final String FILTERED_BY_CANDIDATE_TOP_K = "FILTERED_BY_CANDIDATE_TOP_K"; + private static final String FILTERED_BY_RERANK_CANDIDATE_TOP_K = "FILTERED_BY_RERANK_CANDIDATE_TOP_K"; + private static final String FILTERED_BY_FINAL_TOP_K = "FILTERED_BY_FINAL_TOP_K"; + private static final String FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY = "FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY"; + private static final int STRUCTURE_ANCHOR_MAX_PER_ANCHOR = 2; + private static final int STRUCTURE_ANCHOR_MAX_TOTAL = 4; + private static final int ROUTE_CANDIDATE_SOURCE_MAX_PER_DOCUMENT = 2; private final List retrievalChannels; private final ChatRagProperties properties; private final RagRerankService ragRerankService; private final DocumentKnowledgeService documentKnowledgeService; private final ExecutorService executorService; + private final FinalEvidenceSelectionPolicy finalEvidenceSelectionPolicy; + private final EvidenceApplicabilityService evidenceApplicabilityService; public RagRetrievalEngine(List retrievalChannels, ChatRagProperties properties, @@ -84,6 +92,8 @@ public class RagRetrievalEngine { this.ragRerankService = ragRerankService; this.documentKnowledgeService = documentKnowledgeService; this.executorService = executorService; + this.finalEvidenceSelectionPolicy = new FinalEvidenceSelectionPolicy(properties); + this.evidenceApplicabilityService = new EvidenceApplicabilityService(); } public RagRetrievalContext retrieve(ConversationExecutionPlan plan, ConversationTraceRecorder traceRecorder) { @@ -128,7 +138,7 @@ public class RagRetrievalEngine { evidenceList.size(), acceptedCount, context.getRetrievalNotes()); - assignReferenceIds(evidenceList); + assignReferenceIds(evidenceList, plan); context.setSubQuestionEvidenceList(evidenceList); return context; } @@ -169,7 +179,7 @@ public class RagRetrievalEngine { .filter(result -> result.getDocuments() != null) .toList(); List channelResults = rawChannelResults.stream() - .map(this::applyEvidenceGate) + .map(result -> applyEvidenceGate(result, plan)) .toList(); List channelTraces = buildChannelTraces(rawChannelResults, channelResults, plan); @@ -182,9 +192,17 @@ public class RagRetrievalEngine { mergedCandidates, properties.getParentEvidenceMaxChars() ); - List rerankedCandidates = applyRerank(subQuestionIndex, subQuestion, parentCandidates, usedChannels, notes); + List structureAnchorCandidates = expandStructureAnchoredEvidence(parentCandidates, plan, notes, subQuestionIndex); + List structureNavigationCandidates = buildStructureNavigationContextCandidates(plan, notes, subQuestionIndex); + List rerankInputCandidates = mergeStructureAnchorCandidates(parentCandidates, structureAnchorCandidates); + List rerankedCandidates = applyRerank(subQuestionIndex, subQuestion, rerankInputCandidates, plan, usedChannels, notes); + List finalCandidates = mergeStructureAnchorCandidates(rerankedCandidates, structureAnchorCandidates); + finalCandidates = mergeStructureNavigationCandidates(finalCandidates, structureNavigationCandidates); - List finalDocuments = selectFinalDocuments(rerankedCandidates, plan); + List finalDocuments = selectFinalDocuments(finalCandidates, plan); + if (markEvidenceApplicability(finalDocuments, plan)) { + notes.add("子问题" + subQuestionIndex + "最终证据未明确支持当前目标对象,仅保留为相似但不适用证据。"); + } appendGraphRagCanonicalNotes(subQuestionIndex, subQuestion, finalDocuments, notes); @@ -197,7 +215,7 @@ public class RagRetrievalEngine { recordChannelObservations(traceRecorder, subQuestionIndex, subQuestion, rawChannelResults, channelResults, channelTraces, finalDocuments); recordRetrievalResultObservations(traceRecorder, subQuestionIndex, subQuestion, - rawChannelResults, channelResults, mergedCandidates, rerankedCandidates, finalDocuments); + rawChannelResults, channelResults, mergedCandidates, finalCandidates, finalDocuments, plan); } catch (RuntimeException exception) { log.warn("记录检索观测数据失败, subQuestionIndex={}", subQuestionIndex, exception); } @@ -210,40 +228,418 @@ public class RagRetrievalEngine { new ArrayList<>(), channelTraces, mergedCandidates.size(), - parentCandidates.size(), - rerankedCandidates.size() + rerankInputCandidates.size(), + finalCandidates.size() ); } private List selectFinalDocuments(List rerankedCandidates, ConversationExecutionPlan plan) { + return finalEvidenceSelectionPolicy.select(limitReserveCandidates(rerankedCandidates, plan), plan); + } + + private List limitReserveCandidates(List rerankedCandidates, ConversationExecutionPlan plan) { if (rerankedCandidates == null || rerankedCandidates.isEmpty()) { return List.of(); } - int finalTopK = Math.max(properties.getFinalTopK(), 0); - if (finalTopK <= 0 || rerankedCandidates.size() <= finalTopK) { - return rerankedCandidates.stream() - .limit(finalTopK) - .toList(); + RagRuntimeOptions options = runtimeOptions(plan); + int reserveCandidateTopK = options.getReserveCandidateTopK(); + int finalTopK = Math.max(options.getFinalTopK(), 0); + if (reserveCandidateTopK <= 0) { + return rerankedCandidates; } - List selected = new ArrayList<>(rerankedCandidates.stream() - .limit(finalTopK) - .toList()); - boolean preferCrossDocumentCommunity = shouldReserveCrossDocumentCommunityEvidence(plan); - if (selected.stream().anyMatch(document -> isRequiredGraphRagReserveCandidate(document, plan, preferCrossDocumentCommunity))) { - return selected; + int limit = Math.max(finalTopK, reserveCandidateTopK); + if (limit <= 0 || rerankedCandidates.size() <= limit) { + return rerankedCandidates; + } + return rerankedCandidates.stream() + .limit(limit) + .collect(java.util.stream.Collectors.collectingAndThen( + java.util.stream.Collectors.toCollection(ArrayList::new), + limited -> appendReserveWindowBypassCandidates(limited, rerankedCandidates) + )); + } + + private List appendReserveWindowBypassCandidates(List limitedCandidates, List allCandidates) { + if (allCandidates == null || allCandidates.isEmpty()) { + return limitedCandidates == null ? List.of() : limitedCandidates; } - Document graphRagReserve = selectGraphRagReserveCandidate(rerankedCandidates, finalTopK, selected, plan, preferCrossDocumentCommunity); - if (graphRagReserve == null && preferCrossDocumentCommunity) { - graphRagReserve = selectGraphRagReserveCandidate(rerankedCandidates, finalTopK, selected, plan, false); + List result = limitedCandidates == null ? new ArrayList<>() : new ArrayList<>(limitedCandidates); + for (Document candidate : allCandidates) { + if (!isReserveWindowBypassCandidate(candidate)) { + continue; + } + if (result.stream().noneMatch(selected -> sameEvidenceIdentity(selected, candidate))) { + result.add(candidate); + } } - if (graphRagReserve == null) { - return selected; + return result; + } + + private List expandStructureAnchoredEvidence(List parentCandidates, + ConversationExecutionPlan plan, + List notes, + int subQuestionIndex) { + StructureAnchoredEvidenceRequest request = buildStructureAnchoredEvidenceRequest(parentCandidates, plan); + if (request == null) { + return List.of(); } - int replaceIndex = weakestNonReservedEvidenceIndex(selected, plan); - if (replaceIndex >= 0) { - selected.set(replaceIndex, graphRagReserve); + try { + List expanded = documentKnowledgeService.expandStructureAnchoredEvidence(request); + if (expanded == null || expanded.isEmpty()) { + return List.of(); + } + notes.add("子问题" + subQuestionIndex + "结构锚点正文扩展命中 " + expanded.size() + " 条。"); + return expanded; } - return selected; + catch (RuntimeException exception) { + log.warn("结构锚点正文扩展失败: subQuestionIndex={}, message={}", subQuestionIndex, exception.getMessage(), exception); + notes.add("子问题" + subQuestionIndex + "结构锚点正文扩展失败,已保留普通检索候选继续回答。"); + return List.of(); + } + } + + private StructureAnchoredEvidenceRequest buildStructureAnchoredEvidenceRequest(List parentCandidates, + ConversationExecutionPlan plan) { + List documentIds = resolvePlanDocumentIds(plan); + List taskIds = resolvePlanTaskIds(plan); + if (documentIds.isEmpty() || taskIds.isEmpty()) { + return null; + } + + LinkedHashSet structureNodeIds = new LinkedHashSet<>(); + LinkedHashSet canonicalPaths = new LinkedHashSet<>(); + LinkedHashSet sectionAnchors = new LinkedHashSet<>(); + collectPlanStructureAnchors(plan, structureNodeIds, canonicalPaths, sectionAnchors); + collectCandidateStructureAnchors(parentCandidates, structureNodeIds, canonicalPaths, sectionAnchors); + if (structureNodeIds.isEmpty() && canonicalPaths.isEmpty() && sectionAnchors.isEmpty()) { + return null; + } + + return StructureAnchoredEvidenceRequest.builder() + .candidateDocuments(parentCandidates == null ? List.of() : parentCandidates) + .structureNodeIds(new ArrayList<>(structureNodeIds)) + .canonicalPaths(new ArrayList<>(canonicalPaths)) + .sectionAnchors(new ArrayList<>(sectionAnchors)) + .documentIds(documentIds) + .taskIds(taskIds) + .knowledgeBaseIds(plan == null || plan.getSelectedKnowledgeBaseIds() == null + ? List.of() + : plan.getSelectedKnowledgeBaseIds().stream().filter(Objects::nonNull).distinct().toList()) + .maxPerAnchor(STRUCTURE_ANCHOR_MAX_PER_ANCHOR) + .maxTotal(STRUCTURE_ANCHOR_MAX_TOTAL) + .maxChars(properties.getParentEvidenceMaxChars()) + .build(); + } + + private void collectPlanStructureAnchors(ConversationExecutionPlan plan, + LinkedHashSet structureNodeIds, + LinkedHashSet canonicalPaths, + LinkedHashSet sectionAnchors) { + if (plan == null) { + return; + } + QueryUnderstandingResult queryUnderstanding = plan.getQueryUnderstanding(); + if (queryUnderstanding != null && queryUnderstanding.getSectionAnchors() != null) { + queryUnderstanding.getSectionAnchors().stream() + .map(this::safeText) + .filter(anchor -> !anchor.isBlank()) + .forEach(sectionAnchors::add); + } + DocumentNavigationDecision navigationDecision = plan.getNavigationDecision(); + ConversationStructureAnchor structureAnchor = navigationDecision == null ? null : navigationDecision.getStructureAnchor(); + if (structureAnchor != null && !structureAnchor.isEmpty()) { + if (structureAnchor.getStructureNodeId() != null) { + structureNodeIds.add(structureAnchor.getStructureNodeId()); + } + if (!safeText(structureAnchor.getCanonicalPath()).isBlank()) { + canonicalPaths.add(structureAnchor.getCanonicalPath()); + } + if (!safeText(structureAnchor.getTargetSectionHint()).isBlank()) { + sectionAnchors.add(structureAnchor.getTargetSectionHint()); + } + if (!safeText(structureAnchor.getRootSectionCode()).isBlank()) { + sectionAnchors.add(structureAnchor.getRootSectionCode()); + } + } + collectStructureNavigationResultAnchors( + navigationDecision == null ? null : navigationDecision.getStructureNavigationResult(), + structureNodeIds, + canonicalPaths, + sectionAnchors + ); + } + + private void collectStructureNavigationResultAnchors(StructureNavigationResult result, + LinkedHashSet structureNodeIds, + LinkedHashSet canonicalPaths, + LinkedHashSet sectionAnchors) { + if (result == null) { + return; + } + collectStructureNavigationNodeAnchor(result.getCurrent(), structureNodeIds, canonicalPaths, sectionAnchors); + collectStructureNavigationNodeAnchor(result.getParent(), structureNodeIds, canonicalPaths, sectionAnchors); + collectStructureNavigationNodeAnchor(result.getPreviousSibling(), structureNodeIds, canonicalPaths, sectionAnchors); + collectStructureNavigationNodeAnchor(result.getNextSibling(), structureNodeIds, canonicalPaths, sectionAnchors); + if (result.getDirectChildren() != null) { + result.getDirectChildren().forEach(node -> collectStructureNavigationNodeAnchor(node, structureNodeIds, canonicalPaths, sectionAnchors)); + } + } + + private void collectStructureNavigationNodeAnchor(SuperAgentDocumentStructureNode node, + LinkedHashSet structureNodeIds, + LinkedHashSet canonicalPaths, + LinkedHashSet sectionAnchors) { + if (node == null) { + return; + } + if (node.getId() != null) { + structureNodeIds.add(node.getId()); + } + if (!safeText(node.getCanonicalPath()).isBlank()) { + canonicalPaths.add(node.getCanonicalPath()); + } + if (!safeText(node.getSectionPath()).isBlank()) { + sectionAnchors.add(node.getSectionPath()); + } + } + + private void collectCandidateStructureAnchors(List candidates, + LinkedHashSet structureNodeIds, + LinkedHashSet canonicalPaths, + LinkedHashSet sectionAnchors) { + if (candidates == null || candidates.isEmpty()) { + return; + } + for (Document candidate : candidates) { + if (candidate == null || candidate.getMetadata() == null) { + continue; + } + Long structureNodeId = metadataLong(candidate.getMetadata(), DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID); + if (structureNodeId != null) { + structureNodeIds.add(structureNodeId); + } + String canonicalPath = safeText(candidate.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH)); + if (!canonicalPath.isBlank()) { + canonicalPaths.add(canonicalPath); + } + String sectionPath = safeText(candidate.getMetadata().get(DocumentKnowledgeMetadataKeys.SECTION_PATH)); + if (!sectionPath.isBlank()) { + sectionAnchors.add(sectionPath); + } + } + } + + private List resolvePlanDocumentIds(ConversationExecutionPlan plan) { + if (plan == null) { + return List.of(); + } + if (plan.getRetrievalDocumentIds() != null && !plan.getRetrievalDocumentIds().isEmpty()) { + return plan.getRetrievalDocumentIds().stream().filter(Objects::nonNull).distinct().toList(); + } + if (plan.getSelectedDocumentId() != null) { + return List.of(plan.getSelectedDocumentId()); + } + if (plan.getAllowedKnowledgeBaseDocumentIds() != null && !plan.getAllowedKnowledgeBaseDocumentIds().isEmpty()) { + return plan.getAllowedKnowledgeBaseDocumentIds().stream().filter(Objects::nonNull).distinct().toList(); + } + return List.of(); + } + + private List resolvePlanTaskIds(ConversationExecutionPlan plan) { + if (plan == null) { + return List.of(); + } + if (plan.getRetrievalTaskIds() != null && !plan.getRetrievalTaskIds().isEmpty()) { + return plan.getRetrievalTaskIds().stream().filter(Objects::nonNull).distinct().toList(); + } + if (plan.getSelectedTaskId() != null) { + return List.of(plan.getSelectedTaskId()); + } + return List.of(); + } + + private List mergeStructureAnchorCandidates(List primaryCandidates, List structureCandidates) { + if ((structureCandidates == null || structureCandidates.isEmpty())) { + return primaryCandidates == null ? List.of() : primaryCandidates; + } + List merged = new ArrayList<>(); + if (primaryCandidates != null) { + merged.addAll(primaryCandidates); + } + for (Document structureCandidate : structureCandidates) { + if (structureCandidate == null) { + continue; + } + if (merged.stream().noneMatch(candidate -> sameEvidenceIdentity(candidate, structureCandidate))) { + merged.add(structureCandidate); + } + } + return merged; + } + + private List buildStructureNavigationContextCandidates(ConversationExecutionPlan plan, + List notes, + int subQuestionIndex) { + DocumentNavigationDecision decision = plan == null ? null : plan.getNavigationDecision(); + StructureNavigationResult result = decision == null ? null : decision.getStructureNavigationResult(); + QueryUnderstandingResult understanding = plan == null ? null : plan.getQueryUnderstanding(); + QueryType queryType = understanding == null || understanding.getQueryType() == null + ? QueryType.DOCUMENT_QA + : understanding.getQueryType(); + if (queryType != QueryType.STRUCTURE_NAVIGATION || result == null || !result.isDeterministic()) { + return List.of(); + } + List candidates = new ArrayList<>(); + DocumentNavigationAction action = decision.getNavigationAction(); + if (action == DocumentNavigationAction.CHILD_SECTION_DESCEND) { + addStructureNavigationCandidate(candidates, result.getCurrent(), "CURRENT", FinalEvidenceSelectionPolicy.RESERVE_STRUCTURE_NAVIGATION_CURRENT, plan); + if (result.getDirectChildren() != null) { + result.getDirectChildren().forEach(node -> addStructureNavigationCandidate( + candidates, + node, + "CHILD", + FinalEvidenceSelectionPolicy.RESERVE_STRUCTURE_NAVIGATION_CHILD, + plan + )); + } + } + else { + addStructureNavigationCandidate(candidates, result.getCurrent(), "CURRENT", FinalEvidenceSelectionPolicy.RESERVE_STRUCTURE_NAVIGATION_CURRENT, plan); + addStructureNavigationCandidate(candidates, result.getParent(), "PARENT", FinalEvidenceSelectionPolicy.RESERVE_STRUCTURE_NAVIGATION_PARENT, plan); + addStructureNavigationCandidate(candidates, result.getPreviousSibling(), "SIBLING", FinalEvidenceSelectionPolicy.RESERVE_STRUCTURE_NAVIGATION_SIBLING, plan); + addStructureNavigationCandidate(candidates, result.getNextSibling(), "SIBLING", FinalEvidenceSelectionPolicy.RESERVE_STRUCTURE_NAVIGATION_SIBLING, plan); + } + if (!candidates.isEmpty()) { + notes.add("子问题" + subQuestionIndex + "结构导航确定性上下文命中 " + candidates.size() + " 个节点。"); + } + return candidates; + } + + private void addStructureNavigationCandidate(List candidates, + SuperAgentDocumentStructureNode node, + String role, + String reserveType, + ConversationExecutionPlan plan) { + if (node == null || node.getId() == null) { + return; + } + String documentId = "structure-navigation:" + role + ":" + node.getId(); + if (candidates.stream().anyMatch(candidate -> Objects.equals(candidate.getId(), documentId))) { + return; + } + Map metadata = new LinkedHashMap<>(); + metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "STRUCTURE_NAVIGATION"); + metadata.put(DocumentKnowledgeMetadataKeys.CHANNEL, "structure-navigation"); + metadata.put(DocumentKnowledgeMetadataKeys.SCORE, structureNavigationScore(role, node)); + metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, firstNonNull(node.getDocumentId(), plan == null ? null : plan.getSelectedDocumentId())); + metadata.put(DocumentKnowledgeMetadataKeys.TASK_ID, firstNonNull(node.getParseTaskId(), plan == null ? null : plan.getSelectedTaskId())); + metadata.put(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, node.getId()); + metadata.put(DocumentKnowledgeMetadataKeys.SECTION_PATH, safeText(node.getSectionPath())); + metadata.put(DocumentKnowledgeMetadataKeys.CANONICAL_PATH, safeText(node.getCanonicalPath())); + metadata.put(DocumentKnowledgeMetadataKeys.TITLE, safeText(node.getTitle())); + metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE"); + metadata.put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, reserveType); + metadata.put(DocumentKnowledgeMetadataKeys.CONTEXT_ONLY, true); + metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_EVIDENCE_RESOLVED, false); + Document document = Document.builder() + .id(documentId) + .text(buildStructureNavigationText(role, node)) + .metadata(metadata) + .score(structureNavigationScore(role, node)) + .build(); + candidates.add(document); + } + + private List mergeStructureNavigationCandidates(List primaryCandidates, List navigationCandidates) { + if (navigationCandidates == null || navigationCandidates.isEmpty()) { + return primaryCandidates == null ? List.of() : primaryCandidates; + } + List merged = new ArrayList<>(); + navigationCandidates.stream() + .filter(Objects::nonNull) + .forEach(merged::add); + if (primaryCandidates != null) { + for (Document candidate : primaryCandidates) { + if (candidate != null && merged.stream().noneMatch(existing -> sameEvidenceIdentity(existing, candidate))) { + merged.add(candidate); + } + } + } + return merged; + } + + private String buildStructureNavigationText(String role, SuperAgentDocumentStructureNode node) { + StringBuilder builder = new StringBuilder(); + builder.append("结构导航节点:").append(safeText(node.getTitle())); + builder.append("\n节点角色:").append(role); + if (!safeText(node.getSectionPath()).isBlank()) { + builder.append("\n章节路径:").append(safeText(node.getSectionPath())); + } + if (!safeText(node.getCanonicalPath()).isBlank()) { + builder.append("\ncanonicalPath:").append(safeText(node.getCanonicalPath())); + } + return builder.toString(); + } + + private double structureNavigationScore(String role, SuperAgentDocumentStructureNode node) { + double score = switch (safeText(role)) { + case "CURRENT" -> 1.40D; + case "CHILD" -> 1.35D; + case "PARENT" -> 1.30D; + case "SIBLING" -> 1.25D; + default -> 1.0D; + }; + Integer nodeNo = node == null ? null : node.getNodeNo(); + return nodeNo == null ? score : score - Math.min(0.20D, nodeNo * 0.000001D); + } + + private Long firstNonNull(Long first, Long second) { + return first == null ? second : first; + } + + private boolean isStructureAnchorReserveCandidate(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + Object bypass = document.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW); + return Boolean.TRUE.equals(bypass) || Boolean.parseBoolean(String.valueOf(bypass)); + } + + private boolean isReserveWindowBypassCandidate(Document document) { + return isStructureAnchorReserveCandidate(document) || isRouteCandidateSourceReserve(document); + } + + private boolean isRouteCandidateSourceReserve(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + String reserveType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE)); + return FinalEvidenceSelectionPolicy.RESERVE_ROUTE_CANDIDATE_SOURCE.equals(reserveType); + } + + private boolean markEvidenceApplicability(List finalDocuments, ConversationExecutionPlan plan) { + QueryUnderstandingResult queryUnderstanding = plan == null ? null : plan.getQueryUnderstanding(); + if (finalDocuments == null || finalDocuments.isEmpty() || queryUnderstanding == null) { + return false; + } + boolean hasEvaluated = false; + boolean allNotApplicable = true; + for (Document document : finalDocuments) { + EvidenceApplicabilityResult result = evidenceApplicabilityService.evaluate(queryUnderstanding, document); + if (result == null || document == null || document.getMetadata() == null) { + continue; + } + hasEvaluated = true; + document.getMetadata().put(DocumentKnowledgeMetadataKeys.EVIDENCE_APPLICABILITY_STATUS, result.getStatus()); + document.getMetadata().put(DocumentKnowledgeMetadataKeys.EVIDENCE_APPLICABILITY_REASON, result.getReason()); + if (!result.isApplicable()) { + document.getMetadata().put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY); + } + else { + allNotApplicable = false; + } + } + return hasEvaluated && allNotApplicable; } private Document selectGraphRagReserveCandidate(List rerankedCandidates, @@ -255,7 +651,7 @@ public class RagRetrievalEngine { .skip(finalTopK) .filter(document -> isGraphRagReserveCandidate(document, plan)) .filter(document -> !crossDocumentCommunityOnly || isGraphRagCrossDocumentCommunityReserveCandidate(document, plan)) - .filter(candidate -> selected.stream().noneMatch(selectedDocument -> sameDocument(selectedDocument, candidate))) + .filter(candidate -> selected.stream().noneMatch(selectedDocument -> sameEvidenceIdentity(selectedDocument, candidate))) .max(Comparator.comparingDouble(document -> graphRagEvidenceBudgetPriority(document, plan))) .orElse(null); } @@ -269,13 +665,6 @@ public class RagRetrievalEngine { return !preferCrossDocumentCommunity || isGraphRagCrossDocumentCommunityReserveCandidate(document, plan); } - private boolean sameDocument(Document left, Document right) { - if (left == null || right == null) { - return false; - } - return Objects.equals(left.getId(), right.getId()); - } - private int weakestNonReservedEvidenceIndex(List selected, ConversationExecutionPlan plan) { int replaceIndex = -1; double weakestScore = Double.MAX_VALUE; @@ -293,30 +682,31 @@ public class RagRetrievalEngine { return replaceIndex >= 0 ? replaceIndex : selected.size() - 1; } - private RetrievalChannelResult applyEvidenceGate(RetrievalChannelResult result) { + private RetrievalChannelResult applyEvidenceGate(RetrievalChannelResult result, ConversationExecutionPlan plan) { if (result == null || result.getDocuments() == null || result.getDocuments().isEmpty()) { return result; } List documents = switch (result.getChannelName()) { - case "vector" -> filterVectorCandidates(result.getDocuments()); - case "keyword" -> filterKeywordCandidates(result.getDocuments()); + case "vector" -> filterVectorCandidates(result.getDocuments(), plan); + case "keyword" -> filterKeywordCandidates(result.getDocuments(), plan); default -> result.getDocuments(); }; return new RetrievalChannelResult(result.getChannelName(), documents); } - private List filterVectorCandidates(List documents) { + private List filterVectorCandidates(List documents, ConversationExecutionPlan plan) { + double minSimilarity = runtimeOptions(plan).getMinVectorSimilarity(); return documents.stream() .filter(document -> { Double score = resolveScore(document); - return score != null && score >= properties.getMinVectorSimilarity(); + return score != null && score >= minSimilarity; }) .toList(); } - private List filterKeywordCandidates(List documents) { + private List filterKeywordCandidates(List documents, ConversationExecutionPlan plan) { Double topScore = documents.stream() .map(this::resolveScore) .filter(Objects::nonNull) @@ -326,7 +716,7 @@ public class RagRetrievalEngine { return documents; } - double acceptedFloor = topScore * Math.max(0D, properties.getKeywordRelativeScoreFloor()); + double acceptedFloor = topScore * Math.max(0D, runtimeOptions(plan).getKeywordRelativeScoreFloor()); return documents.stream() .filter(document -> { Double score = resolveScore(document); @@ -347,7 +737,7 @@ public class RagRetrievalEngine { } List sortedHolders = holders.values().stream() - .peek(this::finishHybridScore) + .peek(holder -> finishHybridScore(holder, plan)) .peek(this::writeHybridMetadata) .sorted((left, right) -> Double.compare(right.score, left.score)) .toList(); @@ -361,7 +751,7 @@ public class RagRetrievalEngine { if (sortedHolders == null || sortedHolders.isEmpty()) { return List.of(); } - int candidateTopK = Math.max(properties.getCandidateTopK(), 0); + int candidateTopK = Math.max(runtimeOptions(plan).getCandidateTopK(), 0); if (candidateTopK <= 0 || sortedHolders.size() <= candidateTopK) { return sortedHolders.stream() .limit(candidateTopK) @@ -372,32 +762,107 @@ public class RagRetrievalEngine { .limit(candidateTopK) .toList()); boolean preferCrossDocumentCommunity = shouldReserveCrossDocumentCommunityEvidence(plan); - if (selected.stream().anyMatch(holder -> + if (selected.stream().noneMatch(holder -> isRequiredGraphRagReserveCandidate(holder.document, plan, preferCrossDocumentCommunity))) { - return selected; + CandidateHolder graphRagReserve = selectGraphRagReserveHolder( + sortedHolders, + candidateTopK, + selected, + plan, + preferCrossDocumentCommunity + ); + if (graphRagReserve == null && preferCrossDocumentCommunity) { + graphRagReserve = selectGraphRagReserveHolder(sortedHolders, candidateTopK, selected, plan, false); + } + if (graphRagReserve != null) { + int replaceIndex = weakestNonReservedEvidenceIndex( + selected.stream().map(holder -> holder.document).toList(), + plan + ); + if (replaceIndex >= 0) { + selected.set(replaceIndex, graphRagReserve); + } + } } + appendRouteCandidateSourceReserveHolders(sortedHolders, selected, plan); + return selected; + } - CandidateHolder graphRagReserve = selectGraphRagReserveHolder( - sortedHolders, - candidateTopK, - selected, - plan, - preferCrossDocumentCommunity - ); - if (graphRagReserve == null && preferCrossDocumentCommunity) { - graphRagReserve = selectGraphRagReserveHolder(sortedHolders, candidateTopK, selected, plan, false); + private void appendRouteCandidateSourceReserveHolders(List sortedHolders, + List selected, + ConversationExecutionPlan plan) { + List routeDocumentIds = routeCandidateDocumentIds(plan); + if (routeDocumentIds.size() < 2 || sortedHolders == null || sortedHolders.isEmpty()) { + return; } - if (graphRagReserve == null) { - return selected; + Map selectedCounts = routeDocumentSourceCounts(selected, routeDocumentIds); + for (CandidateHolder holder : sortedHolders) { + if (holder == null || holder.document == null || !isRouteCandidateSourceEvidence(holder.document, routeDocumentIds)) { + continue; + } + Long documentId = metadataLong(holder.document.getMetadata(), DocumentKnowledgeMetadataKeys.DOCUMENT_ID); + if (documentId == null || selectedCounts.getOrDefault(documentId, 0) >= ROUTE_CANDIDATE_SOURCE_MAX_PER_DOCUMENT) { + continue; + } + if (selected.stream().anyMatch(selectedHolder -> sameEvidenceIdentity(selectedHolder.document, holder.document))) { + continue; + } + markRouteCandidateSourceReserve(holder.document); + selected.add(holder); + selectedCounts.put(documentId, selectedCounts.getOrDefault(documentId, 0) + 1); + } + } + + private Map routeDocumentSourceCounts(List selected, List routeDocumentIds) { + Map counts = new LinkedHashMap<>(); + routeDocumentIds.forEach(documentId -> counts.put(documentId, 0)); + if (selected == null || selected.isEmpty()) { + return counts; } - int replaceIndex = weakestNonReservedEvidenceIndex( - selected.stream().map(holder -> holder.document).toList(), - plan + for (CandidateHolder holder : selected) { + if (holder == null || holder.document == null || holder.document.getMetadata() == null + || !EvidenceIdentityResolver.isCitationCapable(holder.document)) { + continue; + } + Long documentId = metadataLong(holder.document.getMetadata(), DocumentKnowledgeMetadataKeys.DOCUMENT_ID); + if (documentId != null && counts.containsKey(documentId)) { + counts.put(documentId, counts.getOrDefault(documentId, 0) + 1); + } + } + return counts; + } + + private boolean isRouteCandidateSourceEvidence(Document document, List routeDocumentIds) { + if (document == null || document.getMetadata() == null || routeDocumentIds == null || routeDocumentIds.isEmpty()) { + return false; + } + Long documentId = metadataLong(document.getMetadata(), DocumentKnowledgeMetadataKeys.DOCUMENT_ID); + return documentId != null + && routeDocumentIds.contains(documentId) + && EvidenceIdentityResolver.isCitationCapable(document); + } + + private void markRouteCandidateSourceReserve(Document document) { + if (document == null || document.getMetadata() == null) { + return; + } + document.getMetadata().putIfAbsent( + DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, + FinalEvidenceSelectionPolicy.RESERVE_ROUTE_CANDIDATE_SOURCE ); - if (replaceIndex >= 0) { - selected.set(replaceIndex, graphRagReserve); + } + + private List routeCandidateDocumentIds(ConversationExecutionPlan plan) { + if (plan == null + || plan.getChatMode() != ChatQueryMode.AUTO_DOCUMENT + || plan.getRetrievalDocumentIds() == null + || plan.getRetrievalDocumentIds().size() < 2) { + return List.of(); } - return selected; + return plan.getRetrievalDocumentIds().stream() + .filter(Objects::nonNull) + .distinct() + .toList(); } private CandidateHolder selectGraphRagReserveHolder(List sortedHolders, @@ -410,7 +875,7 @@ public class RagRetrievalEngine { .filter(holder -> holder != null && holder.document != null) .filter(holder -> isGraphRagReserveCandidate(holder.document, plan)) .filter(holder -> !crossDocumentCommunityOnly || isGraphRagCrossDocumentCommunityReserveCandidate(holder.document, plan)) - .filter(candidate -> selected.stream().noneMatch(selectedHolder -> sameDocument(selectedHolder.document, candidate.document))) + .filter(candidate -> selected.stream().noneMatch(selectedHolder -> sameEvidenceIdentity(selectedHolder.document, candidate.document))) .max(Comparator.comparingDouble(holder -> graphRagEvidenceBudgetPriority(holder.document, plan))) .orElse(null); } @@ -479,9 +944,9 @@ public class RagRetrievalEngine { CandidateHolder holder = holders.computeIfAbsent(documentId, ignored -> new CandidateHolder(document)); mergeGraphRagMetadata(holder, document); holder.rrfScore += rrfScore; - holder.rankScore += channelWeight * hybridRankWeight() * normalizedRankScore; - holder.originalScore += channelWeight * hybridOriginalScoreWeight() * normalizedOriginalScore; - holder.metadataBoost = Math.max(holder.metadataBoost, calculateMetadataBoost(document, metadataBoostTerms)); + holder.rankScore += channelWeight * hybridRankWeight(plan) * normalizedRankScore; + holder.originalScore += channelWeight * hybridOriginalScoreWeight(plan) * normalizedOriginalScore; + holder.metadataBoost = Math.max(holder.metadataBoost, calculateMetadataBoost(document, metadataBoostTerms, plan)); holder.channels.add(channelResult.getChannelName()); if (RetrievalChannelEnum.VECTOR.getName().equals(channelResult.getChannelName()) && originalScore != null) { holder.vectorScore = originalScore; @@ -492,10 +957,10 @@ public class RagRetrievalEngine { } } - private void finishHybridScore(CandidateHolder holder) { + private void finishHybridScore(CandidateHolder holder, ConversationExecutionPlan plan) { holder.score = holder.rankScore + holder.originalScore - + hybridMetadataBoostWeight() * Math.min(holder.metadataBoost, hybridMaxMetadataBoost()); + + hybridMetadataBoostWeight(plan) * Math.min(holder.metadataBoost, hybridMaxMetadataBoost(plan)); } private double normalizeOriginalScore(Double originalScore, double channelMaxScore) { @@ -506,7 +971,7 @@ public class RagRetrievalEngine { } private double resolveChannelWeight(String channelName, ConversationExecutionPlan plan) { - ChatRagProperties.HybridProperties hybrid = properties.getHybrid(); + RagRuntimeOptions.HybridOptions hybrid = runtimeOptions(plan).getHybrid(); double baseWeight; if (RetrievalChannelEnum.VECTOR.getName().equals(channelName)) { baseWeight = hybrid == null ? 1D : Math.max(0D, hybrid.getVectorWeight()); @@ -629,6 +1094,19 @@ public class RagRetrievalEngine { } Map metadata = document.getMetadata(); double priority = finalDocumentScore(document) * 0.01D; + if (isGraphRagCommunitySummaryOnly(document)) { + priority -= 30D; + } + String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL)); + if ("RELATION_STRONG_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 18D; + } + else if ("RELATION_WEAK_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 8D; + } + else if ("COMMUNITY_SOURCE_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 6D; + } if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID))) { priority += 20D; } @@ -659,23 +1137,27 @@ public class RagRetrievalEngine { return plan == null || plan.getRetrievalIntent() == null ? RetrievalIntent.GENERAL : plan.getRetrievalIntent(); } - private double hybridRankWeight() { - ChatRagProperties.HybridProperties hybrid = properties.getHybrid(); + private RagRuntimeOptions runtimeOptions(ConversationExecutionPlan plan) { + return RagRuntimeOptions.resolve(plan, properties); + } + + private double hybridRankWeight(ConversationExecutionPlan plan) { + RagRuntimeOptions.HybridOptions hybrid = runtimeOptions(plan).getHybrid(); return hybrid == null ? 1D : Math.max(0D, hybrid.getRankWeight()); } - private double hybridOriginalScoreWeight() { - ChatRagProperties.HybridProperties hybrid = properties.getHybrid(); + private double hybridOriginalScoreWeight(ConversationExecutionPlan plan) { + RagRuntimeOptions.HybridOptions hybrid = runtimeOptions(plan).getHybrid(); return hybrid == null ? 0.08D : Math.max(0D, hybrid.getOriginalScoreWeight()); } - private double hybridMetadataBoostWeight() { - ChatRagProperties.HybridProperties hybrid = properties.getHybrid(); + private double hybridMetadataBoostWeight(ConversationExecutionPlan plan) { + RagRuntimeOptions.HybridOptions hybrid = runtimeOptions(plan).getHybrid(); return hybrid == null ? 0.04D : Math.max(0D, hybrid.getMetadataBoostWeight()); } - private double hybridMaxMetadataBoost() { - ChatRagProperties.HybridProperties hybrid = properties.getHybrid(); + private double hybridMaxMetadataBoost(ConversationExecutionPlan plan) { + RagRuntimeOptions.HybridOptions hybrid = runtimeOptions(plan).getHybrid(); return hybrid == null ? 1D : Math.max(0D, hybrid.getMaxMetadataBoost()); } @@ -724,7 +1206,7 @@ public class RagRetrievalEngine { } } - private double calculateMetadataBoost(Document document, List terms) { + private double calculateMetadataBoost(Document document, List terms, ConversationExecutionPlan plan) { if (document == null || document.getMetadata() == null || terms == null || terms.isEmpty()) { return 0D; } @@ -734,12 +1216,9 @@ public class RagRetrievalEngine { boost += containsAnyMetadataTerm(document, terms, DocumentKnowledgeMetadataKeys.KEYWORDS) ? 0.18D : 0D; boost += containsAnyMetadataTerm(document, terms, DocumentKnowledgeMetadataKeys.QUESTIONS) ? 0.14D : 0D; boost += containsAnyMetadataTerm(document, terms, DocumentKnowledgeMetadataKeys.DOCUMENT_NAME) ? 0.10D : 0D; - boost += containsAnyMetadataTerm(document, terms, DocumentKnowledgeMetadataKeys.KNOWLEDGE_SCOPE_NAME) ? 0.08D : 0D; - boost += containsAnyMetadataTerm(document, terms, DocumentKnowledgeMetadataKeys.BUSINESS_CATEGORY) ? 0.06D : 0D; - boost += containsAnyMetadataTerm(document, terms, DocumentKnowledgeMetadataKeys.DOCUMENT_TAGS) ? 0.06D : 0D; boost += chunkTypeBoost(document); boost += graphRankMetadataBoost(document); - return Math.min(boost, hybridMaxMetadataBoost()); + return Math.min(boost, hybridMaxMetadataBoost(plan)); } private boolean containsAnyMetadataTerm(Document document, List terms, String metadataKey) { @@ -798,31 +1277,23 @@ public class RagRetrievalEngine { return false; } Map metadata = document.getMetadata(); + if (isGraphRagCommunitySummaryOnly(document)) { + return false; + } if (isGraphRagCommunityReportReserveCandidate(document, plan)) { return true; } boolean hasRelationEvidence = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID)) - && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)); + && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)) + && hasGraphRagSourceQuote(document); if (!hasRelationEvidence) { return false; } - boolean queryPlanOrNhop = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE)) - || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH)) - || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY)); - if (!queryPlanOrNhop) { + if (!hasGraphRagRelationGroundingContext(metadata)) { return false; } Double qualityScore = numericMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE)); - boolean qualityAccepted = qualityScore == null || qualityScore >= 0.55D; - if (!qualityAccepted) { - return false; - } - String relationType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE)).toUpperCase(Locale.ROOT); - if (GRAPH_RAG_ACTION_RELATION_TYPES.contains(relationType)) { - return true; - } - return GRAPH_RAG_WEAK_RELATION_TYPES.contains(relationType) - && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY)); + return qualityScore == null || qualityScore >= 0.55D; } private boolean isGraphRagCrossDocumentCommunityReserveCandidate(Document document, ConversationExecutionPlan plan) { @@ -842,14 +1313,16 @@ public class RagRetrievalEngine { if (!isGraphRagCommunityReportCandidate(metadata)) { return false; } + if (isGraphRagCommunitySummaryOnly(document)) { + return false; + } Integer communityDocumentCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_DOCUMENT_COUNT)); if (communityDocumentCount != null && communityDocumentCount < 2) { return false; } boolean grounded = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)) - && (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY)) - || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY)) - || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_TITLE))); + && hasGraphRagSourceQuote(document) + && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY)); if (!grounded) { return false; } @@ -901,6 +1374,12 @@ public class RagRetrievalEngine { } Map metadata = document.getMetadata(); double priority = finalDocumentScore(document); + if (isGraphRagCommunitySummaryOnly(document)) { + priority -= 2.0D; + } + if (hasGraphRagSourceQuote(document)) { + priority += 0.75D; + } if (isGraphRagCommunityReportReserveCandidate(document, plan)) { priority += 1.35D; Integer communityDocumentCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_DOCUMENT_COUNT)); @@ -916,13 +1395,24 @@ public class RagRetrievalEngine { priority += Math.min(0.24D, communityRelationGroupCount * 0.04D); } } - String relationType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE)).toUpperCase(Locale.ROOT); - if (GRAPH_RAG_ACTION_RELATION_TYPES.contains(relationType)) { - priority += 2.0D; + if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID)) + && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)) + && hasGraphRagSourceQuote(document)) { + priority += 1.10D; } - else if (GRAPH_RAG_WEAK_RELATION_TYPES.contains(relationType)) { + String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL)); + if ("RELATION_STRONG_QUOTE".equalsIgnoreCase(groundingLevel)) { priority += 0.55D; } + else if ("RELATION_WEAK_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 0.25D; + } + else if (groundingLevel.toUpperCase(Locale.ROOT).startsWith("RELATION_")) { + priority += 0.15D; + } + else if ("COMMUNITY_SOURCE_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 0.12D; + } if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH))) { priority += 0.55D; } @@ -948,6 +1438,45 @@ public class RagRetrievalEngine { return priority; } + private boolean hasGraphRagRelationGroundingContext(Map metadata) { + if (metadata == null) { + return false; + } + String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL)); + return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH)) + || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY)) + || groundingLevel.toUpperCase(Locale.ROOT).startsWith("RELATION_"); + } + + private boolean isGraphRagCommunitySummaryOnly(Document document) { + if (document == null || document.getMetadata() == null || !isGraphRagMetadata(document.getMetadata())) { + return false; + } + Map metadata = document.getMetadata(); + Object summaryOnly = metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY); + if (summaryOnly instanceof Boolean bool) { + return bool; + } + if (summaryOnly != null && Boolean.parseBoolean(String.valueOf(summaryOnly))) { + return true; + } + String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL)); + if ("COMMUNITY_SUMMARY_ONLY".equalsIgnoreCase(groundingLevel)) { + return true; + } + return isGraphRagCommunityReportCandidate(metadata) && !hasGraphRagSourceQuote(document); + } + + private boolean hasGraphRagSourceQuote(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + return isMeaningfulMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET)) + || (isMeaningfulMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)) + && isMeaningfulMetadataValue(document.getText())); + } + private Double numericMetadataValue(Object value) { if (value instanceof Number number) { return number.doubleValue(); @@ -973,32 +1502,98 @@ public class RagRetrievalEngine { return value == null ? "" : String.valueOf(value).trim(); } + private boolean booleanMetadataValue(Object value) { + if (value instanceof Boolean bool) { + return bool; + } + if (value instanceof Number number) { + return number.intValue() != 0; + } + return value != null && Boolean.parseBoolean(String.valueOf(value)); + } + private List applyRerank(int subQuestionIndex, String subQuestion, List candidates, + ConversationExecutionPlan plan, List usedChannels, List notes) { if (!properties.isRerankEnabled() || candidates.isEmpty()) { return candidates; } + int rerankCandidateTopK = resolveRerankCandidateTopK(candidates, plan); + List rerankInput = candidates.stream() + .limit(rerankCandidateTopK) + .collect(java.util.stream.Collectors.collectingAndThen( + java.util.stream.Collectors.toCollection(ArrayList::new), + limited -> appendRerankWindowBypassCandidates(limited, candidates) + )); + markRerankWindow(candidates, rerankInput); try { - List rerankedCandidates = ragRerankService.rerank(subQuestion, candidates); + List rerankedCandidates = ragRerankService.rerank(subQuestion, rerankInput); markUsedChannel(usedChannels, RetrievalChannelEnum.RERANK.getName()); return rerankedCandidates; } catch (RuntimeException exception) { Throwable rootCause = unwrapThrowable(exception); - markRerankFailure(candidates, rootCause); + markRerankFailure(rerankInput, rootCause); log.warn("rerank 失败,保留 weighted hybrid 候选继续回答: subQuestionIndex={}, subQuestion='{}', candidateCount={}, exceptionType={}, message={}", subQuestionIndex, subQuestion, - candidates.size(), + rerankInput.size(), rootCause == null ? "" : rootCause.getClass().getName(), rootCause == null ? "" : rootCause.getMessage(), exception); notes.add("子问题" + subQuestionIndex + " rerank 失败或超时,已保留融合候选继续回答。"); - return candidates; + return rerankInput; + } + } + + private int resolveRerankCandidateTopK(List candidates, ConversationExecutionPlan plan) { + if (candidates == null || candidates.isEmpty()) { + return 0; + } + int configured = runtimeOptions(plan).getRerankCandidateTopK(); + if (configured <= 0) { + return candidates.size(); + } + return Math.min(configured, candidates.size()); + } + + private List appendRerankWindowBypassCandidates(List limitedCandidates, List allCandidates) { + if (allCandidates == null || allCandidates.isEmpty()) { + return limitedCandidates == null ? List.of() : limitedCandidates; + } + List result = limitedCandidates == null ? new ArrayList<>() : new ArrayList<>(limitedCandidates); + for (Document candidate : allCandidates) { + if (!isReserveWindowBypassCandidate(candidate)) { + continue; + } + if (result.stream().noneMatch(selected -> sameEvidenceIdentity(selected, candidate))) { + result.add(candidate); + } + } + return result; + } + + private void markRerankWindow(List candidates, List rerankInput) { + if (candidates == null || candidates.isEmpty()) { + return; + } + int acceptedCount = rerankInput == null ? 0 : rerankInput.size(); + for (int index = 0; index < candidates.size(); index++) { + Document candidate = candidates.get(index); + if (candidate == null || candidate.getMetadata() == null) { + continue; + } + candidate.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_CANDIDATE_COUNT, acceptedCount); + candidate.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_TOP_K, acceptedCount); + boolean accepted = rerankInput != null + && rerankInput.stream().anyMatch(input -> sameEvidenceIdentity(input, candidate)); + if (!accepted) { + candidate.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_STATUS, "SKIPPED_BY_RERANK_CANDIDATE_TOP_K"); + } } } @@ -1224,12 +1819,14 @@ public class RagRetrievalEngine { return number == null ? null : number.intValue(); } - private void assignReferenceIds(List evidenceList) { + private void assignReferenceIds(List evidenceList, ConversationExecutionPlan plan) { final int[] referenceNumber = {1}; Map assignedIds = new LinkedHashMap<>(); + Map descriptorMap = knowledgeBaseReferenceDescriptorMap(evidenceList, plan); for (SubQuestionEvidence evidence : evidenceList) { List references = new ArrayList<>(); for (Document document : evidence.getDocuments()) { + enrichKnowledgeBaseReferenceMetadata(document, descriptorMap); SearchReference reference = SearchReferenceMapper.fromDocument( document, @@ -1247,6 +1844,106 @@ public class RagRetrievalEngine { } } + private Map knowledgeBaseReferenceDescriptorMap(List evidenceList, + ConversationExecutionPlan plan) { + Set documentIds = new LinkedHashSet<>(); + if (evidenceList != null) { + for (SubQuestionEvidence evidence : evidenceList) { + if (evidence == null || evidence.getDocuments() == null) { + continue; + } + for (Document document : evidence.getDocuments()) { + Long documentId = metadataLong(document, DocumentKnowledgeMetadataKeys.DOCUMENT_ID); + if (documentId != null && needsKnowledgeBaseReferenceFallback(document)) { + documentIds.add(documentId); + } + } + } + } + if (documentIds.isEmpty()) { + return Map.of(); + } + + List descriptors = plan != null + && plan.getSelectedKnowledgeBaseIds() != null + && !plan.getSelectedKnowledgeBaseIds().isEmpty() + ? documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(plan.getSelectedKnowledgeBaseIds()) + : documentKnowledgeService.listRetrievableDocuments(); + if (descriptors == null || descriptors.isEmpty()) { + return Map.of(); + } + + return descriptors.stream() + .filter(descriptor -> descriptor.getDocumentId() != null && documentIds.contains(descriptor.getDocumentId())) + .collect(java.util.stream.Collectors.toMap( + KnowledgeDocumentDescriptor::getDocumentId, + descriptor -> descriptor, + (left, right) -> left, + LinkedHashMap::new + )); + } + + private boolean needsKnowledgeBaseReferenceFallback(Document document) { + if (document == null || document.getMetadata() == null) { + return false; + } + return !isMeaningfulMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID)) + || !isMeaningfulMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME)); + } + + private void enrichKnowledgeBaseReferenceMetadata(Document document, + Map descriptorMap) { + if (document == null || document.getMetadata() == null || descriptorMap == null || descriptorMap.isEmpty()) { + return; + } + Long documentId = metadataLong(document, DocumentKnowledgeMetadataKeys.DOCUMENT_ID); + if (documentId == null) { + return; + } + KnowledgeDocumentDescriptor descriptor = descriptorMap.get(documentId); + if (descriptor == null) { + return; + } + Map metadata = document.getMetadata(); + if (!isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME)) + && isMeaningfulMetadataValue(descriptor.getDocumentName())) { + metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, descriptor.getDocumentName()); + } + if (!isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID)) + && descriptor.getKnowledgeBaseId() != null) { + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId()); + } + if (!isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME)) + && isMeaningfulMetadataValue(descriptor.getKnowledgeBaseName())) { + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, descriptor.getKnowledgeBaseName()); + } + } + + private Long metadataLong(Document document, String key) { + if (document == null || document.getMetadata() == null) { + return null; + } + return asLong(document.getMetadata().get(key)); + } + + private Long asLong(Object value) { + if (value instanceof Number number) { + return number.longValue(); + } + if (value == null) { + return null; + } + String text = String.valueOf(value).trim(); + if (text.isEmpty()) { + return null; + } + try { + return Long.parseLong(text); + } catch (NumberFormatException exception) { + return null; + } + } + private Double resolveScore(Document document) { if (document == null) { return null; @@ -1371,16 +2068,25 @@ public class RagRetrievalEngine { List filteredResults, List mergedCandidates, List rerankedCandidates, - List finalDocuments) { + List finalDocuments, + ConversationExecutionPlan plan) { List results = new ArrayList<>(); Map finalRankMap = new LinkedHashMap<>(); + Map finalDocumentMap = new LinkedHashMap<>(); Map mergedCandidateMap = new LinkedHashMap<>(); Map rerankedCandidateMap = new LinkedHashMap<>(); if (finalDocuments != null) { for (int i = 0; i < finalDocuments.size(); i++) { - String docId = finalDocuments.get(i).getId(); + Document finalDocument = finalDocuments.get(i); + String docId = finalDocument.getId(); if (docId != null) { finalRankMap.put(docId, i + 1); + finalDocumentMap.put(docId, finalDocument); + } + String citationIdentity = EvidenceIdentityResolver.citationIdentityValue(finalDocument); + if (!citationIdentity.isBlank()) { + finalRankMap.put(citationIdentity, i + 1); + finalDocumentMap.put(citationIdentity, finalDocument); } } } @@ -1409,7 +2115,7 @@ public class RagRetrievalEngine { for (int i = 0; i < rawDocs.size(); i++) { Document doc = rawDocs.get(i); - Document mergedDoc = doc.getId() == null ? null : mergedCandidateMap.get(doc.getId()); + Document mergedDoc = findMatchingDocument(doc, mergedCandidates, mergedCandidateMap); Map scoreMetadata = mergedDoc == null ? doc.getMetadata() : mergedDoc.getMetadata(); Document rerankedDoc = findMatchingDocument(doc, rerankedCandidates, rerankedCandidateMap); Map rerankMetadata = rerankedDoc == null ? scoreMetadata : rerankedDoc.getMetadata(); @@ -1471,6 +2177,11 @@ public class RagRetrievalEngine { view.setChunkId(Long.parseLong(String.valueOf(chunkIdObj))); } + Object chunkTypeObj = doc.getMetadata().get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE); + if (chunkTypeObj != null) { + view.setChunkType(String.valueOf(chunkTypeObj)); + } + Object chunkNoObj = doc.getMetadata().get(DocumentKnowledgeMetadataKeys.CHUNK_NO); if (chunkNoObj != null) { view.setChunkNo(Integer.parseInt(String.valueOf(chunkNoObj))); @@ -1490,6 +2201,12 @@ public class RagRetrievalEngine { if (sectionPathObj != null) { view.setSectionPath(String.valueOf(sectionPathObj)); } + enrichEvidenceIdentityMetadata(doc); + view.setContextIdentity(safeText(doc.getMetadata().get(DocumentKnowledgeMetadataKeys.CONTEXT_IDENTITY))); + view.setCitationIdentity(safeText(doc.getMetadata().get(DocumentKnowledgeMetadataKeys.CITATION_IDENTITY))); + view.setCitationEvidenceType(safeText(doc.getMetadata().get(DocumentKnowledgeMetadataKeys.CITATION_EVIDENCE_TYPE))); + view.setContextOnly(booleanMetadataValue(doc.getMetadata().get(DocumentKnowledgeMetadataKeys.CONTEXT_ONLY))); + view.setSourceEvidenceResolved(booleanMetadataValue(doc.getMetadata().get(DocumentKnowledgeMetadataKeys.SOURCE_EVIDENCE_RESOLVED))); String content = doc.getText(); if (content != null && !content.isEmpty()) { @@ -1509,25 +2226,12 @@ public class RagRetrievalEngine { if (isSelected) { view.setFinalRank(finalRank); - view.setSelectionReason("已选入最终 Prompt"); + Document finalDocument = findMatchingDocument(doc, finalDocuments, finalDocumentMap); + view.setSelectionReason(resolveSelectedReason(finalDocument)); } else if (!passedGate) { - - double score = originalScore == null ? 0D : originalScore; - if ("vector".equals(channelName)) { - view.setSelectionReason(String.format( - "向量闸门过滤:分数 %.4f < 阈值 %.4f", - score, properties.getMinVectorSimilarity() - )); - } else if ("keyword".equals(channelName)) { - view.setSelectionReason(String.format( - "关键词闸门过滤:分数 %.4f 低于相对阈值(floor=%.2f)", - score, properties.getKeywordRelativeScoreFloor() - )); - } else { - view.setSelectionReason("闸门过滤:分数 " + String.format("%.4f", score)); - } + view.setSelectionReason(resolveGateFilteredReason(channelName)); } else { - view.setSelectionReason("超出 finalTopK 限制(topK=" + properties.getFinalTopK() + ")"); + view.setSelectionReason(resolveFilteredReason(mergedDoc, rerankedDoc)); } results.add(view); @@ -1538,6 +2242,34 @@ public class RagRetrievalEngine { traceRecorder.recordRetrievalResults(results); } + private String resolveSelectedReason(Document finalDocument) { + if (finalDocument == null || finalDocument.getMetadata() == null) { + return FinalEvidenceSelectionPolicy.SELECTED_TOP_RANK; + } + String reason = safeText(finalDocument.getMetadata().get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON)); + return reason.isBlank() ? FinalEvidenceSelectionPolicy.SELECTED_TOP_RANK : reason; + } + + private String resolveGateFilteredReason(String channelName) { + if (RetrievalChannelEnum.VECTOR.getName().equals(channelName)) { + return FILTERED_BY_VECTOR_GATE; + } + if (RetrievalChannelEnum.KEYWORD.getName().equals(channelName)) { + return FILTERED_BY_KEYWORD_RELATIVE_SCORE; + } + return FILTERED_BY_CHANNEL_GATE; + } + + private String resolveFilteredReason(Document mergedDoc, Document rerankedDoc) { + if (mergedDoc == null) { + return FILTERED_BY_CANDIDATE_TOP_K; + } + if (rerankedDoc == null) { + return FILTERED_BY_RERANK_CANDIDATE_TOP_K; + } + return FILTERED_BY_FINAL_TOP_K; + } + private Document findMatchingDocument(Document candidate, List documents, Map byId) { @@ -1547,6 +2279,10 @@ public class RagRetrievalEngine { if (candidate.getId() != null && byId != null && byId.containsKey(candidate.getId())) { return byId.get(candidate.getId()); } + String citationIdentity = EvidenceIdentityResolver.citationIdentityValue(candidate); + if (!citationIdentity.isBlank() && byId != null && byId.containsKey(citationIdentity)) { + return byId.get(citationIdentity); + } return documents.stream() .filter(document -> sameEvidenceIdentity(candidate, document)) .findFirst() @@ -1572,6 +2308,10 @@ public class RagRetrievalEngine { if (candidate.getId() != null && finalRankMap != null && finalRankMap.containsKey(candidate.getId())) { return finalRankMap.get(candidate.getId()); } + String citationIdentity = EvidenceIdentityResolver.citationIdentityValue(candidate); + if (!citationIdentity.isBlank() && finalRankMap != null && finalRankMap.containsKey(citationIdentity)) { + return finalRankMap.get(citationIdentity); + } for (int index = 0; index < finalDocuments.size(); index++) { if (sameEvidenceIdentity(candidate, finalDocuments.get(index))) { return index + 1; @@ -1587,24 +2327,31 @@ public class RagRetrievalEngine { if (Objects.equals(left.getId(), right.getId())) { return true; } - Map leftMetadata = left.getMetadata(); - Map rightMetadata = right.getMetadata(); - Long leftDocumentId = metadataLong(leftMetadata, DocumentKnowledgeMetadataKeys.DOCUMENT_ID); - Long rightDocumentId = metadataLong(rightMetadata, DocumentKnowledgeMetadataKeys.DOCUMENT_ID); - if (leftDocumentId != null && rightDocumentId != null && !Objects.equals(leftDocumentId, rightDocumentId)) { - return false; - } - Long leftChunkId = metadataLong(leftMetadata, DocumentKnowledgeMetadataKeys.CHUNK_ID); - Long rightChunkId = metadataLong(rightMetadata, DocumentKnowledgeMetadataKeys.CHUNK_ID); - if (leftChunkId != null && rightChunkId != null && Objects.equals(leftChunkId, rightChunkId)) { + if (EvidenceIdentityResolver.sameCitationEvidence(left, right)) { return true; } - Long leftParentBlockId = metadataLong(leftMetadata, DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID); - Long rightParentBlockId = metadataLong(rightMetadata, DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID); - return leftChunkId == null && rightChunkId == null - && leftParentBlockId != null - && rightParentBlockId != null - && Objects.equals(leftParentBlockId, rightParentBlockId); + return EvidenceIdentityResolver.isContextOnly(left) + && EvidenceIdentityResolver.isContextOnly(right) + && EvidenceIdentityResolver.sameContext(left, right); + } + + private void enrichEvidenceIdentityMetadata(Document document) { + if (document == null || document.getMetadata() == null) { + return; + } + Map metadata = document.getMetadata(); + String citationIdentity = EvidenceIdentityResolver.citationIdentityValue(document); + String contextIdentity = EvidenceIdentityResolver.contextIdentityValue(document); + if (!citationIdentity.isBlank()) { + metadata.put(DocumentKnowledgeMetadataKeys.CITATION_IDENTITY, citationIdentity); + } + if (!contextIdentity.isBlank()) { + metadata.put(DocumentKnowledgeMetadataKeys.CONTEXT_IDENTITY, contextIdentity); + } + metadata.put(DocumentKnowledgeMetadataKeys.CITATION_EVIDENCE_TYPE, EvidenceIdentityResolver.citationEvidenceType(document).name()); + boolean contextOnly = EvidenceIdentityResolver.isContextOnly(document); + metadata.put(DocumentKnowledgeMetadataKeys.CONTEXT_ONLY, contextOnly); + metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_EVIDENCE_RESOLVED, !contextOnly); } private Long metadataLong(Map metadata, String key) { diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/StructureNavigationResolver.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/StructureNavigationResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..e96c3d94fa1092c43ef67e3fc297185de4b4a5d6 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/StructureNavigationResolver.java @@ -0,0 +1,137 @@ +package org.javaup.ai.chatagent.rag.service; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import org.javaup.ai.chatagent.rag.model.ConversationStructureAnchor; +import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationOperation; +import org.javaup.ai.chatagent.rag.model.StructureNavigationResult; +import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode; +import org.javaup.ai.manage.service.DocumentStructureNodeService; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; + +@Service +@AllArgsConstructor +public class StructureNavigationResolver { + + private final DocumentStructureNodeService structureNodeService; + + public StructureNavigationResult resolve(Long documentId, + Long parseTaskId, + StructureNavigationIntent intent, + ConversationStructureAnchor conversationAnchor) { + if (documentId == null) { + return missed(null, null, "DOCUMENT_ID_EMPTY"); + } + SuperAgentDocumentStructureNode anchor = resolveAnchor(documentId, parseTaskId, intent, conversationAnchor); + if (anchor == null) { + return missed(documentId, null, "ANCHOR_NOT_FOUND"); + } + List operations = intent == null || intent.getOperations() == null + ? List.of() + : intent.getOperations(); + SuperAgentDocumentStructureNode parent = shouldResolveParent(operations) + ? structureNodeService.findById(documentId, parseTaskId, anchor.getParentNodeId()) + : null; + SuperAgentDocumentStructureNode previous = shouldResolvePrevious(operations) + ? structureNodeService.findPreviousSibling(documentId, parseTaskId, anchor.getId()) + : null; + SuperAgentDocumentStructureNode next = shouldResolveNext(operations) + ? structureNodeService.findNextSibling(documentId, parseTaskId, anchor.getId()) + : null; + List children = shouldResolveChildren(operations) + ? structureNodeService.listChildren(documentId, parseTaskId, anchor.getId()) + : List.of(); + return StructureNavigationResult.builder() + .documentId(documentId) + .anchorNodeId(anchor.getId()) + .current(anchor) + .parent(parent) + .previousSibling(previous) + .nextSibling(next) + .directChildren(children) + .deterministic(true) + .build(); + } + + private SuperAgentDocumentStructureNode resolveAnchor(Long documentId, + Long parseTaskId, + StructureNavigationIntent intent, + ConversationStructureAnchor conversationAnchor) { + Long intentNodeId = intent == null ? null : intent.getAnchorStructureNodeId(); + SuperAgentDocumentStructureNode anchor = structureNodeService.findById(documentId, parseTaskId, intentNodeId); + if (anchor != null) { + return anchor; + } + Long conversationNodeId = conversationAnchor == null ? null : conversationAnchor.getStructureNodeId(); + anchor = structureNodeService.findById(documentId, parseTaskId, conversationNodeId); + if (anchor != null) { + return anchor; + } + String canonicalPath = firstNonBlank( + intent == null ? null : intent.getAnchorCanonicalPath(), + conversationAnchor == null ? null : conversationAnchor.getCanonicalPath() + ); + if (StrUtil.isNotBlank(canonicalPath)) { + anchor = findByCanonicalPath(documentId, parseTaskId, canonicalPath); + if (anchor != null) { + return anchor; + } + } + String sectionPath = intent == null ? null : intent.getAnchorSectionPath(); + return StrUtil.isBlank(sectionPath) ? null : findBySectionPath(documentId, parseTaskId, sectionPath); + } + + private SuperAgentDocumentStructureNode findByCanonicalPath(Long documentId, Long parseTaskId, String canonicalPath) { + return structureNodeService.listDocumentNodes(documentId, parseTaskId).stream() + .filter(node -> Objects.equals(StrUtil.trim(node.getCanonicalPath()), StrUtil.trim(canonicalPath))) + .findFirst() + .orElse(null); + } + + private SuperAgentDocumentStructureNode findBySectionPath(Long documentId, Long parseTaskId, String sectionPath) { + return structureNodeService.listDocumentNodes(documentId, parseTaskId).stream() + .filter(node -> Objects.equals(StrUtil.trim(node.getSectionPath()), StrUtil.trim(sectionPath))) + .findFirst() + .orElse(null); + } + + private boolean shouldResolveParent(List operations) { + return operations.isEmpty() + || operations.contains(StructureNavigationOperation.PARENT_SECTION) + || operations.contains(StructureNavigationOperation.SECTION_WITH_SIBLINGS); + } + + private boolean shouldResolvePrevious(List operations) { + return operations.isEmpty() + || operations.contains(StructureNavigationOperation.PREVIOUS_SIBLING) + || operations.contains(StructureNavigationOperation.SECTION_WITH_SIBLINGS); + } + + private boolean shouldResolveNext(List operations) { + return operations.isEmpty() + || operations.contains(StructureNavigationOperation.NEXT_SIBLING) + || operations.contains(StructureNavigationOperation.SECTION_WITH_SIBLINGS); + } + + private boolean shouldResolveChildren(List operations) { + return operations.contains(StructureNavigationOperation.DIRECT_CHILDREN) + || operations.contains(StructureNavigationOperation.SECTION_WITH_CHILDREN); + } + + private StructureNavigationResult missed(Long documentId, Long anchorNodeId, String reason) { + return StructureNavigationResult.builder() + .documentId(documentId) + .anchorNodeId(anchorNodeId) + .deterministic(false) + .missReason(reason) + .build(); + } + + private String firstNonBlank(String first, String second) { + return StrUtil.isNotBlank(first) ? first : second; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/support/EvidenceIdentityResolver.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/support/EvidenceIdentityResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..b5f96635ed5cc1b4f1935f90ea403f2b68ee39c8 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/support/EvidenceIdentityResolver.java @@ -0,0 +1,294 @@ +package org.javaup.ai.chatagent.rag.support; + +import cn.hutool.core.util.StrUtil; +import org.javaup.ai.chatagent.model.SearchReference; +import org.javaup.ai.chatagent.rag.model.CitationEvidenceType; +import org.javaup.ai.chatagent.rag.model.EvidenceIdentity; +import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.springframework.ai.document.Document; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public final class EvidenceIdentityResolver { + + private static final List BODY_CHUNK_TYPES = List.of("TEXT", "LIST", "TABLE", "BODY"); + + private EvidenceIdentityResolver() { + } + + public static EvidenceIdentity citationIdentity(Document document) { + if (document == null || document.getMetadata() == null) { + return null; + } + Map metadata = document.getMetadata(); + Long documentId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.DOCUMENT_ID)); + Long chunkId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID)); + Long kgEvidenceId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)); + Long raptorNodeId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID)); + Long tableId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.TABLE_ID)); + + if (isTableEvidence(metadata)) { + return EvidenceIdentity.citation("TABLE:" + tableId + ":" + tableEvidenceKey(metadata), CitationEvidenceType.TABLE_CELL_OR_ROW); + } + if (isGraphRagQuoteEvidence(metadata, document.getText())) { + String sourceChunk = chunkId == null ? "" : ":CHUNK:" + chunkId; + return EvidenceIdentity.citation("KG_QUOTE:" + kgEvidenceId + sourceChunk, CitationEvidenceType.KG_QUOTE_SOURCE); + } + if (isRaptorSourceChunk(metadata) && chunkId != null) { + return EvidenceIdentity.citation("RAPTOR_SOURCE:" + raptorNodeId + ":" + chunkId, CitationEvidenceType.RAPTOR_SOURCE_CHUNK); + } + if (isRawDocumentChunk(metadata) && chunkId != null) { + return EvidenceIdentity.citation("CHUNK:" + documentScope(documentId) + chunkId, CitationEvidenceType.CHUNK); + } + return null; + } + + public static EvidenceIdentity contextIdentity(Document document) { + if (document == null || document.getMetadata() == null) { + return null; + } + Map metadata = document.getMetadata(); + Long documentId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.DOCUMENT_ID)); + Long parentBlockId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID)); + Long chunkId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID)); + Long kgEvidenceId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)); + Long raptorNodeId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID)); + Long tableId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.TABLE_ID)); + if (parentBlockId != null) { + return EvidenceIdentity.context("PARENT:" + documentScope(documentId) + parentBlockId); + } + if (chunkId != null) { + return EvidenceIdentity.context("CHUNK_CONTEXT:" + documentScope(documentId) + chunkId); + } + if (kgEvidenceId != null) { + return EvidenceIdentity.context("KG_CONTEXT:" + kgEvidenceId); + } + if (raptorNodeId != null) { + return EvidenceIdentity.context("RAPTOR_CONTEXT:" + raptorNodeId + ":" + safeText(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS))); + } + if (tableId != null) { + return EvidenceIdentity.context("TABLE_CONTEXT:" + tableId); + } + if (document.getId() != null) { + return EvidenceIdentity.context("DOC_OBJECT:" + document.getId()); + } + return null; + } + + public static EvidenceIdentity citationIdentity(SearchReference reference) { + if (reference == null) { + return null; + } + if (isTableEvidence(reference)) { + return EvidenceIdentity.citation("TABLE:" + reference.getTableId() + ":" + tableEvidenceKey(reference), CitationEvidenceType.TABLE_CELL_OR_ROW); + } + if (reference.getKgEvidenceId() != null && StrUtil.isNotBlank(reference.getQuoteText())) { + String sourceChunk = reference.getChunkId() == null ? "" : ":CHUNK:" + reference.getChunkId(); + return EvidenceIdentity.citation("KG_QUOTE:" + reference.getKgEvidenceId() + sourceChunk, CitationEvidenceType.KG_QUOTE_SOURCE); + } + if (isRaptorSourceChunk(reference) && reference.getChunkId() != null) { + return EvidenceIdentity.citation("RAPTOR_SOURCE:" + reference.getRaptorNodeId() + ":" + reference.getChunkId(), CitationEvidenceType.RAPTOR_SOURCE_CHUNK); + } + if (reference.getChunkId() != null && isRawDocumentChunk(reference)) { + return EvidenceIdentity.citation("CHUNK:" + documentScope(reference.getDocumentId()) + reference.getChunkId(), CitationEvidenceType.CHUNK); + } + return null; + } + + public static EvidenceIdentity contextIdentity(SearchReference reference) { + if (reference == null) { + return null; + } + if (reference.getParentBlockId() != null) { + return EvidenceIdentity.context("PARENT:" + documentScope(reference.getDocumentId()) + reference.getParentBlockId()); + } + if (reference.getChunkId() != null) { + return EvidenceIdentity.context("CHUNK_CONTEXT:" + documentScope(reference.getDocumentId()) + reference.getChunkId()); + } + if (reference.getKgEvidenceId() != null) { + return EvidenceIdentity.context("KG_CONTEXT:" + reference.getKgEvidenceId()); + } + if (reference.getRaptorNodeId() != null) { + return EvidenceIdentity.context("RAPTOR_CONTEXT:" + reference.getRaptorNodeId() + ":" + StrUtil.blankToDefault(reference.getRaptorSourceStatus(), "")); + } + if (reference.getTableId() != null) { + return EvidenceIdentity.context("TABLE_CONTEXT:" + reference.getTableId()); + } + if (StrUtil.isNotBlank(reference.getUrl())) { + return EvidenceIdentity.context("WEB:" + reference.getUrl()); + } + return EvidenceIdentity.context(StrUtil.blankToDefault(reference.getSourceType(), "UNKNOWN") + + ":" + StrUtil.blankToDefault(reference.getTitle(), "") + + ":" + StrUtil.blankToDefault(reference.getSnippet(), "")); + } + + public static boolean sameCitationEvidence(Document left, Document right) { + EvidenceIdentity leftIdentity = citationIdentity(left); + EvidenceIdentity rightIdentity = citationIdentity(right); + return samePresentIdentity(leftIdentity, rightIdentity); + } + + public static boolean sameContext(Document left, Document right) { + EvidenceIdentity leftIdentity = contextIdentity(left); + EvidenceIdentity rightIdentity = contextIdentity(right); + return samePresentIdentity(leftIdentity, rightIdentity); + } + + public static boolean isCitationCapable(Document document) { + EvidenceIdentity identity = citationIdentity(document); + return identity != null && identity.present() && identity.citationCapable(); + } + + public static boolean isContextOnly(Document document) { + return !isCitationCapable(document); + } + + public static boolean isContextOnly(SearchReference reference) { + String sourceType = StrUtil.blankToDefault(reference.getSourceType(), ""); + if ("GRAPH_RAG".equalsIgnoreCase(sourceType) && (reference.getKgEvidenceId() == null || StrUtil.isBlank(reference.getQuoteText()))) { + return true; + } + if (reference.getRaptorNodeId() != null && !isRaptorSourceChunk(reference)) { + return true; + } + if (reference.getChunkId() != null && !isRawDocumentChunk(reference) && !isTableEvidence(reference) + && !(reference.getKgEvidenceId() != null && StrUtil.isNotBlank(reference.getQuoteText())) + && !isRaptorSourceChunk(reference)) { + return true; + } + return false; + } + + public static String citationIdentityValue(Document document) { + EvidenceIdentity identity = citationIdentity(document); + return identity == null ? "" : StrUtil.blankToDefault(identity.value(), ""); + } + + public static String contextIdentityValue(Document document) { + EvidenceIdentity identity = contextIdentity(document); + return identity == null ? "" : StrUtil.blankToDefault(identity.value(), ""); + } + + public static CitationEvidenceType citationEvidenceType(Document document) { + EvidenceIdentity identity = citationIdentity(document); + return identity == null ? CitationEvidenceType.CONTEXT_ONLY : identity.type(); + } + + private static boolean samePresentIdentity(EvidenceIdentity left, EvidenceIdentity right) { + return left != null && right != null + && left.present() + && right.present() + && Objects.equals(left.value(), right.value()); + } + + private static boolean isRawDocumentChunk(Map metadata) { + String sourceType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.SOURCE_TYPE)); + if ("GRAPH_RAG".equalsIgnoreCase(sourceType) || "RAPTOR".equalsIgnoreCase(sourceType) || "DOCUMENT_TABLE".equalsIgnoreCase(sourceType)) { + return false; + } + String chunkType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE)).toUpperCase(); + return BODY_CHUNK_TYPES.contains(chunkType) || chunkType.isBlank(); + } + + private static boolean isRawDocumentChunk(SearchReference reference) { + String sourceType = StrUtil.blankToDefault(reference.getSourceType(), ""); + if ("GRAPH_RAG".equalsIgnoreCase(sourceType) || "RAPTOR".equalsIgnoreCase(sourceType) || "DOCUMENT_TABLE".equalsIgnoreCase(sourceType)) { + return false; + } + String chunkType = StrUtil.blankToDefault(reference.getChunkType(), "").trim().toUpperCase(); + return BODY_CHUNK_TYPES.contains(chunkType) || chunkType.isBlank(); + } + + private static boolean isGraphRagQuoteEvidence(Map metadata, String text) { + Long kgEvidenceId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID)); + if (kgEvidenceId == null) { + return false; + } + String originalSnippet = safeText(metadata.get(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET)); + return StrUtil.isNotBlank(originalSnippet); + } + + private static boolean isRaptorSourceChunk(Map metadata) { + String sourceStatus = safeText(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS)); + String chunkType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE)); + return "SOURCE_CHUNK".equalsIgnoreCase(sourceStatus) || "RAPTOR_SOURCE_CHUNK".equalsIgnoreCase(chunkType); + } + + private static boolean isRaptorSourceChunk(SearchReference reference) { + return "SOURCE_CHUNK".equalsIgnoreCase(StrUtil.blankToDefault(reference.getRaptorSourceStatus(), "")); + } + + private static boolean isTableEvidence(Map metadata) { + Long tableId = asLong(metadata.get(DocumentKnowledgeMetadataKeys.TABLE_ID)); + return tableId != null + && (!asLongList(metadata.get(DocumentKnowledgeMetadataKeys.TABLE_EVIDENCE_CELL_IDS)).isEmpty() + || !asLongList(metadata.get(DocumentKnowledgeMetadataKeys.TABLE_EVIDENCE_ROW_IDS)).isEmpty()); + } + + private static boolean isTableEvidence(SearchReference reference) { + return reference.getTableId() != null + && ((reference.getTableEvidenceCellIds() != null && !reference.getTableEvidenceCellIds().isEmpty()) + || (reference.getTableEvidenceRowIds() != null && !reference.getTableEvidenceRowIds().isEmpty())); + } + + private static String tableEvidenceKey(Map metadata) { + List cellIds = asLongList(metadata.get(DocumentKnowledgeMetadataKeys.TABLE_EVIDENCE_CELL_IDS)); + if (!cellIds.isEmpty()) { + return "CELLS:" + cellIds; + } + List rowIds = asLongList(metadata.get(DocumentKnowledgeMetadataKeys.TABLE_EVIDENCE_ROW_IDS)); + if (!rowIds.isEmpty()) { + return "ROWS:" + rowIds; + } + return "TABLE"; + } + + private static String tableEvidenceKey(SearchReference reference) { + if (reference.getTableEvidenceCellIds() != null && !reference.getTableEvidenceCellIds().isEmpty()) { + return "CELLS:" + reference.getTableEvidenceCellIds(); + } + if (reference.getTableEvidenceRowIds() != null && !reference.getTableEvidenceRowIds().isEmpty()) { + return "ROWS:" + reference.getTableEvidenceRowIds(); + } + return "TABLE"; + } + + private static List asLongList(Object value) { + if (!(value instanceof Iterable iterable)) { + return List.of(); + } + java.util.ArrayList values = new java.util.ArrayList<>(); + for (Object item : iterable) { + Long parsed = asLong(item); + if (parsed != null) { + values.add(parsed); + } + } + return values; + } + + private static Long asLong(Object value) { + if (value instanceof Number number) { + return number.longValue(); + } + if (value == null) { + return null; + } + try { + return Long.parseLong(String.valueOf(value).trim()); + } + catch (NumberFormatException exception) { + return null; + } + } + + private static String safeText(Object value) { + return value == null ? "" : String.valueOf(value).trim(); + } + + private static String documentScope(Long documentId) { + return documentId == null ? "" : documentId + ":"; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/support/SearchReferenceMapper.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/support/SearchReferenceMapper.java index 80425d907e4885efbb20714e6b61d70ff3104a96..af31195d14312eb18efb01d0c8d7640f1a861128 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/support/SearchReferenceMapper.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/support/SearchReferenceMapper.java @@ -1,6 +1,7 @@ package org.javaup.ai.chatagent.rag.support; import org.javaup.ai.chatagent.model.SearchReference; +import org.javaup.ai.chatagent.rag.model.EvidenceIdentity; import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; import org.springframework.ai.document.Document; @@ -33,6 +34,15 @@ public final class SearchReferenceMapper { reference.setSubQuestion(subQuestion); reference.setChannel(asText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL), "vector")); reference.setScore(asDouble(metadata.get(DocumentKnowledgeMetadataKeys.SCORE))); + reference.setFinalSelectionReason(asText(metadata.get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON), "")); + reference.setEvidenceApplicabilityStatus(asText(metadata.get(DocumentKnowledgeMetadataKeys.EVIDENCE_APPLICABILITY_STATUS), "")); + reference.setEvidenceApplicabilityReason(asText(metadata.get(DocumentKnowledgeMetadataKeys.EVIDENCE_APPLICABILITY_REASON), "")); + reference.setEvidenceRole(asText(metadata.get(DocumentKnowledgeMetadataKeys.EVIDENCE_ROLE), "")); + reference.setContextIdentity(asText(metadata.get(DocumentKnowledgeMetadataKeys.CONTEXT_IDENTITY), "")); + reference.setCitationIdentity(asText(metadata.get(DocumentKnowledgeMetadataKeys.CITATION_IDENTITY), "")); + reference.setCitationEvidenceType(asText(metadata.get(DocumentKnowledgeMetadataKeys.CITATION_EVIDENCE_TYPE), "")); + reference.setContextOnly(asBoolean(metadata.get(DocumentKnowledgeMetadataKeys.CONTEXT_ONLY))); + reference.setSourceEvidenceResolved(asBoolean(metadata.get(DocumentKnowledgeMetadataKeys.SOURCE_EVIDENCE_RESOLVED))); if ("WEB".equalsIgnoreCase(sourceType)) { reference.setTitle(asText(metadata.get(DocumentKnowledgeMetadataKeys.TITLE), "网页来源")); @@ -44,17 +54,18 @@ public final class SearchReferenceMapper { reference.setTitle(asText(metadata.get(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME), "文档片段")); reference.setDocumentId(asLong(metadata.get(DocumentKnowledgeMetadataKeys.DOCUMENT_ID))); reference.setDocumentName(asText(metadata.get(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME), "")); + reference.setKnowledgeBaseId(asLong(metadata.get(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID))); + reference.setKnowledgeBaseName(asText(metadata.get(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME), "")); reference.setParentBlockId(asLong(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID))); reference.setParentBlockNo(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_NO))); reference.setChunkId(asLong(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID))); + reference.setChunkType(asText(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE), "")); reference.setChunkNo(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_NO))); reference.setSectionPath(asText(metadata.get(DocumentKnowledgeMetadataKeys.SECTION_PATH), "")); reference.setStructureNodeId(asLong(metadata.get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID))); reference.setStructureNodeType(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_TYPE))); reference.setCanonicalPath(asText(metadata.get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH), "")); reference.setItemIndex(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.ITEM_INDEX))); - reference.setKnowledgeScopeCode(asText(metadata.get(DocumentKnowledgeMetadataKeys.KNOWLEDGE_SCOPE_CODE), "")); - reference.setKnowledgeScopeName(asText(metadata.get(DocumentKnowledgeMetadataKeys.KNOWLEDGE_SCOPE_NAME), "")); reference.setPageNo(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.PAGE_NO))); reference.setPageRange(asText(metadata.get(DocumentKnowledgeMetadataKeys.PAGE_RANGE), "")); reference.setBboxJson(asText(metadata.get(DocumentKnowledgeMetadataKeys.BBOX_JSON), "")); @@ -89,6 +100,7 @@ public final class SearchReferenceMapper { reference.setKgRelationGroupEvidenceCount(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_EVIDENCE_COUNT))); reference.setKgRelationGroupDocumentCount(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_DOCUMENT_COUNT))); reference.setKgEvidenceId(asLong(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID))); + reference.setKgEvidenceGroundingLevel(asText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL), "")); reference.setKgGraphPath(asText(metadata.get(DocumentKnowledgeMetadataKeys.KG_GRAPH_PATH), "")); reference.setKgHopCount(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.KG_HOP_COUNT))); reference.setKgQueryPlanSource(asText(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE), "")); @@ -98,6 +110,7 @@ public final class SearchReferenceMapper { reference.setKgNhopSeedEntityName(asText(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_SEED_ENTITY_NAME), "")); reference.setKgNhopPath(asText(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH), "")); reference.setKgCrossDocumentCommunityKey(asText(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY), "")); + reference.setKgCommunitySummaryOnly(asBoolean(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY))); reference.setKgCrossDocumentCommunityEntityCount(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_ENTITY_COUNT))); reference.setKgCrossDocumentCommunityRelationGroupCount(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_RELATION_GROUP_COUNT))); reference.setKgCrossDocumentCommunityEvidenceCount(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_EVIDENCE_COUNT))); @@ -114,9 +127,31 @@ public final class SearchReferenceMapper { reference.setRaptorNodeTitle(asText(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_TITLE), "")); reference.setRaptorNodeLevel(asInteger(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_LEVEL))); reference.setRaptorSummary(asText(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_SUMMARY), "")); + reference.setRaptorSourceStatus(asText(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS), "")); + reference.setQuoteText(asText(metadata.get(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET), "")); + enrichEvidenceIdentity(reference); return reference; } + private static void enrichEvidenceIdentity(SearchReference reference) { + EvidenceIdentity citationIdentity = EvidenceIdentityResolver.citationIdentity(reference); + EvidenceIdentity contextIdentity = EvidenceIdentityResolver.contextIdentity(reference); + if (citationIdentity != null && citationIdentity.present()) { + reference.setCitationIdentity(citationIdentity.value()); + reference.setCitationEvidenceType(citationIdentity.type().name()); + reference.setSourceEvidenceResolved(true); + reference.setContextOnly(false); + } + else { + reference.setCitationEvidenceType("CONTEXT_ONLY"); + reference.setSourceEvidenceResolved(false); + reference.setContextOnly(true); + } + if (contextIdentity != null && contextIdentity.present()) { + reference.setContextIdentity(contextIdentity.value()); + } + } + private static String asText(Object value, String defaultValue) { return value == null ? defaultValue : String.valueOf(value); } @@ -133,6 +168,13 @@ public final class SearchReferenceMapper { return value instanceof Number number ? number.doubleValue() : null; } + private static boolean asBoolean(Object value) { + if (value instanceof Boolean bool) { + return bool; + } + return value != null && Boolean.parseBoolean(String.valueOf(value)); + } + private static List asLongList(Object value) { if (!(value instanceof Iterable iterable)) { return List.of(); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/BusinessChatService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/BusinessChatService.java index 04bd8be608796d4cea3a737debafe57a1b948e98..7031a87612798c8d178118719bf4b86098c78f37 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/BusinessChatService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/BusinessChatService.java @@ -37,10 +37,15 @@ import org.javaup.ai.chatagent.vo.ConversationSessionListVo; import org.javaup.ai.chatagent.vo.ConversationStopVo; import org.javaup.ai.prompt.PromptTemplateNames; import org.javaup.ai.prompt.PromptTemplateService; +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; import org.javaup.enums.ChatTurnStatus; import org.javaup.enums.ChatQueryMode; +import org.javaup.enums.KnowledgeBaseSelectionMode; import org.javaup.exception.SuperAgentFrameException; import org.javaup.lease.RedisLeaseManager; +import org.javaup.ai.manage.service.KnowledgeBaseManageService; +import org.javaup.ai.manage.service.KnowledgeBaseRetrievalScopeService; +import org.javaup.ai.manage.vo.KnowledgeBaseOptionVo; import org.springframework.ai.chat.messages.AbstractMessage; import org.springframework.ai.chat.messages.MessageType; import org.springframework.stereotype.Service; @@ -99,6 +104,8 @@ public class BusinessChatService { private final StageBenchmarkService stageBenchmarkService; private final PromptTemplateService promptTemplateService; private final RagCitationRepairService ragCitationRepairService; + private final KnowledgeBaseRetrievalScopeService knowledgeBaseRetrievalScopeService; + private final KnowledgeBaseManageService knowledgeBaseManageService; public Flux openConversationStream(ChatRequestDto request) { @@ -151,7 +158,8 @@ public class BusinessChatService { launchPlan.getQuestion(), launchPlan.getChatMode(), launchPlan.getSelectedDocumentId(), - launchPlan.getSelectedDocumentName() + launchPlan.getSelectedDocumentName(), + launchPlan.getKnowledgeBaseSelectionSnapshot() ); TaskInfo taskInfo = createTaskInfo(launchPlan, exchangeView); @@ -229,6 +237,7 @@ public class BusinessChatService { launchPlan.getSelectedDocumentId(), launchPlan.getSelectedDocumentName(), launchPlan.getSelectedTaskId(), + launchPlan.getKnowledgeBaseSelectionSnapshot(), launchPlan.getCurrentDate(), launchPlan.getCurrentDateText(), null, @@ -306,8 +315,15 @@ public class BusinessChatService { String conversationId = normalizeConversationId(request.getConversationId()); ChatQueryMode chatMode = parseRequiredChatMode(request.getChatMode()); + KnowledgeBaseSelectionMode selectionMode = parseKnowledgeBaseSelectionMode(request.getKnowledgeBaseSelectionMode()); + validateChatModeAndKnowledgeBaseSelection(chatMode, selectionMode); - KnowledgeDocumentDescriptor selectedDocument = resolveSelectedDocument(chatMode, request.getSelectedDocumentId()); + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection = knowledgeBaseRetrievalScopeService.resolve( + chatMode, + selectionMode, + request.getSelectedKnowledgeBaseIds() + ); + KnowledgeDocumentDescriptor selectedDocument = resolveSelectedDocument(chatMode, request.getSelectedDocumentId(), knowledgeBaseSelection); LocalDate currentDate = LocalDate.now(CHAT_ZONE_ID); String currentDateText = formatCurrentDate(currentDate); @@ -318,6 +334,7 @@ public class BusinessChatService { selectedDocument == null ? null : selectedDocument.getDocumentId(), selectedDocument == null ? "" : selectedDocument.getDocumentName(), selectedDocument == null ? null : selectedDocument.getLastIndexTaskId(), + knowledgeBaseSelection, buildChatLeaseKey(conversationId), @@ -520,6 +537,10 @@ public class BusinessChatService { .toList(); } + public List listKnowledgeBaseOptions() { + return knowledgeBaseManageService.listOptions(); + } + public ConversationMemorySummaryView rebuildConversationSummary(String conversationId) { return conversationMemoryService.rebuildConversationSummary(conversationId); } @@ -933,7 +954,8 @@ public class BusinessChatService { taskInfo.conversationId(), executionPlan.getChatMode(), executionPlan.getSelectedDocumentId(), - executionPlan.getSelectedDocumentName() + executionPlan.getSelectedDocumentName(), + taskInfo.knowledgeBaseSelectionSnapshot() ); putContextIfNotNull(taskInfo.runnableConfig(), ChatContextKeys.SELECTED_DOCUMENT_ID, executionPlan.getSelectedDocumentId()); putContextIfNotBlank(taskInfo.runnableConfig(), ChatContextKeys.SELECTED_DOCUMENT_NAME, executionPlan.getSelectedDocumentName()); @@ -980,6 +1002,9 @@ public class BusinessChatService { archiveRecord.chatMode(), archiveRecord.selectedDocumentId() == null ? "" : String.valueOf(archiveRecord.selectedDocumentId()), archiveRecord.selectedDocumentName(), + archiveRecord.knowledgeBaseSelectionMode(), + archiveRecord.selectedKnowledgeBaseIds(), + archiveRecord.selectedKnowledgeBaseNames(), archiveRecord.createdAt(), archiveRecord.updatedAt(), exchanges, @@ -1040,12 +1065,18 @@ public class BusinessChatService { exchange.getErrorMessage(), toNullable(taskInfo.firstResponseTimeMs().get()), System.currentTimeMillis() - taskInfo.startTime(), + exchange.getKnowledgeBaseSelectionMode(), + exchange.getSelectedKnowledgeBaseIds(), + exchange.getSelectedKnowledgeBaseNames(), + exchange.getRetrievalConfigSnapshotJson(), exchange.getCreateTime(), exchange.getEditTime() ); } - private KnowledgeDocumentDescriptor resolveSelectedDocument(ChatQueryMode chatMode, String selectedDocumentId) { + private KnowledgeDocumentDescriptor resolveSelectedDocument(ChatQueryMode chatMode, + String selectedDocumentId, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { if (chatMode == null) { throw new IllegalArgumentException("chatMode 不能为空"); } @@ -1068,19 +1099,22 @@ public class BusinessChatService { throw new IllegalArgumentException("当前文档问答模式下必须选择一个文档"); } final Long resolvedDocumentId = parseRequiredLong(normalizedDocumentId, "selectedDocumentId"); - return documentKnowledgeService.listRetrievableDocuments().stream() + List searchableDocuments = knowledgeBaseSelection == null + || knowledgeBaseSelection.getSelectionMode() == KnowledgeBaseSelectionMode.NONE + ? List.of() + : knowledgeBaseSelection.getAllowedDocuments(); + return searchableDocuments.stream() .filter(item -> Objects.equals(item.getDocumentId(), resolvedDocumentId)) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("所选文档当前不可检索: " + normalizedDocumentId)); + .orElseThrow(() -> new IllegalArgumentException("所选文档不在当前知识库范围内,或当前不可检索: " + normalizedDocumentId)); } private KnowledgeDocumentOptionView toKnowledgeDocumentOptionView(KnowledgeDocumentDescriptor descriptor) { return new KnowledgeDocumentOptionView( descriptor.getDocumentId() == null ? "" : String.valueOf(descriptor.getDocumentId()), descriptor.getDocumentName(), - descriptor.getKnowledgeScopeName(), - descriptor.getBusinessCategory(), - descriptor.getDocumentTags() + descriptor.getKnowledgeBaseId() == null ? "" : String.valueOf(descriptor.getKnowledgeBaseId()), + descriptor.getKnowledgeBaseName() ); } @@ -1130,6 +1164,32 @@ public class BusinessChatService { return chatMode; } + private KnowledgeBaseSelectionMode parseKnowledgeBaseSelectionMode(String value) { + try { + return KnowledgeBaseSelectionMode.fromName(value); + } + catch (IllegalArgumentException exception) { + throw new IllegalArgumentException("knowledgeBaseSelectionMode 非法: " + value, exception); + } + } + + private void validateChatModeAndKnowledgeBaseSelection(ChatQueryMode chatMode, + KnowledgeBaseSelectionMode selectionMode) { + KnowledgeBaseSelectionMode mode = selectionMode == null ? KnowledgeBaseSelectionMode.NONE : selectionMode; + if (chatMode == ChatQueryMode.OPEN_CHAT) { + if (mode != KnowledgeBaseSelectionMode.NONE) { + throw new IllegalArgumentException("开放式提问模式必须使用 NONE 知识库选择模式"); + } + return; + } + if (chatMode == ChatQueryMode.AUTO_DOCUMENT && mode == KnowledgeBaseSelectionMode.NONE) { + throw new IllegalArgumentException("自动知识问答模式必须选择知识库或使用全部知识库"); + } + if (chatMode == ChatQueryMode.DOCUMENT && mode == KnowledgeBaseSelectionMode.NONE) { + throw new IllegalArgumentException("当前文档问答模式必须选择知识库或使用全部知识库"); + } + } + private ChatTurnStatus parseOptionalTurnStatus(String value) { if (StrUtil.isBlank(value) || "ALL".equalsIgnoreCase(value.trim())) { return null; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/ConversationArchiveStore.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/ConversationArchiveStore.java index 410fd9661cd44e758938890b3eec9a040834bdbf..350c26f0978085d73efe33d3589470b7b1e4c3de 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/ConversationArchiveStore.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/ConversationArchiveStore.java @@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.service; import org.javaup.ai.chatagent.model.ConversationExchangeView; import org.javaup.ai.chatagent.model.SearchReference; import org.javaup.ai.chatagent.model.debug.ChatDebugTrace; +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; import org.javaup.enums.ChatQueryMode; import org.javaup.enums.ChatTurnStatus; @@ -22,12 +23,14 @@ public interface ConversationArchiveStore { String question, ChatQueryMode chatMode, Long selectedDocumentId, - String selectedDocumentName); + String selectedDocumentName, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection); void refreshSessionScope(String conversationId, ChatQueryMode chatMode, Long selectedDocumentId, - String selectedDocumentName); + String selectedDocumentName, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection); void completeExchange(String conversationId, long exchangeId, @@ -66,6 +69,9 @@ public interface ConversationArchiveStore { ChatQueryMode chatMode, Long selectedDocumentId, String selectedDocumentName, + String knowledgeBaseSelectionMode, + List selectedKnowledgeBaseIds, + List selectedKnowledgeBaseNames, Instant createdAt, Instant updatedAt, List exchanges diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/MybatisConversationArchiveStore.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/MybatisConversationArchiveStore.java index 74df2b37b55d30a96640c9508ddc9af139a46f34..f6b22d90a431533ecde5f6e4fa1ef4e2a4a072ae 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/MybatisConversationArchiveStore.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/MybatisConversationArchiveStore.java @@ -16,8 +16,10 @@ import org.javaup.ai.chatagent.mapper.SuperAgentChatExchangeMapper; import org.javaup.ai.chatagent.model.ConversationExchangeView; import org.javaup.ai.chatagent.model.SearchReference; import org.javaup.ai.chatagent.model.debug.ChatDebugTrace; +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; import org.javaup.enums.BusinessStatus; import org.javaup.enums.ChatQueryMode; +import org.javaup.enums.KnowledgeBaseSelectionMode; import org.javaup.enums.ChatSessionStatus; import org.javaup.enums.ChatTurnStatus; import org.javaup.util.DateUtils; @@ -70,8 +72,9 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore String question, ChatQueryMode chatMode, Long selectedDocumentId, - String selectedDocumentName) { - upsertDialogue(conversationId, ChatSessionStatus.RUNNING, chatMode, selectedDocumentId, selectedDocumentName); + String selectedDocumentName, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + upsertDialogue(conversationId, ChatSessionStatus.RUNNING, chatMode, selectedDocumentId, selectedDocumentName, knowledgeBaseSelection); long exchangeId = uidGenerator.getUid(); @@ -90,6 +93,10 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore exchange.setErrorMessage(""); exchange.setFirstResponseTimeMs(null); exchange.setTotalResponseTimeMs(null); + exchange.setKnowledgeBaseSelectionMode(selectionModeName(knowledgeBaseSelection)); + exchange.setSelectedKnowledgeBaseIdsJson(writeJson(selectionIds(knowledgeBaseSelection))); + exchange.setSelectedKnowledgeBaseNamesJson(writeJson(selectionNames(knowledgeBaseSelection))); + exchange.setRetrievalConfigSnapshotJson(writeNullableJson(knowledgeBaseSelection == null ? null : knowledgeBaseSelection.getRagRuntimeOptions())); exchange.setStatus(BusinessStatus.YES.getCode()); exchangeMapper.insert(exchange); @@ -106,8 +113,12 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore "", null, null, - DateUtils.now(), - DateUtils.now() + selectionModeName(knowledgeBaseSelection), + selectionIds(knowledgeBaseSelection), + selectionNames(knowledgeBaseSelection), + writeNullableJson(knowledgeBaseSelection == null ? null : knowledgeBaseSelection.getRagRuntimeOptions()), + DateUtils.now(), + DateUtils.now() ); } @@ -116,8 +127,9 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore public void refreshSessionScope(String conversationId, ChatQueryMode chatMode, Long selectedDocumentId, - String selectedDocumentName) { - upsertDialogue(conversationId, ChatSessionStatus.RUNNING, chatMode, selectedDocumentId, selectedDocumentName); + String selectedDocumentName, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { + upsertDialogue(conversationId, ChatSessionStatus.RUNNING, chatMode, selectedDocumentId, selectedDocumentName, knowledgeBaseSelection); } @Override @@ -191,6 +203,9 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore resolveChatMode(dialogue), dialogue.getSelectedDocumentId(), safeText(dialogue.getSelectedDocumentName()), + safeText(dialogue.getKnowledgeBaseSelectionMode()), + readStringList(dialogue.getSelectedKnowledgeBaseIdsJson()), + readStringList(dialogue.getSelectedKnowledgeBaseNamesJson()), toInstant(dialogue.getCreateTime()), toInstant(dialogue.getEditTime()), exchanges @@ -279,6 +294,9 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore resolveChatMode(dialogue), dialogue.getSelectedDocumentId(), safeText(dialogue.getSelectedDocumentName()), + safeText(dialogue.getKnowledgeBaseSelectionMode()), + readStringList(dialogue.getSelectedKnowledgeBaseIdsJson()), + readStringList(dialogue.getSelectedKnowledgeBaseNamesJson()), toInstant(dialogue.getCreateTime()), toInstant(dialogue.getEditTime()), exchangeViewMap.getOrDefault(dialogue.getConversationId(), List.of()) @@ -320,6 +338,9 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore resolveChatMode(dialogue), dialogue.getSelectedDocumentId(), safeText(dialogue.getSelectedDocumentName()), + safeText(dialogue.getKnowledgeBaseSelectionMode()), + readStringList(dialogue.getSelectedKnowledgeBaseIdsJson()), + readStringList(dialogue.getSelectedKnowledgeBaseNamesJson()), toInstant(dialogue.getCreateTime()), toInstant(dialogue.getEditTime()), latestExchangeMap.containsKey(dialogue.getConversationId()) @@ -360,7 +381,8 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore ChatSessionStatus dialogueStage, ChatQueryMode chatMode, Long selectedDocumentId, - String selectedDocumentName) { + String selectedDocumentName, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) { Objects.requireNonNull(chatMode, "chatMode 不能为空"); SuperAgentChatDialogue dialogue = dialogueMapper.selectOne( activeDialogueByConversation(conversationId) @@ -376,6 +398,9 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore newDialogue.setChatMode(chatMode.getCode()); newDialogue.setSelectedDocumentId(selectedDocumentId); newDialogue.setSelectedDocumentName(selectedDocumentName); + newDialogue.setKnowledgeBaseSelectionMode(selectionModeName(knowledgeBaseSelection)); + newDialogue.setSelectedKnowledgeBaseIdsJson(writeJson(selectionIds(knowledgeBaseSelection))); + newDialogue.setSelectedKnowledgeBaseNamesJson(writeJson(selectionNames(knowledgeBaseSelection))); newDialogue.setStatus(BusinessStatus.YES.getCode()); dialogueMapper.insert(newDialogue); @@ -386,14 +411,20 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore boolean chatModeChanged = !Objects.equals(chatMode.getCode(), dialogue.getChatMode()); boolean documentScopeChanged = !Objects.equals(selectedDocumentId, dialogue.getSelectedDocumentId()) || !Objects.equals(safeText(selectedDocumentName), safeText(dialogue.getSelectedDocumentName())); + boolean knowledgeBaseScopeChanged = !Objects.equals(selectionModeName(knowledgeBaseSelection), safeText(dialogue.getKnowledgeBaseSelectionMode())) + || !Objects.equals(selectionIds(knowledgeBaseSelection), readStringList(dialogue.getSelectedKnowledgeBaseIdsJson())) + || !Objects.equals(selectionNames(knowledgeBaseSelection), readStringList(dialogue.getSelectedKnowledgeBaseNamesJson())); - if (stageChanged || chatModeChanged || documentScopeChanged) { + if (stageChanged || chatModeChanged || documentScopeChanged || knowledgeBaseScopeChanged) { SuperAgentChatDialogue updateDialogue = new SuperAgentChatDialogue(); updateDialogue.setId(dialogue.getId()); updateDialogue.setSessionStatus(dialogueStage.getCode()); updateDialogue.setChatMode(chatMode.getCode()); updateDialogue.setSelectedDocumentId(selectedDocumentId); updateDialogue.setSelectedDocumentName(selectedDocumentName); + updateDialogue.setKnowledgeBaseSelectionMode(selectionModeName(knowledgeBaseSelection)); + updateDialogue.setSelectedKnowledgeBaseIdsJson(writeJson(selectionIds(knowledgeBaseSelection))); + updateDialogue.setSelectedKnowledgeBaseNamesJson(writeJson(selectionNames(knowledgeBaseSelection))); dialogueMapper.updateById(updateDialogue); } } @@ -522,11 +553,42 @@ public class MybatisConversationArchiveStore implements ConversationArchiveStore safeText(exchange.getErrorMessage()), exchange.getFirstResponseTimeMs(), exchange.getTotalResponseTimeMs(), + safeText(exchange.getKnowledgeBaseSelectionMode()), + readStringList(exchange.getSelectedKnowledgeBaseIdsJson()), + readStringList(exchange.getSelectedKnowledgeBaseNamesJson()), + safeText(exchange.getRetrievalConfigSnapshotJson()), exchange.getCreateTime(), exchange.getEditTime() ); } + private String selectionModeName(KnowledgeBaseSelectionSnapshot snapshot) { + KnowledgeBaseSelectionMode mode = snapshot == null || snapshot.getSelectionMode() == null + ? KnowledgeBaseSelectionMode.NONE + : snapshot.getSelectionMode(); + return mode.name(); + } + + private List selectionIds(KnowledgeBaseSelectionSnapshot snapshot) { + if (snapshot == null || snapshot.getSelectedKnowledgeBaseIds() == null) { + return List.of(); + } + return snapshot.getSelectedKnowledgeBaseIds().stream() + .filter(Objects::nonNull) + .map(String::valueOf) + .toList(); + } + + private List selectionNames(KnowledgeBaseSelectionSnapshot snapshot) { + if (snapshot == null || snapshot.getSelectedKnowledgeBaseNames() == null) { + return List.of(); + } + return snapshot.getSelectedKnowledgeBaseNames().stream() + .filter(StrUtil::isNotBlank) + .map(String::trim) + .toList(); + } + private List readStringList(String json) { if (StrUtil.isBlank(json)) { return List.of(); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/MybatisRetrievalObserveStore.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/MybatisRetrievalObserveStore.java index ea13a86d9f638d5202b2438a0daa70fcb8901ca2..1b029de9f7851353159fe95637d74b72c09dd0a8 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/MybatisRetrievalObserveStore.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/MybatisRetrievalObserveStore.java @@ -72,12 +72,18 @@ public class MybatisRetrievalObserveStore implements RetrievalObserveStore { entity.setDocumentId(view.getDocumentId()); entity.setDocumentName(view.getDocumentName()); entity.setChunkId(view.getChunkId()); + entity.setChunkType(view.getChunkType()); entity.setChunkNo(view.getChunkNo()); entity.setParentBlockId(view.getParentBlockId()); entity.setParentBlockNo(view.getParentBlockNo()); entity.setSectionPath(view.getSectionPath()); entity.setChunkTextPreview(view.getChunkTextPreview()); entity.setChunkCharCount(view.getChunkCharCount()); + entity.setContextIdentity(view.getContextIdentity()); + entity.setCitationIdentity(view.getCitationIdentity()); + entity.setCitationEvidenceType(view.getCitationEvidenceType()); + entity.setContextOnly(view.isContextOnly() ? 1 : 0); + entity.setSourceEvidenceResolved(view.isSourceEvidenceResolved() ? 1 : 0); entity.setStatus(BusinessStatus.YES.getCode()); retrievalResultMapper.insert(entity); } @@ -181,12 +187,18 @@ public class MybatisRetrievalObserveStore implements RetrievalObserveStore { entity.getDocumentId(), StrUtil.blankToDefault(entity.getDocumentName(), ""), entity.getChunkId(), + StrUtil.blankToDefault(entity.getChunkType(), ""), entity.getChunkNo(), entity.getParentBlockId(), entity.getParentBlockNo(), StrUtil.blankToDefault(entity.getSectionPath(), ""), StrUtil.blankToDefault(entity.getChunkTextPreview(), ""), entity.getChunkCharCount(), + StrUtil.blankToDefault(entity.getContextIdentity(), ""), + StrUtil.blankToDefault(entity.getCitationIdentity(), ""), + StrUtil.blankToDefault(entity.getCitationEvidenceType(), ""), + entity.getContextOnly() != null && entity.getContextOnly() == 1, + entity.getSourceEvidenceResolved() != null && entity.getSourceEvidenceResolved() == 1, toInstant(entity.getCreateTime()) ); } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/StreamLaunchPlan.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/StreamLaunchPlan.java index e05832fe60071e9f3c39f4ede47cb0d4107236b1..9eae6ee7a104f4cd1baeaf051f70524bce309889 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/StreamLaunchPlan.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/StreamLaunchPlan.java @@ -2,6 +2,7 @@ package org.javaup.ai.chatagent.service; import lombok.AllArgsConstructor; import lombok.Data; +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; import org.javaup.enums.ChatQueryMode; import java.time.LocalDate; @@ -28,6 +29,8 @@ public class StreamLaunchPlan { private final Long selectedTaskId; + private final KnowledgeBaseSelectionSnapshot knowledgeBaseSelectionSnapshot; + private final String leaseKey; private final String leaseOwnerToken; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/TaskInfo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/TaskInfo.java index 07e5522c8a4765da2c96e393e2e722d8b253d0b7..78df1d316c239c8ced07b3a28bd7bb6c544167d4 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/TaskInfo.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/service/TaskInfo.java @@ -6,6 +6,7 @@ import org.javaup.ai.chatagent.model.debug.ChatDebugTrace; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; import org.javaup.ai.chatagent.model.SearchReference; import org.javaup.ai.chatagent.support.StreamEventMetadata; +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; import org.javaup.enums.ChatQueryMode; import reactor.core.Disposable; import reactor.core.publisher.Sinks; @@ -33,6 +34,7 @@ public class TaskInfo { private final Long selectedDocumentId; private final String selectedDocumentName; private final Long selectedTaskId; + private final KnowledgeBaseSelectionSnapshot knowledgeBaseSelectionSnapshot; private final LocalDate currentDate; private final String currentDateText; @@ -70,6 +72,7 @@ public class TaskInfo { Long selectedDocumentId, String selectedDocumentName, Long selectedTaskId, + KnowledgeBaseSelectionSnapshot knowledgeBaseSelectionSnapshot, LocalDate currentDate, String currentDateText, ConversationExecutionPlan executionPlan, @@ -92,6 +95,7 @@ public class TaskInfo { this.selectedDocumentId = selectedDocumentId; this.selectedDocumentName = selectedDocumentName; this.selectedTaskId = selectedTaskId; + this.knowledgeBaseSelectionSnapshot = knowledgeBaseSelectionSnapshot; this.currentDate = currentDate; this.currentDateText = currentDateText; this.executionPlan = executionPlan; @@ -148,6 +152,10 @@ public class TaskInfo { return selectedTaskId; } + public KnowledgeBaseSelectionSnapshot knowledgeBaseSelectionSnapshot() { + return knowledgeBaseSelectionSnapshot; + } + public LocalDate currentDate() { return currentDate; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/DocumentElasticsearchIndexInitializer.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/DocumentElasticsearchIndexInitializer.java index 852555a23b08898c7a52ec41cc3fb5b5ef50f87c..f85ea225e1279302a357596526d49191dbeb2426 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/DocumentElasticsearchIndexInitializer.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/DocumentElasticsearchIndexInitializer.java @@ -67,6 +67,8 @@ public class DocumentElasticsearchIndexInitializer { .properties("documentName", property -> property.text(text -> text .analyzer(analyzer) .searchAnalyzer(searchAnalyzer))) + .properties("knowledgeBaseId", property -> property.long_(number -> number)) + .properties("knowledgeBaseName", property -> property.keyword(keyword -> keyword)) .properties("sectionPath", property -> property.text(text -> text .analyzer(analyzer) .searchAnalyzer(searchAnalyzer))) @@ -78,12 +80,6 @@ public class DocumentElasticsearchIndexInitializer { .properties("pageRange", property -> property.keyword(keyword -> keyword)) .properties("bboxJson", property -> property.keyword(keyword -> keyword)) .properties("sourceBlockIds", property -> property.keyword(keyword -> keyword)) - .properties("knowledgeScopeCode", property -> property.keyword(keyword -> keyword)) - .properties("knowledgeScopeName", property -> property.text(text -> text - .analyzer(analyzer) - .searchAnalyzer(searchAnalyzer))) - .properties("businessCategory", property -> property.keyword(keyword -> keyword)) - .properties("documentTags", property -> property.keyword(keyword -> keyword)) .properties("contentWithWeight", property -> property.text(text -> text .analyzer(analyzer) .searchAnalyzer(searchAnalyzer))) diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/DocumentManageProperties.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/DocumentManageProperties.java index 024bbce00b808cdd588ab4484aa43959f1f44d75..544cb518936d9fde4b5f81eb9dfcb9afd8050d79 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/DocumentManageProperties.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/DocumentManageProperties.java @@ -58,6 +58,10 @@ public class DocumentManageProperties { private Integer semanticMaxChars = 700; private Integer semanticMinChars = 240; private Double semanticSimilarityThreshold = 0.18D; + private Integer parentBlockMaxChars = 2200; + private Integer parentBlockOverlapChars = 180; + private Integer parentSemanticMaxChars = 1600; + private Integer parentSemanticMinChars = 480; private Boolean llmEnabled = Boolean.FALSE; private Integer llmMaxChars = 3500; private Boolean recommendLlmWhenLowQuality = Boolean.TRUE; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/KnowledgeRouteElasticsearchIndexInitializer.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/KnowledgeRouteElasticsearchIndexInitializer.java index 59affe2065575f5e57732f27fc78366e03577a6d..c35d3ccf1c2b3fb3951eea6ca297720e85889522 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/KnowledgeRouteElasticsearchIndexInitializer.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/config/KnowledgeRouteElasticsearchIndexInitializer.java @@ -61,14 +61,14 @@ public class KnowledgeRouteElasticsearchIndexInitializer { .mappings(mapping -> mapping .properties("routeId", property -> property.keyword(keyword -> keyword)) .properties("entityType", property -> property.keyword(keyword -> keyword)) - .properties("entityCode", property -> property.keyword(keyword -> keyword)) + .properties("entityId", property -> property.long_(number -> number)) .properties("documentId", property -> property.long_(number -> number)) - .properties("scopeCode", property -> property.keyword(keyword -> keyword)) + .properties("knowledgeBaseId", property -> property.long_(number -> number)) + .properties("scopeId", property -> property.long_(number -> number)) .properties("scopeName", property -> property.text(text -> text.analyzer(analyzer).searchAnalyzer(searchAnalyzer))) - .properties("topicCode", property -> property.keyword(keyword -> keyword)) + .properties("topicId", property -> property.long_(number -> number)) .properties("topicName", property -> property.text(text -> text.analyzer(analyzer).searchAnalyzer(searchAnalyzer))) .properties("documentName", property -> property.text(text -> text.analyzer(analyzer).searchAnalyzer(searchAnalyzer))) - .properties("businessCategory", property -> property.keyword(keyword -> keyword)) .properties("displayName", property -> property.text(text -> text.analyzer(analyzer).searchAnalyzer(searchAnalyzer))) .properties("descriptionText", property -> property.text(text -> text.analyzer(analyzer).searchAnalyzer(searchAnalyzer))) .properties("aliasesText", property -> property.text(text -> text.analyzer(analyzer).searchAnalyzer(searchAnalyzer))) diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/controller/KnowledgeBaseManageController.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/controller/KnowledgeBaseManageController.java new file mode 100644 index 0000000000000000000000000000000000000000..6065c6195dcadca813dd4af5a67e9c459e7b1ced --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/controller/KnowledgeBaseManageController.java @@ -0,0 +1,56 @@ +package org.javaup.ai.manage.controller; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import org.javaup.ai.manage.dto.KnowledgeBaseConfigUpdateDto; +import org.javaup.ai.manage.dto.KnowledgeBaseDeleteDto; +import org.javaup.ai.manage.dto.KnowledgeBaseDetailDto; +import org.javaup.ai.manage.dto.KnowledgeBaseSaveDto; +import org.javaup.ai.manage.service.KnowledgeBaseManageService; +import org.javaup.ai.manage.vo.KnowledgeBaseItemVo; +import org.javaup.common.ApiResponse; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@AllArgsConstructor +@RestController +@RequestMapping("/manage/knowledge/base") +public class KnowledgeBaseManageController { + + private final KnowledgeBaseManageService knowledgeBaseManageService; + + @Operation(summary = "保存知识库") + @PostMapping("/save") + public ApiResponse save(@Valid @RequestBody KnowledgeBaseSaveDto dto) { + return ApiResponse.ok(knowledgeBaseManageService.save(dto)); + } + + @Operation(summary = "删除知识库") + @PostMapping("/delete") + public ApiResponse delete(@Valid @RequestBody KnowledgeBaseDeleteDto dto) { + return ApiResponse.ok(knowledgeBaseManageService.delete(dto)); + } + + @Operation(summary = "查询知识库列表") + @PostMapping("/list") + public ApiResponse> list() { + return ApiResponse.ok(knowledgeBaseManageService.list()); + } + + @Operation(summary = "查询知识库详情") + @PostMapping("/detail") + public ApiResponse detail(@Valid @RequestBody KnowledgeBaseDetailDto dto) { + return ApiResponse.ok(knowledgeBaseManageService.detail(dto)); + } + + @Operation(summary = "更新知识库检索配置") + @PostMapping("/config/update") + public ApiResponse updateConfig(@Valid @RequestBody KnowledgeBaseConfigUpdateDto dto) { + return ApiResponse.ok(knowledgeBaseManageService.updateConfig(dto)); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/controller/KnowledgeManageController.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/controller/KnowledgeManageController.java index da7503e012f3643019c9583f6b98e954c7be63b1..faf105847897cef2aecc74f57c3b7fb0042f9730 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/controller/KnowledgeManageController.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/controller/KnowledgeManageController.java @@ -7,6 +7,7 @@ import org.javaup.ai.manage.dto.DocumentProfileDetailQueryDto; import org.javaup.ai.manage.dto.DocumentProfileRegenerateDto; import org.javaup.ai.manage.dto.KnowledgeRouteTraceQueryDto; import org.javaup.ai.manage.dto.KnowledgeScopeDeleteDto; +import org.javaup.ai.manage.dto.KnowledgeScopeQueryDto; import org.javaup.ai.manage.dto.KnowledgeScopeSaveDto; import org.javaup.ai.manage.dto.KnowledgeTopicDeleteDto; import org.javaup.ai.manage.dto.KnowledgeTopicQueryDto; @@ -57,8 +58,8 @@ public class KnowledgeManageController { @Operation(summary = "查询知识范围列表") @PostMapping("/scope/list") - public ApiResponse> listScopes() { - return ApiResponse.ok(knowledgeManageService.listScopes()); + public ApiResponse> listScopes(@RequestBody(required = false) KnowledgeScopeQueryDto dto) { + return ApiResponse.ok(knowledgeManageService.listScopes(dto == null ? new KnowledgeScopeQueryDto() : dto)); } @Operation(summary = "保存知识主题节点") diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentDocument.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentDocument.java index 7d18c0a47659815fd3a1153195034af95fbfc197..9ef4117cef27cf1b52f8105f1b6067fc194f4b10 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentDocument.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentDocument.java @@ -61,13 +61,9 @@ public class SuperAgentDocument extends BaseTableData { private String parseErrorMsg; - private String knowledgeScopeCode; + private Long knowledgeBaseId; - private String knowledgeScopeName; - - private String businessCategory; - - private String documentTags; + private String knowledgeBaseName; private Long currentPlanId; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeBase.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeBase.java new file mode 100644 index 0000000000000000000000000000000000000000..c4e7b45502a4815368506debfe59ebf346867690 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeBase.java @@ -0,0 +1,39 @@ +package org.javaup.ai.manage.data; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.javaup.database.data.BaseTableData; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@TableName("super_agent_knowledge_base") +@EqualsAndHashCode(callSuper = true) +public class SuperAgentKnowledgeBase extends BaseTableData { + + @TableId(value = "id", type = IdType.INPUT) + private Long id; + + private String baseName; + + private String description; + + private String embeddingModel; + + private String retrievalConfigJson; + + private String graphRagConfigJson; + + private String raptorConfigJson; + + private String metadataFilterJson; + + private Integer isDefault; + + private Integer sortOrder; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeRouteTrace.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeRouteTrace.java index 3cbd7574a7a7799675f3bc52d8a18fec03b29c87..d748cf920f8dc11ddc449a4a5dd514abbcff413a 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeRouteTrace.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeRouteTrace.java @@ -36,6 +36,14 @@ public class SuperAgentKnowledgeRouteTrace extends BaseTableData { private String mode; + private String knowledgeBaseSelectionMode; + + private String selectedKnowledgeBaseIdsJson; + + private String selectedKnowledgeBaseNamesJson; + + private String allowedDocumentIdsJson; + private String topScopesJson; private String topTopicsJson; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeScopeNode.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeScopeNode.java index 4a5f5672daf39ef4aae9ed899dbc8add691b3623..bad0a824c6d77ba6bfb0ee9f6b9f7936d1a730ff 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeScopeNode.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeScopeNode.java @@ -24,11 +24,11 @@ public class SuperAgentKnowledgeScopeNode extends BaseTableData { @TableId(value = "id", type = IdType.INPUT) private Long id; - private String scopeCode; + private Long knowledgeBaseId; private String scopeName; - private String parentScopeCode; + private Long parentScopeId; private String description; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeTopicNode.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeTopicNode.java index 770f7e99c55733bf77769d344610e09afbaff10a..da1042ef00bdbebcdbcb2950610adc2faaa8eda9 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeTopicNode.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentKnowledgeTopicNode.java @@ -24,11 +24,11 @@ public class SuperAgentKnowledgeTopicNode extends BaseTableData { @TableId(value = "id", type = IdType.INPUT) private Long id; - private String topicCode; + private Long knowledgeBaseId; private String topicName; - private String scopeCode; + private Long scopeId; private String description; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentTopicDocumentRelation.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentTopicDocumentRelation.java index 33228298668c1ba46a454d10bcbd8cd538048c2f..110bfd32b9f69cecf29786854d6e1847c15d8a2a 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentTopicDocumentRelation.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/data/SuperAgentTopicDocumentRelation.java @@ -26,7 +26,9 @@ public class SuperAgentTopicDocumentRelation extends BaseTableData { @TableId(value = "id", type = IdType.INPUT) private Long id; - private String topicCode; + private Long knowledgeBaseId; + + private Long topicId; private Long documentId; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/DocumentUploadDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/DocumentUploadDto.java index a7713c0a919ff49dd9e647f9e99cceb54e7c2363..8760504d47d3e488969fe48f75dab85f9bed1874 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/DocumentUploadDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/DocumentUploadDto.java @@ -15,11 +15,5 @@ public class DocumentUploadDto { private String operatorId; - private String knowledgeScopeCode; - - private String knowledgeScopeName; - - private String businessCategory; - - private String documentTags; + private String knowledgeBaseId; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseConfigUpdateDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseConfigUpdateDto.java new file mode 100644 index 0000000000000000000000000000000000000000..f7ae480613ed24a1cd995f323ab952dd4919900b --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseConfigUpdateDto.java @@ -0,0 +1,19 @@ +package org.javaup.ai.manage.dto; + +import lombok.Data; + +@Data +public class KnowledgeBaseConfigUpdateDto { + + private String id; + + private String retrievalConfigJson; + + private String graphRagConfigJson; + + private String raptorConfigJson; + + private String metadataFilterJson; + + private String operatorId; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseDeleteDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseDeleteDto.java new file mode 100644 index 0000000000000000000000000000000000000000..b8dfe302917be44368b77c962e5f1cd2093c4e3a --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseDeleteDto.java @@ -0,0 +1,9 @@ +package org.javaup.ai.manage.dto; + +import lombok.Data; + +@Data +public class KnowledgeBaseDeleteDto { + + private String id; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseDetailDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseDetailDto.java new file mode 100644 index 0000000000000000000000000000000000000000..fc65a7455621ba28c4c9c7e51d0c688d1d6d96b4 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseDetailDto.java @@ -0,0 +1,9 @@ +package org.javaup.ai.manage.dto; + +import lombok.Data; + +@Data +public class KnowledgeBaseDetailDto { + + private String id; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseSaveDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseSaveDto.java new file mode 100644 index 0000000000000000000000000000000000000000..ee79b3556d24d9f97bdc7eac5adbcef592b0188f --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeBaseSaveDto.java @@ -0,0 +1,29 @@ +package org.javaup.ai.manage.dto; + +import lombok.Data; + +@Data +public class KnowledgeBaseSaveDto { + + private String id; + + private String baseName; + + private String description; + + private String embeddingModel; + + private String retrievalConfigJson; + + private String graphRagConfigJson; + + private String raptorConfigJson; + + private String metadataFilterJson; + + private String isDefault; + + private String sortOrder; + + private String operatorId; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeDeleteDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeDeleteDto.java index 2e3d60a25950e241e9898da72487f4bb012c5e7f..036cd7e237bac35b12a5e0a08991c917ef5f0fe8 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeDeleteDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeDeleteDto.java @@ -10,7 +10,9 @@ import lombok.Data; @Data public class KnowledgeScopeDeleteDto { - private String scopeCode; + private String id; + + private String knowledgeBaseId; private String operatorId; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeQueryDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeQueryDto.java new file mode 100644 index 0000000000000000000000000000000000000000..53a7e4a400a636432d8e79d9503f8e653d112849 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeQueryDto.java @@ -0,0 +1,9 @@ +package org.javaup.ai.manage.dto; + +import lombok.Data; + +@Data +public class KnowledgeScopeQueryDto { + + private String knowledgeBaseId; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeSaveDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeSaveDto.java index b1aee2592ec3eecf718b6bff81c416de50afde63..04985bc3fede1e81b72f412ae2b6d7673ac1ca83 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeSaveDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeScopeSaveDto.java @@ -12,11 +12,11 @@ public class KnowledgeScopeSaveDto { private String id; - private String scopeCode; + private String knowledgeBaseId; private String scopeName; - private String parentScopeCode; + private String parentScopeId; private String description; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicDeleteDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicDeleteDto.java index 0b22b31ea9f1ad1dac0dec0e3fc2ac6b090ca142..50a25ba4fc820d691ad65e73bb1bb159b5f9578c 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicDeleteDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicDeleteDto.java @@ -10,7 +10,9 @@ import lombok.Data; @Data public class KnowledgeTopicDeleteDto { - private String topicCode; + private String id; + + private String knowledgeBaseId; private String operatorId; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicQueryDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicQueryDto.java index 33a371befadd80829c1ace3ad51062271f2820fe..1ca0726bacf70b67949e1077ac6d7245516fd465 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicQueryDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicQueryDto.java @@ -10,5 +10,7 @@ import lombok.Data; @Data public class KnowledgeTopicQueryDto { - private String scopeCode; + private String knowledgeBaseId; + + private String scopeId; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicSaveDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicSaveDto.java index 2c20c70d7af8efba02bebb377e6a1f46f76ddf2e..2d5ed86e1b2aedd10e9b17e1c742902bc0e0e9ec 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicSaveDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/KnowledgeTopicSaveDto.java @@ -12,11 +12,11 @@ public class KnowledgeTopicSaveDto { private String id; - private String topicCode; + private String knowledgeBaseId; private String topicName; - private String scopeCode; + private String scopeId; private String description; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationListQueryDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationListQueryDto.java index 2a37424ce083584c5bc9b2c6f3bfefe40a76b45f..72f87e75fc6563a57143dc8d4566c7051fcaf553 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationListQueryDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationListQueryDto.java @@ -10,5 +10,7 @@ import lombok.Data; @Data public class TopicDocumentRelationListQueryDto { - private String topicCode; + private String knowledgeBaseId; + + private String topicId; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationRemoveDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationRemoveDto.java index 8784bc7b9e639c62b9a9c366a769458df7a884c2..ead52b8192e5890db7af7fadcc41d2d29263a7a0 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationRemoveDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationRemoveDto.java @@ -10,7 +10,9 @@ import lombok.Data; @Data public class TopicDocumentRelationRemoveDto { - private String topicCode; + private String knowledgeBaseId; + + private String topicId; private String documentId; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationSaveDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationSaveDto.java index b9e7b545e0fe6ede565c867658f5b6eb5b3b2147..02b8774d3d256cc9323adf5ccc3d285d89588fd8 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationSaveDto.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/dto/TopicDocumentRelationSaveDto.java @@ -10,7 +10,9 @@ import lombok.Data; @Data public class TopicDocumentRelationSaveDto { - private String topicCode; + private String knowledgeBaseId; + + private String topicId; private String documentId; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/mapper/SuperAgentKnowledgeBaseMapper.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/mapper/SuperAgentKnowledgeBaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..3d635557ab1049ce4a81421ef4ab6fba7c551d82 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/mapper/SuperAgentKnowledgeBaseMapper.java @@ -0,0 +1,9 @@ +package org.javaup.ai.manage.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; + +@Mapper +public interface SuperAgentKnowledgeBaseMapper extends BaseMapper { +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/DocumentRetrieveFilters.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/DocumentRetrieveFilters.java index b5a07ba11410e75366f966cc5e90652ef427e616..fd99e151a74d35f658b0d42749ce76fd0103e660 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/DocumentRetrieveFilters.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/DocumentRetrieveFilters.java @@ -23,12 +23,6 @@ public class DocumentRetrieveFilters { @Builder.Default private List documentNameHints = new ArrayList<>(); - @Builder.Default - private List businessCategoryHints = new ArrayList<>(); - - @Builder.Default - private List documentTagHints = new ArrayList<>(); - @Builder.Default private List sectionPathHints = new ArrayList<>(); @@ -46,8 +40,6 @@ public class DocumentRetrieveFilters { public boolean isEmpty() { return documentNameHints.isEmpty() - && businessCategoryHints.isEmpty() - && documentTagHints.isEmpty() && sectionPathHints.isEmpty() && canonicalPathHints.isEmpty() && structureNodeIdHints.isEmpty() diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeBaseIndexingOptions.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeBaseIndexingOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..2f706aad33fefaa6ba3f5be8fe9da4cc17ae6642 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeBaseIndexingOptions.java @@ -0,0 +1,99 @@ +package org.javaup.ai.manage.model; + +import lombok.Data; +import org.javaup.ai.manage.config.DocumentManageProperties; + +@Data +public class KnowledgeBaseIndexingOptions { + + private ChunkOptions chunk = new ChunkOptions(); + + private GraphRagBuildOptions graphRag = new GraphRagBuildOptions(); + + private RaptorBuildOptions raptor = new RaptorBuildOptions(); + + public static KnowledgeBaseIndexingOptions fromDefaults(DocumentManageProperties properties, + Integer raptorMaxClusterSize, + Integer raptorMaxLevels, + Boolean raptorLlmSummaryEnabled, + Double raptorSummaryQualityFloor) { + KnowledgeBaseIndexingOptions options = new KnowledgeBaseIndexingOptions(); + DocumentManageProperties.Chunk chunk = properties == null ? new DocumentManageProperties.Chunk() : properties.getChunk(); + options.getChunk().setChildRecursiveMaxChars(defaultInteger(chunk.getRecursiveMaxChars(), 800)); + options.getChunk().setChildRecursiveOverlapChars(defaultInteger(chunk.getRecursiveOverlapChars(), 120)); + options.getChunk().setChildSemanticMaxChars(defaultInteger(chunk.getSemanticMaxChars(), 700)); + options.getChunk().setChildSemanticMinChars(defaultInteger(chunk.getSemanticMinChars(), 240)); + options.getChunk().setChildSemanticSimilarityThreshold(defaultDouble(chunk.getSemanticSimilarityThreshold(), 0.18D)); + options.getChunk().setParentBlockMaxChars(defaultInteger(chunk.getParentBlockMaxChars(), 2200)); + options.getChunk().setParentBlockOverlapChars(defaultInteger(chunk.getParentBlockOverlapChars(), 180)); + options.getChunk().setParentSemanticMaxChars(defaultInteger(chunk.getParentSemanticMaxChars(), 1600)); + options.getChunk().setParentSemanticMinChars(defaultInteger(chunk.getParentSemanticMinChars(), 480)); + + options.getGraphRag().setGraphRagBuildEnabled(Boolean.TRUE); + + options.getRaptor().setRaptorBuildEnabled(Boolean.TRUE); + options.getRaptor().setRaptorMaxClusterSize(defaultInteger(raptorMaxClusterSize, 6)); + options.getRaptor().setRaptorMaxLevels(defaultInteger(raptorMaxLevels, 3)); + options.getRaptor().setRaptorLlmSummaryEnabled(defaultBoolean(raptorLlmSummaryEnabled, true)); + options.getRaptor().setRaptorSummaryQualityFloor(defaultDouble(raptorSummaryQualityFloor, 0.42D)); + return options; + } + + public static KnowledgeBaseIndexingOptions defaults() { + return fromDefaults(new DocumentManageProperties(), 6, 3, true, 0.42D); + } + + private static Integer defaultInteger(Integer value, Integer defaultValue) { + return value == null ? defaultValue : value; + } + + private static Double defaultDouble(Double value, Double defaultValue) { + return value == null ? defaultValue : value; + } + + private static Boolean defaultBoolean(Boolean value, Boolean defaultValue) { + return value == null ? defaultValue : value; + } + + @Data + public static class ChunkOptions { + + private Integer childRecursiveMaxChars; + + private Integer childRecursiveOverlapChars; + + private Integer childSemanticMaxChars; + + private Integer childSemanticMinChars; + + private Double childSemanticSimilarityThreshold; + + private Integer parentBlockMaxChars; + + private Integer parentBlockOverlapChars; + + private Integer parentSemanticMaxChars; + + private Integer parentSemanticMinChars; + } + + @Data + public static class GraphRagBuildOptions { + + private Boolean graphRagBuildEnabled; + } + + @Data + public static class RaptorBuildOptions { + + private Boolean raptorBuildEnabled; + + private Integer raptorMaxClusterSize; + + private Integer raptorMaxLevels; + + private Boolean raptorLlmSummaryEnabled; + + private Double raptorSummaryQualityFloor; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeBaseSelectionSnapshot.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeBaseSelectionSnapshot.java new file mode 100644 index 0000000000000000000000000000000000000000..972891ca26b515ff803651b6d21752ba5cf19025 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeBaseSelectionSnapshot.java @@ -0,0 +1,49 @@ +package org.javaup.ai.manage.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; +import org.javaup.enums.KnowledgeBaseSelectionMode; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class KnowledgeBaseSelectionSnapshot { + + @Builder.Default + private KnowledgeBaseSelectionMode selectionMode = KnowledgeBaseSelectionMode.NONE; + + @Builder.Default + private List selectedKnowledgeBaseIds = new ArrayList<>(); + + @Builder.Default + private List selectedKnowledgeBaseNames = new ArrayList<>(); + + @Builder.Default + private List selectedKnowledgeBases = new ArrayList<>(); + + @Builder.Default + private List allowedDocuments = new ArrayList<>(); + + @Builder.Default + private List allowedDocumentIds = new ArrayList<>(); + + @Builder.Default + private List allowedTaskIds = new ArrayList<>(); + + private RagRuntimeOptions ragRuntimeOptions; + + public static KnowledgeBaseSelectionSnapshot none(RagRuntimeOptions options) { + return KnowledgeBaseSelectionSnapshot.builder() + .selectionMode(KnowledgeBaseSelectionMode.NONE) + .ragRuntimeOptions(options) + .build(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeDocumentDescriptor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeDocumentDescriptor.java index f4547ecbda36244a39e6b517f4fd66b23d8b67c1..02e382c6686522bc524386f21a2782cfe588fc1c 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeDocumentDescriptor.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/KnowledgeDocumentDescriptor.java @@ -21,11 +21,7 @@ public class KnowledgeDocumentDescriptor { private Long lastIndexTaskId; - private String knowledgeScopeCode; + private Long knowledgeBaseId; - private String knowledgeScopeName; - - private String businessCategory; - - private String documentTags; + private String knowledgeBaseName; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/StructureAnchoredEvidenceRequest.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/StructureAnchoredEvidenceRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..39514c825b284892f173f26dd231e4a704e41393 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/StructureAnchoredEvidenceRequest.java @@ -0,0 +1,43 @@ +package org.javaup.ai.manage.model; + +import lombok.Builder; +import lombok.Data; +import org.springframework.ai.document.Document; + +import java.util.ArrayList; +import java.util.List; + +/** + * 结构锚点正文证据扩展请求。只使用结构化 metadata,不使用业务关键词。 + */ +@Data +@Builder +public class StructureAnchoredEvidenceRequest { + + @Builder.Default + private List candidateDocuments = new ArrayList<>(); + + @Builder.Default + private List sectionAnchors = new ArrayList<>(); + + @Builder.Default + private List structureNodeIds = new ArrayList<>(); + + @Builder.Default + private List canonicalPaths = new ArrayList<>(); + + @Builder.Default + private List documentIds = new ArrayList<>(); + + @Builder.Default + private List taskIds = new ArrayList<>(); + + @Builder.Default + private List knowledgeBaseIds = new ArrayList<>(); + + private int maxPerAnchor; + + private int maxTotal; + + private int maxChars; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/es/DocumentKeywordIndexRecord.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/es/DocumentKeywordIndexRecord.java index 1433eb2454c5412d833af309421fa158f2b19b02..f97dff593d7e66c6ef11236909ca4009d9ffd8fa 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/es/DocumentKeywordIndexRecord.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/es/DocumentKeywordIndexRecord.java @@ -32,6 +32,10 @@ public class DocumentKeywordIndexRecord { private String documentName; + private Long knowledgeBaseId; + + private String knowledgeBaseName; + private String sectionPath; private Long structureNodeId; @@ -50,15 +54,6 @@ public class DocumentKeywordIndexRecord { private String sourceBlockIds; - private String knowledgeScopeCode; - - private String knowledgeScopeName; - - private String businessCategory; - - @Builder.Default - private List documentTags = new ArrayList<>(); - private String contentWithWeight; private String chunkType; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/es/KnowledgeRouteIndexRecord.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/es/KnowledgeRouteIndexRecord.java index d28b5ad8ae39dc3976b81c4cd379d1195fe566f0..0280b55e1be4ad7f8846a3abec69c8f446b9acc8 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/es/KnowledgeRouteIndexRecord.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/es/KnowledgeRouteIndexRecord.java @@ -24,22 +24,22 @@ public class KnowledgeRouteIndexRecord { private String entityType; - private String entityCode; + private Long entityId; private Long documentId; - private String scopeCode; + private Long knowledgeBaseId; + + private Long scopeId; private String scopeName; - private String topicCode; + private Long topicId; private String topicName; private String documentName; - private String businessCategory; - private String displayName; private String descriptionText; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/raptor/RaptorSearchResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/raptor/RaptorSearchResult.java index 9af48d8e92a675ed43d7a5618c6954c6d43e4a1b..c7f469c7d7c2d74aa21a282a8b65f3d750c864ed 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/raptor/RaptorSearchResult.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/raptor/RaptorSearchResult.java @@ -23,6 +23,8 @@ public class RaptorSearchResult { private String raptorSummary; + private String sourceStatus; + private Long chunkId; private Long parentBlockId; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/DocumentRouteCandidate.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/DocumentRouteCandidate.java index e075587416d5b00bc8c7b9fadd8fded73604ae2f..4499050a07ae72b86055605ad5bb1c88554fc885 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/DocumentRouteCandidate.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/DocumentRouteCandidate.java @@ -22,14 +22,6 @@ public class DocumentRouteCandidate { private String lastIndexTaskId; - private String knowledgeScopeCode; - - private String knowledgeScopeName; - - private String businessCategory; - - private String documentTags; - private BigDecimal score; private String reason; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/KnowledgeRouteContext.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/KnowledgeRouteContext.java new file mode 100644 index 0000000000000000000000000000000000000000..b7b2c32e48850c1a0dc3a47210aa65ba5673929e --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/KnowledgeRouteContext.java @@ -0,0 +1,37 @@ +package org.javaup.ai.manage.model.route; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.enums.KnowledgeBaseSelectionMode; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class KnowledgeRouteContext { + + private String question; + + private String rewriteQuestion; + + @Builder.Default + private KnowledgeBaseSelectionMode knowledgeBaseSelectionMode = KnowledgeBaseSelectionMode.NONE; + + @Builder.Default + private List selectedKnowledgeBaseIds = new ArrayList<>(); + + @Builder.Default + private List selectedKnowledgeBaseNames = new ArrayList<>(); + + @Builder.Default + private List allowedDocuments = new ArrayList<>(); + + @Builder.Default + private List allowedDocumentIds = new ArrayList<>(); +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/ScopeRouteCandidate.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/ScopeRouteCandidate.java index 5af07ce99ae02aa4944a574172875262cbbc2cd1..48c2b38393cf7a9ff3f328fc35e88f3073c58ee8 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/ScopeRouteCandidate.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/ScopeRouteCandidate.java @@ -16,7 +16,7 @@ import java.math.BigDecimal; @AllArgsConstructor public class ScopeRouteCandidate { - private String scopeCode; + private Long scopeId; private String scopeName; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/TopicRouteCandidate.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/TopicRouteCandidate.java index 89432abc126fd7d7497d3742225366f29f764068..e6c3d6662d8972fdd71ec3680740f28fce3582e3 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/TopicRouteCandidate.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/model/route/TopicRouteCandidate.java @@ -16,11 +16,11 @@ import java.math.BigDecimal; @AllArgsConstructor public class TopicRouteCandidate { - private String topicCode; + private Long topicId; private String topicName; - private String scopeCode; + private Long scopeId; private BigDecimal score; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/DocumentKnowledgeService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/DocumentKnowledgeService.java index 039ba5823c1114335075046f832a1524b9272779..018fe0d015bb1cc5f028c418e37c454f13ca4d37 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/DocumentKnowledgeService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/DocumentKnowledgeService.java @@ -2,8 +2,10 @@ package org.javaup.ai.manage.service; import org.javaup.ai.manage.model.DocumentRetrieveRequest; import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.ai.manage.model.StructureAnchoredEvidenceRequest; import org.springframework.ai.document.Document; +import java.util.Collection; import java.util.List; /** @@ -16,9 +18,15 @@ public interface DocumentKnowledgeService { List listRetrievableDocuments(); + List listRetrievableDocumentsByKnowledgeBaseIds(Collection knowledgeBaseIds); + List vectorSearch(DocumentRetrieveRequest request); List keywordSearch(DocumentRetrieveRequest request); List elevateToParentBlocks(List childDocuments, int maxChars); + + default List expandStructureAnchoredEvidence(StructureAnchoredEvidenceRequest request) { + return List.of(); + } } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/DocumentStructureNodeService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/DocumentStructureNodeService.java index 2bec8f798a7c7b0f6c5f6bfc296b3b1b1b2a2992..30372be103314824966652a6d1636015f2d9e39f 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/DocumentStructureNodeService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/DocumentStructureNodeService.java @@ -22,5 +22,21 @@ public interface DocumentStructureNodeService { Map nodeMap(Long documentId, Long parseTaskId); + default List listChildren(Long documentId, Long parseTaskId, Long parentNodeId) { + return List.of(); + } + + default SuperAgentDocumentStructureNode findById(Long documentId, Long parseTaskId, Long nodeId) { + return null; + } + + default SuperAgentDocumentStructureNode findPreviousSibling(Long documentId, Long parseTaskId, Long nodeId) { + return null; + } + + default SuperAgentDocumentStructureNode findNextSibling(Long documentId, Long parseTaskId, Long nodeId) { + return null; + } + void deleteByDocumentId(Long documentId); } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeBaseManageService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeBaseManageService.java new file mode 100644 index 0000000000000000000000000000000000000000..4cb2f828bee1df9e37dd2fe6ad8b2601f067ae89 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeBaseManageService.java @@ -0,0 +1,33 @@ +package org.javaup.ai.manage.service; + +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; +import org.javaup.ai.manage.dto.KnowledgeBaseConfigUpdateDto; +import org.javaup.ai.manage.dto.KnowledgeBaseDeleteDto; +import org.javaup.ai.manage.dto.KnowledgeBaseDetailDto; +import org.javaup.ai.manage.dto.KnowledgeBaseSaveDto; +import org.javaup.ai.manage.vo.KnowledgeBaseItemVo; +import org.javaup.ai.manage.vo.KnowledgeBaseOptionVo; + +import java.util.Collection; +import java.util.List; + +public interface KnowledgeBaseManageService { + + KnowledgeBaseItemVo save(KnowledgeBaseSaveDto dto); + + boolean delete(KnowledgeBaseDeleteDto dto); + + List list(); + + KnowledgeBaseItemVo detail(KnowledgeBaseDetailDto dto); + + KnowledgeBaseItemVo updateConfig(KnowledgeBaseConfigUpdateDto dto); + + List listOptions(); + + List listEnabledByIds(Collection ids); + + List listAllEnabled(); + + SuperAgentKnowledgeBase requireEnabled(Long id); +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeBaseRetrievalScopeService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeBaseRetrievalScopeService.java new file mode 100644 index 0000000000000000000000000000000000000000..d84cb479cb1c891ad769dc4ede89915b77dddd93 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeBaseRetrievalScopeService.java @@ -0,0 +1,14 @@ +package org.javaup.ai.manage.service; + +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; +import org.javaup.enums.ChatQueryMode; +import org.javaup.enums.KnowledgeBaseSelectionMode; + +import java.util.Collection; + +public interface KnowledgeBaseRetrievalScopeService { + + KnowledgeBaseSelectionSnapshot resolve(ChatQueryMode chatMode, + KnowledgeBaseSelectionMode selectionMode, + Collection selectedKnowledgeBaseIds); +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeManageService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeManageService.java index 3b907fda17434ac3fe037aaadf38cab0dd01c047..b2450964d66cde941b378aafd743d8c75feef8ce 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeManageService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeManageService.java @@ -5,6 +5,7 @@ import org.javaup.ai.manage.dto.DocumentProfileDetailQueryDto; import org.javaup.ai.manage.dto.DocumentProfileRegenerateDto; import org.javaup.ai.manage.dto.KnowledgeRouteTraceQueryDto; import org.javaup.ai.manage.dto.KnowledgeScopeDeleteDto; +import org.javaup.ai.manage.dto.KnowledgeScopeQueryDto; import org.javaup.ai.manage.dto.KnowledgeScopeSaveDto; import org.javaup.ai.manage.dto.KnowledgeTopicDeleteDto; import org.javaup.ai.manage.dto.KnowledgeTopicQueryDto; @@ -31,7 +32,7 @@ public interface KnowledgeManageService { boolean deleteScope(KnowledgeScopeDeleteDto dto); - List listScopes(); + List listScopes(KnowledgeScopeQueryDto dto); KnowledgeTopicItemVo saveTopic(KnowledgeTopicSaveDto dto); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeRouteIndexService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeRouteIndexService.java index 0b0853a9d07e46354799dcb7989c61aff948a0e0..c8cc79eb1c6b454e5b8d7c1091a0f4e0c199ff06 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeRouteIndexService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeRouteIndexService.java @@ -1,5 +1,6 @@ package org.javaup.ai.manage.service; +import java.util.Collection; import java.util.List; /** @@ -12,17 +13,18 @@ public interface KnowledgeRouteIndexService { void refreshIfNeeded(); - List search(String routingText, String entityType, int size); + List search(String routingText, String entityType, int size, Collection knowledgeBaseIds); void deleteDocumentRoute(Long documentId); record RouteLexicalHit( String routeId, - String entityCode, + Long entityId, String entityType, Long documentId, - String scopeCode, - String topicCode, + Long knowledgeBaseId, + Long scopeId, + Long topicId, String documentName, double score ) { diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeRouteService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeRouteService.java index e1595e038a12e658beaf757194318fcd8fdabe86..a566174987038e5c6f369303e819df3378859e2d 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeRouteService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/KnowledgeRouteService.java @@ -1,5 +1,6 @@ package org.javaup.ai.manage.service; +import org.javaup.ai.manage.model.route.KnowledgeRouteContext; import org.javaup.ai.manage.model.route.KnowledgeRouteDecision; /** @@ -9,17 +10,15 @@ import org.javaup.ai.manage.model.route.KnowledgeRouteDecision; **/ public interface KnowledgeRouteService { - KnowledgeRouteDecision route(String question, String rewriteQuestion); + KnowledgeRouteDecision route(KnowledgeRouteContext context); void recordShadowRoute(String conversationId, long exchangeId, Long selectedDocumentId, - String question, - String rewriteQuestion); + KnowledgeRouteContext context); void recordAutoRoute(String conversationId, long exchangeId, - String question, - String rewriteQuestion, + KnowledgeRouteContext context, KnowledgeRouteDecision decision); } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/RaptorBuildService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/RaptorBuildService.java index 8145a9aba357c9dc51dca635f245b8b05a8bab65..acc94eba4a53a148b88051ef09d3ff5f2b6c3bf0 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/RaptorBuildService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/RaptorBuildService.java @@ -9,7 +9,7 @@ public interface RaptorBuildService { RaptorBuildResult rebuildDocumentTree(Long documentId, Long taskId, List chunks); - RaptorBuildResult rebuildKnowledgeScopeTree(String knowledgeScopeCode); + RaptorBuildResult rebuildKnowledgeScopeTree(Long knowledgeBaseId, Long scopeId); void deleteByTask(Long documentId, Long taskId); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentAsyncProcessServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentAsyncProcessServiceImpl.java index e18fcd9ceb8c1515f28816b54b0b9307f37071da..cd9185e2047c2c95dcea727ed0f32a002e8e40ed 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentAsyncProcessServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentAsyncProcessServiceImpl.java @@ -54,7 +54,6 @@ import org.javaup.ai.manage.support.DocumentStrategyPlanDraft; import org.javaup.ai.manage.support.DocumentStrategyStepDraft; import org.javaup.ai.manage.support.MybatisBatchExecutor; import org.javaup.ai.manage.support.ParentBlockCandidate; -import org.javaup.ai.manage.support.RaptorScopeSupport; import org.javaup.enums.BusinessStatus; import org.javaup.enums.DocumentChunkSourceTypeEnum; import org.javaup.enums.DocumentFileTypeEnum; @@ -102,8 +101,6 @@ public class DocumentAsyncProcessServiceImpl implements DocumentAsyncProcessServ private static final Set RUNNING_INDEX_TASK_IDS = ConcurrentHashMap.newKeySet(); - private static final Set RUNNING_DATASET_RAPTOR_SCOPE_KEYS = ConcurrentHashMap.newKeySet(); - private static final TypeReference> EXT_JSON_TYPE = new TypeReference<>() { }; @@ -864,8 +861,6 @@ public class DocumentAsyncProcessServiceImpl implements DocumentAsyncProcessServ document.setLastIndexTaskId(taskId); documentMapper.updateById(document); - submitDatasetRaptorBuild(document, taskId); - finishTaskSuccess(task, DocumentTaskStageEnum.STORE_COMPLETE.getCode(), startTime); progressCacheService.update(document, task); saveIndexBuildLog(taskId, documentId, @@ -919,143 +914,6 @@ public class DocumentAsyncProcessServiceImpl implements DocumentAsyncProcessServ } } - private void submitDatasetRaptorBuild(SuperAgentDocument document, Long taskId) { - if (document == null || StrUtil.isBlank(document.getKnowledgeScopeCode())) { - return; - } - Long documentId = document.getId(); - String knowledgeScopeCode = document.getKnowledgeScopeCode(); - String knowledgeScopeName = document.getKnowledgeScopeName(); - String normalizedScopeCode = RaptorScopeSupport.normalizeScopeCode(knowledgeScopeCode); - String scopeKey = RaptorScopeSupport.knowledgeScopeKey(normalizedScopeCode); - if (StrUtil.isBlank(normalizedScopeCode) || StrUtil.isBlank(scopeKey)) { - return; - } - if (!RUNNING_DATASET_RAPTOR_SCOPE_KEYS.add(scopeKey)) { - log.info("RAPTOR dataset-level 构建已在后台执行中,跳过重复提交,documentId={}, taskId={}, scopeKey={}", - documentId, taskId, scopeKey); - saveIndexBuildLog(taskId, documentId, - DocumentTaskStageEnum.RAPTOR.getCode(), - DocumentTaskEventTypeEnum.COMPLETE.getCode(), - DocumentLogLevelEnum.INFO.getCode(), - DocumentOperatorTypeEnum.SYSTEM.getCode(), - null, - "knowledge scope 级 RAPTOR 跨文档摘要树已有后台任务执行中,本次跳过重复提交。", - detail("knowledgeScopeCode", knowledgeScopeCode, - "knowledgeScopeName", knowledgeScopeName, - "scopeKey", scopeKey, - "async", true, - "deduplicated", true)); - return; - } - try { - datasetRaptorExecutorService.execute(() -> { - try { - runDatasetRaptorBuild(documentId, taskId, knowledgeScopeCode, knowledgeScopeName, scopeKey); - } - finally { - RUNNING_DATASET_RAPTOR_SCOPE_KEYS.remove(scopeKey); - } - }); - log.info("RAPTOR dataset-level 构建已提交后台执行,documentId={}, taskId={}, scopeKey={}", - documentId, taskId, scopeKey); - saveIndexBuildLog(taskId, documentId, - DocumentTaskStageEnum.RAPTOR.getCode(), - DocumentTaskEventTypeEnum.START.getCode(), - DocumentLogLevelEnum.INFO.getCode(), - DocumentOperatorTypeEnum.SYSTEM.getCode(), - null, - "knowledge scope 级 RAPTOR 跨文档摘要树已提交后台构建。", - detail("knowledgeScopeCode", knowledgeScopeCode, - "knowledgeScopeName", knowledgeScopeName, - "scopeKey", scopeKey, - "async", true)); - } - catch (RejectedExecutionException exception) { - RUNNING_DATASET_RAPTOR_SCOPE_KEYS.remove(scopeKey); - log.warn("RAPTOR dataset-level 后台线程池已满,跳过本次提交,documentId={}, taskId={}, scopeKey={}", - documentId, taskId, scopeKey, exception); - saveIndexBuildLog(taskId, documentId, - DocumentTaskStageEnum.RAPTOR.getCode(), - DocumentTaskEventTypeEnum.FAILED.getCode(), - DocumentLogLevelEnum.WARN.getCode(), - DocumentOperatorTypeEnum.SYSTEM.getCode(), - null, - "knowledge scope 级 RAPTOR 跨文档摘要树后台线程池已满,本次未提交。", - detail("knowledgeScopeCode", knowledgeScopeCode, - "knowledgeScopeName", knowledgeScopeName, - "scopeKey", scopeKey, - "async", true, - "error", exception.getMessage())); - } - } - - private void runDatasetRaptorBuild(Long documentId, - Long taskId, - String knowledgeScopeCode, - String knowledgeScopeName, - String scopeKey) { - long datasetRaptorStartedNanos = System.nanoTime(); - saveIndexBuildLog(taskId, documentId, - DocumentTaskStageEnum.RAPTOR.getCode(), - DocumentTaskEventTypeEnum.START.getCode(), - DocumentLogLevelEnum.INFO.getCode(), - DocumentOperatorTypeEnum.SYSTEM.getCode(), - null, - "开始后台构建 knowledge scope 级 RAPTOR 跨文档摘要树。", - detail("knowledgeScopeCode", knowledgeScopeCode, - "knowledgeScopeName", knowledgeScopeName, - "scopeKey", scopeKey, - "async", true)); - try { - RaptorBuildResult datasetRaptorBuildResult = raptorBuildService.rebuildKnowledgeScopeTree(knowledgeScopeCode); - long datasetRaptorCostMillis = elapsedMillis(datasetRaptorStartedNanos); - log.info("RAPTOR dataset-level 后台构建完成,documentId={}, taskId={}, scopeKey={}, inputMode={}, inputCount={}, nodeCount={}, levelCount={}, sourceChunkCount={}, costMillis={}", - documentId, taskId, scopeKey, datasetRaptorBuildResult.getInputMode(), datasetRaptorBuildResult.getInputCount(), - datasetRaptorBuildResult.getNodeCount(), datasetRaptorBuildResult.getLevelCount(), - datasetRaptorBuildResult.getSourceChunkCount(), datasetRaptorCostMillis); - saveIndexBuildLog(taskId, documentId, - DocumentTaskStageEnum.RAPTOR.getCode(), - DocumentTaskEventTypeEnum.COMPLETE.getCode(), - DocumentLogLevelEnum.INFO.getCode(), - DocumentOperatorTypeEnum.SYSTEM.getCode(), - null, - "knowledge scope 级 RAPTOR 跨文档摘要树后台构建完成,耗时 " + datasetRaptorCostMillis + "ms。", - detail("knowledgeScopeCode", knowledgeScopeCode, - "knowledgeScopeName", knowledgeScopeName, - "scopeKey", scopeKey, - "async", true, - "inputMode", datasetRaptorBuildResult.getInputMode(), - "inputCount", datasetRaptorBuildResult.getInputCount(), - "reusableSummaryInputCount", datasetRaptorBuildResult.getReusableSummaryInputCount(), - "originalChunkInputCount", datasetRaptorBuildResult.getOriginalChunkInputCount(), - "nodeCount", datasetRaptorBuildResult.getNodeCount(), - "levelCount", datasetRaptorBuildResult.getLevelCount(), - "sourceChunkCount", datasetRaptorBuildResult.getSourceChunkCount(), - "sourceQualityReport", datasetRaptorBuildResult.getSourceQualityReport(), - "savedQualityReport", datasetRaptorBuildResult.getSavedQualityReport(), - "costMillis", datasetRaptorCostMillis)); - } - catch (Exception exception) { - long datasetRaptorCostMillis = elapsedMillis(datasetRaptorStartedNanos); - log.error("RAPTOR dataset-level 后台构建失败,documentId={}, taskId={}, scopeKey={}, costMillis={}", - documentId, taskId, scopeKey, datasetRaptorCostMillis, exception); - saveIndexBuildLog(taskId, documentId, - DocumentTaskStageEnum.RAPTOR.getCode(), - DocumentTaskEventTypeEnum.FAILED.getCode(), - DocumentLogLevelEnum.ERROR.getCode(), - DocumentOperatorTypeEnum.SYSTEM.getCode(), - null, - "knowledge scope 级 RAPTOR 跨文档摘要树后台构建失败,单文档索引结果保持成功。", - detail("knowledgeScopeCode", knowledgeScopeCode, - "knowledgeScopeName", knowledgeScopeName, - "scopeKey", scopeKey, - "async", true, - "error", exception.getMessage(), - "costMillis", datasetRaptorCostMillis)); - } - } - private void cleanupIndexBuildTaskArtifacts(Long documentId, Long taskId) { log.warn("检测到索引构建任务重入,开始清理同 task 旧产物,documentId={}, taskId={}", documentId, taskId); long startedNanos = System.nanoTime(); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentKnowledgeServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentKnowledgeServiceImpl.java index 10ebd5560d282d3f95867fd0ea67c0c3d0efc361..765226a1afd2295259ec6bf7b4aec8acc63c5f9a 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentKnowledgeServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentKnowledgeServiceImpl.java @@ -6,18 +6,22 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.javaup.ai.manage.data.SuperAgentDocument; +import org.javaup.ai.manage.data.SuperAgentDocumentChunk; import org.javaup.ai.manage.data.SuperAgentDocumentParentBlock; import org.javaup.ai.manage.mapper.SuperAgentDocumentMapper; +import org.javaup.ai.manage.mapper.SuperAgentDocumentChunkMapper; import org.javaup.ai.manage.mapper.SuperAgentDocumentParentBlockMapper; import org.javaup.ai.manage.model.DocumentRetrieveFilters; import org.javaup.ai.manage.model.DocumentRetrieveRequest; import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.ai.manage.model.StructureAnchoredEvidenceRequest; import org.javaup.ai.manage.service.DocumentKnowledgeService; import org.javaup.ai.manage.service.keyword.DocumentKeywordSearchGateway; import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; import org.javaup.ai.manage.support.DocumentPgVectorConstants; import org.javaup.ai.manage.support.GraphRagTypedChunkMetadataSupport; import org.javaup.enums.BusinessStatus; +import org.javaup.enums.DocumentChunkSourceTypeEnum; import org.javaup.enums.DocumentIndexStatusEnum; import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; @@ -28,6 +32,7 @@ import org.springframework.stereotype.Service; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Collection; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; @@ -50,6 +55,21 @@ import java.util.stream.IntStream; @Service public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { + private static final String STRUCTURE_ANCHOR_CHANNEL = "structure-anchor"; + private static final String STRUCTURE_ANCHOR_BODY_CANDIDATE = "STRUCTURE_ANCHOR_BODY_CANDIDATE"; + private static final String MATCH_NODE_ID = "NODE_ID"; + private static final String MATCH_CANONICAL_EXACT = "CANONICAL_EXACT"; + private static final String MATCH_CANONICAL_DESCENDANT = "CANONICAL_DESCENDANT"; + private static final String MATCH_TITLE_SAME_SECTION = "TITLE_SAME_SECTION"; + private static final String BODY_RESOLVED_NODE_TEXT = "NODE_TEXT"; + private static final String BODY_RESOLVED_PARENT_TEXT = "PARENT_TEXT"; + private static final String BODY_RESOLVED_CONTINUATION_LIST = "CONTINUATION_LIST"; + private static final String BODY_RESOLVED_DIRECT_CHILD = "DIRECT_CHILD"; + private static final String BODY_RESOLVED_DESCENDANT = "DESCENDANT"; + private static final String BODY_KIND_TEXT_CHUNK = "TEXT_CHUNK"; + private static final String BODY_KIND_LIST_CONTINUATION = "LIST_CONTINUATION"; + private static final String BODY_KIND_CHILD_SECTION = "CHILD_SECTION"; + private static final String VECTOR_RETRIEVE_SQL_TEMPLATE = """ SELECT id, @@ -82,6 +102,8 @@ public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { private final SuperAgentDocumentMapper documentMapper; private final SuperAgentDocumentParentBlockMapper parentBlockMapper; + + private final SuperAgentDocumentChunkMapper documentChunkMapper; @Qualifier("documentManagePgVectorJdbcTemplate") private final JdbcTemplate pgVectorJdbcTemplate; @@ -95,12 +117,32 @@ public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { @Override public List listRetrievableDocuments() { - List documents = documentMapper.selectList(new LambdaQueryWrapper() + return toDescriptors(documentMapper.selectList(new LambdaQueryWrapper() + .eq(SuperAgentDocument::getStatus, BusinessStatus.YES.getCode()) + .eq(SuperAgentDocument::getIndexStatus, DocumentIndexStatusEnum.BUILD_SUCCESS.getCode()) + .isNotNull(SuperAgentDocument::getLastIndexTaskId) + .orderByDesc(SuperAgentDocument::getEditTime) + .orderByDesc(SuperAgentDocument::getId))); + } + + @Override + public List listRetrievableDocumentsByKnowledgeBaseIds(Collection knowledgeBaseIds) { + List ids = knowledgeBaseIds == null + ? List.of() + : knowledgeBaseIds.stream().filter(Objects::nonNull).distinct().toList(); + if (ids.isEmpty()) { + return List.of(); + } + return toDescriptors(documentMapper.selectList(new LambdaQueryWrapper() .eq(SuperAgentDocument::getStatus, BusinessStatus.YES.getCode()) .eq(SuperAgentDocument::getIndexStatus, DocumentIndexStatusEnum.BUILD_SUCCESS.getCode()) .isNotNull(SuperAgentDocument::getLastIndexTaskId) + .in(SuperAgentDocument::getKnowledgeBaseId, ids) .orderByDesc(SuperAgentDocument::getEditTime) - .orderByDesc(SuperAgentDocument::getId)); + .orderByDesc(SuperAgentDocument::getId))); + } + + private List toDescriptors(List documents) { if (CollUtil.isEmpty(documents)) { return List.of(); } @@ -110,10 +152,8 @@ public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { document.getId(), document.getDocumentName(), document.getLastIndexTaskId(), - document.getKnowledgeScopeCode(), - document.getKnowledgeScopeName(), - document.getBusinessCategory(), - document.getDocumentTags() + document.getKnowledgeBaseId(), + document.getKnowledgeBaseName() )) .toList(); } @@ -273,6 +313,499 @@ public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { return elevatedDocuments; } + @Override + public List expandStructureAnchoredEvidence(StructureAnchoredEvidenceRequest request) { + if (request == null) { + return List.of(); + } + int maxTotal = request.getMaxTotal() <= 0 ? 4 : request.getMaxTotal(); + int maxPerAnchor = request.getMaxPerAnchor() <= 0 ? 2 : request.getMaxPerAnchor(); + int maxChars = request.getMaxChars() <= 0 ? 2200 : request.getMaxChars(); + + List allowedDocumentIds = normalizeLongs(request.getDocumentIds()); + List allowedTaskIds = normalizeLongs(request.getTaskIds()); + List allowedKnowledgeBaseIds = normalizeLongs(request.getKnowledgeBaseIds()); + if (allowedDocumentIds.isEmpty() || allowedTaskIds.isEmpty()) { + return List.of(); + } + + Map descriptorMap = listDescriptorMap(allowedDocumentIds).values().stream() + .filter(descriptor -> descriptor != null && descriptor.getDocumentId() != null) + .filter(descriptor -> allowedKnowledgeBaseIds.isEmpty() + || descriptor.getKnowledgeBaseId() != null && allowedKnowledgeBaseIds.contains(descriptor.getKnowledgeBaseId())) + .collect(Collectors.toMap( + KnowledgeDocumentDescriptor::getDocumentId, + descriptor -> descriptor, + (left, right) -> left, + LinkedHashMap::new + )); + if (descriptorMap.isEmpty()) { + return List.of(); + } + + List probes = buildStructureAnchorProbes(request); + if (probes.isEmpty()) { + return List.of(); + } + + LinkedHashMap expanded = new LinkedHashMap<>(); + for (StructureAnchorProbe probe : probes) { + if (expanded.size() >= maxTotal) { + break; + } + List documents = findBodyEvidenceForProbe( + probe, + descriptorMap, + allowedDocumentIds, + allowedTaskIds, + maxPerAnchor, + maxChars + ); + for (Document document : documents) { + if (expanded.size() >= maxTotal) { + break; + } + expanded.putIfAbsent(structureEvidenceIdentity(document), document); + } + } + return new ArrayList<>(expanded.values()); + } + + private List buildStructureAnchorProbes(StructureAnchoredEvidenceRequest request) { + LinkedHashMap probes = new LinkedHashMap<>(); + for (Long structureNodeId : normalizeLongs(request.getStructureNodeIds())) { + probes.putIfAbsent("node:" + structureNodeId, + new StructureAnchorProbe(structureNodeId, "", "", MATCH_NODE_ID)); + } + for (String canonicalPath : normalizeTexts(request.getCanonicalPaths())) { + probes.putIfAbsent("canonical:" + normalizeAnchor(canonicalPath), + new StructureAnchorProbe(null, canonicalPath, "", MATCH_CANONICAL_EXACT)); + } + for (String sectionAnchor : normalizeTexts(request.getSectionAnchors())) { + probes.putIfAbsent("section:" + normalizeAnchor(sectionAnchor), + new StructureAnchorProbe(null, "", sectionAnchor, MATCH_TITLE_SAME_SECTION)); + } + if (request.getCandidateDocuments() != null) { + for (Document document : request.getCandidateDocuments()) { + if (document == null || document.getMetadata() == null) { + continue; + } + Long structureNodeId = asLong(document.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID)); + String canonicalPath = asText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH)); + String sectionPath = asText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.SECTION_PATH)); + if (structureNodeId != null) { + probes.putIfAbsent("candidate-node:" + structureNodeId, + new StructureAnchorProbe(structureNodeId, canonicalPath, sectionPath, MATCH_NODE_ID)); + continue; + } + if (StrUtil.isNotBlank(canonicalPath)) { + probes.putIfAbsent("candidate-canonical:" + normalizeAnchor(canonicalPath), + new StructureAnchorProbe(null, canonicalPath, sectionPath, MATCH_CANONICAL_EXACT)); + } + else if (StrUtil.isNotBlank(sectionPath)) { + probes.putIfAbsent("candidate-section:" + normalizeAnchor(sectionPath), + new StructureAnchorProbe(null, "", sectionPath, MATCH_TITLE_SAME_SECTION)); + } + } + } + return new ArrayList<>(probes.values()); + } + + private List findBodyEvidenceForProbe(StructureAnchorProbe probe, + Map descriptorMap, + List allowedDocumentIds, + List allowedTaskIds, + int maxPerAnchor, + int maxChars) { + List parentBlocks = queryParentBlocks(probe, allowedDocumentIds, allowedTaskIds, maxPerAnchor); + if (parentBlocks.isEmpty()) { + return List.of(); + } + List bodyEvidence = new ArrayList<>(); + for (SuperAgentDocumentParentBlock parentBlock : parentBlocks) { + KnowledgeDocumentDescriptor descriptor = descriptorMap.get(parentBlock.getDocumentId()); + if (descriptor == null) { + continue; + } + bodyEvidence.addAll(resolveRawBodyEvidence(parentBlock, descriptor, probe, allowedDocumentIds, allowedTaskIds, maxPerAnchor, maxChars)); + if (bodyEvidence.size() >= maxPerAnchor) { + break; + } + } + return bodyEvidence.stream() + .limit(Math.max(1, maxPerAnchor)) + .toList(); + } + + private List queryParentBlocks(StructureAnchorProbe probe, + List allowedDocumentIds, + List allowedTaskIds, + int maxPerAnchor) { + LambdaQueryWrapper exactWrapper = baseParentBlockWrapper(allowedDocumentIds, allowedTaskIds); + boolean hasExact = false; + if (probe.structureNodeId() != null) { + exactWrapper.eq(SuperAgentDocumentParentBlock::getStructureNodeId, probe.structureNodeId()); + hasExact = true; + } + else if (StrUtil.isNotBlank(probe.canonicalPath())) { + exactWrapper.eq(SuperAgentDocumentParentBlock::getCanonicalPath, probe.canonicalPath().trim()); + hasExact = true; + } + else if (StrUtil.isNotBlank(probe.sectionPath())) { + exactWrapper.eq(SuperAgentDocumentParentBlock::getSectionPath, probe.sectionPath().trim()); + hasExact = true; + } + if (hasExact) { + List exact = parentBlockMapper.selectList(exactWrapper + .orderByAsc(SuperAgentDocumentParentBlock::getParentNo) + .last("LIMIT " + Math.max(1, maxPerAnchor))); + if (!exact.isEmpty()) { + return exact; + } + } + + String descendantPath = StrUtil.blankToDefault(probe.canonicalPath(), ""); + if (StrUtil.isBlank(descendantPath)) { + descendantPath = StrUtil.blankToDefault(probe.sectionPath(), ""); + } + if (StrUtil.isBlank(descendantPath)) { + return List.of(); + } + return parentBlockMapper.selectList(baseParentBlockWrapper(allowedDocumentIds, allowedTaskIds) + .likeRight(SuperAgentDocumentParentBlock::getCanonicalPath, descendantPath.trim() + "/") + .orderByAsc(SuperAgentDocumentParentBlock::getCanonicalPath) + .orderByAsc(SuperAgentDocumentParentBlock::getParentNo) + .last("LIMIT " + Math.max(1, maxPerAnchor))); + } + + private LambdaQueryWrapper baseParentBlockWrapper(List allowedDocumentIds, + List allowedTaskIds) { + return new LambdaQueryWrapper() + .in(SuperAgentDocumentParentBlock::getDocumentId, allowedDocumentIds) + .in(SuperAgentDocumentParentBlock::getTaskId, allowedTaskIds) + .eq(SuperAgentDocumentParentBlock::getStatus, BusinessStatus.YES.getCode()); + } + + private List resolveRawBodyEvidence(SuperAgentDocumentParentBlock anchorParent, + KnowledgeDocumentDescriptor descriptor, + StructureAnchorProbe probe, + List allowedDocumentIds, + List allowedTaskIds, + int maxPerAnchor, + int maxChars) { + LinkedHashMap resolved = new LinkedHashMap<>(); + addChunkEvidence(resolved, queryNodeTextChunks(anchorParent, allowedDocumentIds, allowedTaskIds), + anchorParent, descriptor, probe, BODY_RESOLVED_NODE_TEXT, BODY_KIND_TEXT_CHUNK, maxChars); + addChunkEvidence(resolved, queryParentTextChunks(anchorParent, allowedDocumentIds, allowedTaskIds), + anchorParent, descriptor, probe, BODY_RESOLVED_PARENT_TEXT, BODY_KIND_TEXT_CHUNK, maxChars); + if (resolved.isEmpty()) { + addChunkEvidence(resolved, queryContinuationListChunks(anchorParent, allowedDocumentIds, allowedTaskIds, Math.max(2, maxPerAnchor)), + anchorParent, descriptor, probe, BODY_RESOLVED_CONTINUATION_LIST, BODY_KIND_LIST_CONTINUATION, maxChars); + } + if (resolved.size() < maxPerAnchor) { + addChunkEvidence(resolved, queryDirectChildTextChunks(anchorParent, allowedDocumentIds, allowedTaskIds, maxPerAnchor), + anchorParent, descriptor, probe, BODY_RESOLVED_DIRECT_CHILD, BODY_KIND_CHILD_SECTION, maxChars); + } + if (resolved.size() < maxPerAnchor) { + addChunkEvidence(resolved, queryDescendantTextChunks(anchorParent, allowedDocumentIds, allowedTaskIds, maxPerAnchor), + anchorParent, descriptor, probe, BODY_RESOLVED_DESCENDANT, BODY_KIND_TEXT_CHUNK, maxChars); + } + return resolved.values().stream() + .limit(Math.max(1, maxPerAnchor)) + .toList(); + } + + private void addChunkEvidence(Map target, + List chunks, + SuperAgentDocumentParentBlock anchorParent, + KnowledgeDocumentDescriptor descriptor, + StructureAnchorProbe probe, + String resolvedFrom, + String candidateKind, + int maxChars) { + if (CollUtil.isEmpty(chunks)) { + return; + } + for (SuperAgentDocumentChunk chunk : chunks) { + if (!isRawBodyChunk(chunk, BODY_RESOLVED_CONTINUATION_LIST.equals(resolvedFrom))) { + continue; + } + Document evidence = buildStructureAnchorChunkEvidence(chunk, anchorParent, descriptor, probe, resolvedFrom, candidateKind, maxChars); + target.putIfAbsent(structureEvidenceIdentity(evidence), evidence); + } + } + + private List queryNodeTextChunks(SuperAgentDocumentParentBlock anchorParent, + List allowedDocumentIds, + List allowedTaskIds) { + if (anchorParent.getStructureNodeId() == null) { + return List.of(); + } + return documentChunkMapper.selectList(baseChunkWrapper(allowedDocumentIds, allowedTaskIds) + .eq(SuperAgentDocumentChunk::getStructureNodeId, anchorParent.getStructureNodeId()) + .eq(SuperAgentDocumentChunk::getChunkType, "TEXT") + .orderByAsc(SuperAgentDocumentChunk::getChunkNo) + .last("LIMIT 4")); + } + + private List queryParentTextChunks(SuperAgentDocumentParentBlock anchorParent, + List allowedDocumentIds, + List allowedTaskIds) { + if (anchorParent.getId() == null) { + return List.of(); + } + return documentChunkMapper.selectList(baseChunkWrapper(allowedDocumentIds, allowedTaskIds) + .eq(SuperAgentDocumentChunk::getParentBlockId, anchorParent.getId()) + .eq(SuperAgentDocumentChunk::getChunkType, "TEXT") + .orderByAsc(SuperAgentDocumentChunk::getChunkNo) + .last("LIMIT 4")); + } + + private List queryContinuationListChunks(SuperAgentDocumentParentBlock anchorParent, + List allowedDocumentIds, + List allowedTaskIds, + int limit) { + Integer anchorNo = anchorParent.getParentNo(); + if (anchorNo == null) { + return List.of(); + } + List nextChunks = documentChunkMapper.selectList(baseChunkWrapper(allowedDocumentIds, allowedTaskIds) + .eq(SuperAgentDocumentChunk::getDocumentId, anchorParent.getDocumentId()) + .eq(SuperAgentDocumentChunk::getTaskId, anchorParent.getTaskId()) + .gt(SuperAgentDocumentChunk::getParentBlockId, anchorParent.getId()) + .orderByAsc(SuperAgentDocumentChunk::getChunkNo) + .last("LIMIT " + Math.max(2, limit * 2))); + return nextChunks.stream() + .filter(this::isListContinuationChunk) + .limit(Math.max(1, limit)) + .toList(); + } + + private List queryDirectChildTextChunks(SuperAgentDocumentParentBlock anchorParent, + List allowedDocumentIds, + List allowedTaskIds, + int limit) { + String canonicalPath = normalizeAnchor(anchorParent.getCanonicalPath()); + if (StrUtil.isBlank(canonicalPath)) { + return List.of(); + } + String childPrefix = canonicalPath + "/"; + return documentChunkMapper.selectList(baseChunkWrapper(allowedDocumentIds, allowedTaskIds) + .likeRight(SuperAgentDocumentChunk::getCanonicalPath, childPrefix) + .and(wrapper -> wrapper.eq(SuperAgentDocumentChunk::getChunkType, "TEXT") + .or() + .eq(SuperAgentDocumentChunk::getChunkType, "LIST")) + .orderByAsc(SuperAgentDocumentChunk::getCanonicalPath) + .orderByAsc(SuperAgentDocumentChunk::getChunkNo) + .last("LIMIT " + Math.max(1, limit * 2))).stream() + .filter(chunk -> isDirectCanonicalChild(canonicalPath, chunk.getCanonicalPath())) + .limit(Math.max(1, limit)) + .toList(); + } + + private List queryDescendantTextChunks(SuperAgentDocumentParentBlock anchorParent, + List allowedDocumentIds, + List allowedTaskIds, + int limit) { + String canonicalPath = normalizeAnchor(anchorParent.getCanonicalPath()); + if (StrUtil.isBlank(canonicalPath)) { + return List.of(); + } + return documentChunkMapper.selectList(baseChunkWrapper(allowedDocumentIds, allowedTaskIds) + .likeRight(SuperAgentDocumentChunk::getCanonicalPath, canonicalPath + "/") + .and(wrapper -> wrapper.eq(SuperAgentDocumentChunk::getChunkType, "TEXT") + .or() + .eq(SuperAgentDocumentChunk::getChunkType, "LIST")) + .orderByAsc(SuperAgentDocumentChunk::getCanonicalPath) + .orderByAsc(SuperAgentDocumentChunk::getChunkNo) + .last("LIMIT " + Math.max(1, limit))); + } + + private LambdaQueryWrapper baseChunkWrapper(List allowedDocumentIds, + List allowedTaskIds) { + return new LambdaQueryWrapper() + .in(SuperAgentDocumentChunk::getDocumentId, allowedDocumentIds) + .in(SuperAgentDocumentChunk::getTaskId, allowedTaskIds) + .eq(SuperAgentDocumentChunk::getSourceType, DocumentChunkSourceTypeEnum.ORIGINAL.getCode()) + .eq(SuperAgentDocumentChunk::getStatus, BusinessStatus.YES.getCode()); + } + + private Document buildStructureAnchorChunkEvidence(SuperAgentDocumentChunk chunk, + SuperAgentDocumentParentBlock anchorParent, + KnowledgeDocumentDescriptor descriptor, + StructureAnchorProbe probe, + String resolvedFrom, + String candidateKind, + int maxChars) { + Map metadata = new LinkedHashMap<>(); + metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "DOCUMENT"); + metadata.put(DocumentKnowledgeMetadataKeys.CHANNEL, STRUCTURE_ANCHOR_CHANNEL); + metadata.put(DocumentKnowledgeMetadataKeys.SCORE, 1D); + metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, chunk.getDocumentId()); + metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, descriptor == null ? "" : safeText(descriptor.getDocumentName())); + if (descriptor != null) { + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId()); + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, safeText(descriptor.getKnowledgeBaseName())); + } + metadata.put(DocumentKnowledgeMetadataKeys.TASK_ID, chunk.getTaskId()); + metadata.put(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, chunk.getParentBlockId()); + metadata.put(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_NO, anchorParent.getParentNo()); + metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_ID, chunk.getId()); + metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_NO, chunk.getChunkNo()); + metadata.put(DocumentKnowledgeMetadataKeys.SECTION_PATH, safeText(chunk.getSectionPath())); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, chunk.getStructureNodeId()); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_TYPE, chunk.getStructureNodeType()); + metadata.put(DocumentKnowledgeMetadataKeys.CANONICAL_PATH, safeText(chunk.getCanonicalPath())); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.ITEM_INDEX, chunk.getItemIndex()); + metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, normalizeBodyChunkType(chunk)); + metadata.put(DocumentKnowledgeMetadataKeys.CONTENT_WITH_WEIGHT, safeText(chunk.getContentWithWeight())); + metadata.put(DocumentKnowledgeMetadataKeys.TITLE, safeText(chunk.getTitle())); + metadata.put(DocumentKnowledgeMetadataKeys.KEYWORDS, safeText(chunk.getKeywords())); + metadata.put(DocumentKnowledgeMetadataKeys.QUESTIONS, safeText(chunk.getQuestions())); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PAGE_NO, chunk.getPageNo()); + metadata.put(DocumentKnowledgeMetadataKeys.PAGE_RANGE, safeText(chunk.getPageRange())); + metadata.put(DocumentKnowledgeMetadataKeys.BBOX_JSON, safeText(chunk.getBboxJson())); + metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_BLOCK_IDS, safeText(chunk.getSourceBlockIds())); + metadata.put(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET, safeText(chunk.getChunkText())); + metadata.put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, STRUCTURE_ANCHOR_BODY_CANDIDATE); + metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_STRUCTURE_ANCHOR, probe.sourceAnchor()); + metadata.put(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE, resolveAnchorMatchType(chunk, anchorParent, probe)); + metadata.put(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW, true); + metadata.put(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_RAW_BODY, true); + metadata.put(DocumentKnowledgeMetadataKeys.STRUCTURE_BODY_RESOLVED_FROM, resolvedFrom); + metadata.put(DocumentKnowledgeMetadataKeys.STRUCTURE_BODY_CANDIDATE_KIND, candidateKind); + + return Document.builder() + .id("structure-chunk-" + chunk.getId()) + .text(trimText(safeText(chunk.getChunkText()), maxChars)) + .metadata(metadata) + .score(1D) + .build(); + } + + private boolean isRawBodyChunk(SuperAgentDocumentChunk chunk, boolean allowListContinuation) { + if (chunk == null || chunk.getSourceType() == null + || !Objects.equals(chunk.getSourceType(), DocumentChunkSourceTypeEnum.ORIGINAL.getCode())) { + return false; + } + String chunkType = asText(chunk.getChunkType()); + if ("TEXT".equalsIgnoreCase(chunkType) || "LIST".equalsIgnoreCase(chunkType) || "TABLE".equalsIgnoreCase(chunkType)) { + return hasBodyText(chunk.getChunkText()) && !isTitleOnlyText(chunk.getChunkText()); + } + return allowListContinuation && isListContinuationChunk(chunk); + } + + private boolean isListContinuationChunk(SuperAgentDocumentChunk chunk) { + return chunk != null + && hasBodyText(chunk.getChunkText()) + && containsMultipleOrderedItems(chunk.getChunkText()) + && !"GRAPH_RAG".equalsIgnoreCase(asText(chunk.getChunkType())) + && !safeText(chunk.getChunkText()).startsWith("[GraphRAG") + && !safeText(chunk.getChunkText()).startsWith("[RAPTOR"); + } + + private boolean hasBodyText(String text) { + return safeText(text).replace("#", "").trim().length() >= 12; + } + + private boolean isTitleOnlyText(String text) { + String normalized = safeText(text).trim(); + if (normalized.isBlank()) { + return true; + } + String withoutHashes = normalized.replaceAll("^#{1,6}\\s*", "").trim(); + return normalized.length() <= 120 + && (normalized.startsWith("#") + || withoutHashes.matches("^\\d+(?:\\.\\d+){1,5}\\s+\\S[^。;;!?!?]*$")); + } + + private boolean containsMultipleOrderedItems(String text) { + String normalized = safeText(text).replace('\n', ' '); + java.util.regex.Matcher matcher = java.util.regex.Pattern + .compile("(?:^|\\s)(\\d{1,2})[、.]\\s+") + .matcher(normalized); + int count = 0; + while (matcher.find()) { + count++; + if (count >= 2) { + return true; + } + } + return false; + } + + private boolean isDirectCanonicalChild(String parentCanonicalPath, String candidateCanonicalPath) { + String parent = normalizeAnchor(parentCanonicalPath); + String candidate = normalizeAnchor(candidateCanonicalPath); + if (parent.isBlank() || !candidate.startsWith(parent + "/")) { + return false; + } + String remainder = candidate.substring(parent.length() + 1); + return !remainder.isBlank() && !remainder.contains("/"); + } + + private String normalizeBodyChunkType(SuperAgentDocumentChunk chunk) { + if (chunk == null) { + return "TEXT"; + } + if (isListContinuationChunk(chunk)) { + return "LIST"; + } + String chunkType = safeText(chunk.getChunkType()); + return StrUtil.isBlank(chunkType) ? "TEXT" : chunkType; + } + + private String resolveAnchorMatchType(SuperAgentDocumentChunk chunk, + SuperAgentDocumentParentBlock anchorParent, + StructureAnchorProbe probe) { + if (probe.structureNodeId() != null && Objects.equals(probe.structureNodeId(), chunk.getStructureNodeId())) { + return MATCH_NODE_ID; + } + String canonicalPath = normalizeAnchor(chunk.getCanonicalPath()); + String probeCanonical = normalizeAnchor(probe.canonicalPath()); + if (StrUtil.isNotBlank(probeCanonical)) { + if (canonicalPath.equals(probeCanonical)) { + return MATCH_CANONICAL_EXACT; + } + if (canonicalPath.startsWith(probeCanonical + "/")) { + return MATCH_CANONICAL_DESCENDANT; + } + } + if (anchorParent != null && !Objects.equals(anchorParent.getStructureNodeId(), chunk.getStructureNodeId())) { + return MATCH_CANONICAL_DESCENDANT; + } + return MATCH_TITLE_SAME_SECTION; + } + + private String resolveAnchorMatchType(SuperAgentDocumentParentBlock parentBlock, StructureAnchorProbe probe) { + if (probe.structureNodeId() != null && Objects.equals(probe.structureNodeId(), parentBlock.getStructureNodeId())) { + return MATCH_NODE_ID; + } + String canonicalPath = normalizeAnchor(parentBlock.getCanonicalPath()); + String probeCanonical = normalizeAnchor(probe.canonicalPath()); + if (StrUtil.isNotBlank(probeCanonical)) { + if (canonicalPath.equals(probeCanonical)) { + return MATCH_CANONICAL_EXACT; + } + if (canonicalPath.startsWith(probeCanonical + "/")) { + return MATCH_CANONICAL_DESCENDANT; + } + } + return MATCH_TITLE_SAME_SECTION; + } + + private String structureEvidenceIdentity(Document document) { + if (document == null || document.getMetadata() == null) { + return ""; + } + Long parentBlockId = asLong(document.getMetadata().get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID)); + if (parentBlockId != null) { + return "parent:" + parentBlockId; + } + return "document:" + asText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.DOCUMENT_ID)) + + ":section:" + normalizeAnchor(document.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH)); + } + private Document buildRetrievedDocument(long chunkId, String chunkText, String contentWithWeight, @@ -323,10 +856,8 @@ public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, descriptor.getDocumentId()); metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, safeText(descriptor.getDocumentName())); - metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_SCOPE_CODE, safeText(descriptor.getKnowledgeScopeCode())); - metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_SCOPE_NAME, safeText(descriptor.getKnowledgeScopeName())); - metadata.put(DocumentKnowledgeMetadataKeys.BUSINESS_CATEGORY, safeText(descriptor.getBusinessCategory())); - metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_TAGS, safeText(descriptor.getDocumentTags())); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId()); + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, safeText(descriptor.getKnowledgeBaseName())); } graphRagTypedChunkMetadataSupport.enrichMetadata(metadata, chunkType, sourceBlockIds); @@ -552,6 +1083,19 @@ public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { private double graphRagMetadataPriority(Document document) { Map metadata = document.getMetadata(); double priority = resolveScoreOrZero(document) * 0.01D; + if (Boolean.parseBoolean(asText(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY)))) { + priority -= 30D; + } + String groundingLevel = asText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL)); + if ("RELATION_STRONG_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 18D; + } + else if ("RELATION_WEAK_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 8D; + } + else if ("COMMUNITY_SOURCE_QUOTE".equalsIgnoreCase(groundingLevel)) { + priority += 6D; + } if (metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID) != null) { priority += 20D; } @@ -729,6 +1273,35 @@ public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { return value == null ? "" : String.valueOf(value); } + private List normalizeLongs(Collection values) { + if (values == null || values.isEmpty()) { + return List.of(); + } + return values.stream() + .filter(Objects::nonNull) + .distinct() + .toList(); + } + + private List normalizeTexts(Collection values) { + if (values == null || values.isEmpty()) { + return List.of(); + } + return values.stream() + .map(this::safeText) + .filter(StrUtil::isNotBlank) + .distinct() + .toList(); + } + + private String normalizeAnchor(Object value) { + return asText(value) + .trim() + .replace('\\', '/') + .replaceAll("\\s+", "") + .toLowerCase(Locale.ROOT); + } + private int resolveTopK(int topK) { return topK <= 0 ? 10 : Math.min(topK, 50); @@ -775,4 +1348,20 @@ public class DocumentKnowledgeServiceImpl implements DocumentKnowledgeService { DocumentRetrieveFilters filters ) { } + + private record StructureAnchorProbe(Long structureNodeId, + String canonicalPath, + String sectionPath, + String sourceMatchType) { + + private String sourceAnchor() { + if (structureNodeId != null) { + return String.valueOf(structureNodeId); + } + if (StrUtil.isNotBlank(canonicalPath)) { + return canonicalPath; + } + return StrUtil.blankToDefault(sectionPath, sourceMatchType); + } + } } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentManageServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentManageServiceImpl.java index 054ba8a2d188f0c097cba54dd4564da443927215..b41ca0b16d9d34a4ead68733e53b6c1f33ea4853 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentManageServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentManageServiceImpl.java @@ -18,6 +18,7 @@ import org.javaup.ai.manage.data.SuperAgentDocumentStrategyPlan; import org.javaup.ai.manage.data.SuperAgentDocumentStrategyStep; import org.javaup.ai.manage.data.SuperAgentDocumentTask; import org.javaup.ai.manage.data.SuperAgentDocumentTaskLog; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; import org.javaup.ai.manage.data.SuperAgentTopicDocumentRelation; import org.javaup.ai.manage.dto.DocumentChunkQueryDto; import org.javaup.ai.manage.dto.DocumentChunkDetailQueryDto; @@ -58,6 +59,7 @@ import org.javaup.ai.manage.service.DocumentStrategyService; import org.javaup.ai.manage.service.DocumentTaskLogService; import org.javaup.ai.manage.service.DocumentVectorGateway; import org.javaup.ai.manage.service.GraphRagBuildService; +import org.javaup.ai.manage.service.KnowledgeBaseManageService; import org.javaup.ai.manage.service.KnowledgeRouteIndexService; import org.javaup.ai.manage.service.RaptorBuildService; import org.javaup.ai.manage.service.keyword.DocumentKeywordSearchGateway; @@ -188,6 +190,8 @@ public class DocumentManageServiceImpl implements DocumentManageService { private final RaptorBuildService raptorBuildService; + private final KnowledgeBaseManageService knowledgeBaseManageService; + private final DocumentKafkaProducer kafkaProducer; private final TransactionTemplate transactionTemplate; @@ -220,6 +224,8 @@ public class DocumentManageServiceImpl implements DocumentManageService { byte[] fileBytes = getFileBytes(file); Long documentId = uidGenerator.getUid(); + Long knowledgeBaseId = parseRequiredLong(dto.getKnowledgeBaseId(), "knowledgeBaseId"); + SuperAgentKnowledgeBase knowledgeBase = knowledgeBaseManageService.requireEnabled(knowledgeBaseId); StoredObjectInfo storedObjectInfo = storageService.uploadOriginalFile( documentId, originalFileName, fileBytes, file.getContentType()); @@ -241,10 +247,8 @@ public class DocumentManageServiceImpl implements DocumentManageService { document.setCharCount(0); document.setTokenCount(0); - document.setKnowledgeScopeCode(StrUtil.trimToNull(dto.getKnowledgeScopeCode())); - document.setKnowledgeScopeName(StrUtil.trimToNull(dto.getKnowledgeScopeName())); - document.setBusinessCategory(StrUtil.trimToNull(dto.getBusinessCategory())); - document.setDocumentTags(StrUtil.trimToNull(dto.getDocumentTags())); + document.setKnowledgeBaseId(knowledgeBase.getId()); + document.setKnowledgeBaseName(knowledgeBase.getBaseName()); document.setStatus(BusinessStatus.YES.getCode()); Long taskId = uidGenerator.getUid(); @@ -1337,10 +1341,8 @@ public class DocumentManageServiceImpl implements DocumentManageService { document.getIndexStatus(), enumMsg(DocumentIndexStatusEnum.getRc(document.getIndexStatus())), document.getParseErrorMsg(), - document.getKnowledgeScopeCode(), - document.getKnowledgeScopeName(), - document.getBusinessCategory(), - document.getDocumentTags(), + document.getKnowledgeBaseId(), + document.getKnowledgeBaseName(), document.getCurrentPlanId(), document.getLastIndexTaskId(), latestTask == null ? null : latestTask.getId(), diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentProfileServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentProfileServiceImpl.java index 50bb67cfd9b4f9eb80ecdce1bddc9a78147ae8b5..3297c36ecf18184179cb42b1a321f7e959e225e4 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentProfileServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentProfileServiceImpl.java @@ -94,15 +94,11 @@ public class DocumentProfileServiceImpl implements DocumentProfileService { documentProfileMapper.updateById(profile); } - backfillDocumentMetadata(document, draft); - log.info("文档画像生成完成: documentId={}, documentType={}, graphFriendly={}, supportsItemLookup={}, scopeCode='{}', businessCategory='{}', tags='{}'", + log.info("文档画像生成完成: documentId={}, documentType={}, graphFriendly={}, supportsItemLookup={}", documentId, draft.documentType(), draft.graphFriendly(), - draft.supportsItemLookup(), - draft.knowledgeScopeCode(), - draft.businessCategory(), - draft.documentTags()); + draft.supportsItemLookup()); return profile; } @@ -164,10 +160,6 @@ public class DocumentProfileServiceImpl implements DocumentProfileService { List coreTopics = buildCoreTopics(document, sectionTitles); List exampleQuestions = buildExampleQuestions(documentType, coreTopics); String summary = buildSummary(document, sectionTitles, parsedText); - String knowledgeScopeCode = inferKnowledgeScopeCode(document, sectionTitles, parsedText); - String knowledgeScopeName = inferKnowledgeScopeName(knowledgeScopeCode); - String businessCategory = inferBusinessCategory(documentType, parsedText); - String documentTags = buildDocumentTags(document, knowledgeScopeCode, documentType, coreTopics); return new DocumentProfileDraft( summary, documentType, @@ -176,37 +168,10 @@ public class DocumentProfileServiceImpl implements DocumentProfileService { graphFriendly, supportsGraphOutline, supportsItemLookup, - true, - knowledgeScopeCode, - knowledgeScopeName, - businessCategory, - documentTags + true ); } - private void backfillDocumentMetadata(SuperAgentDocument document, DocumentProfileDraft draft) { - boolean changed = false; - if (StrUtil.isBlank(document.getKnowledgeScopeCode()) && StrUtil.isNotBlank(draft.knowledgeScopeCode())) { - document.setKnowledgeScopeCode(draft.knowledgeScopeCode()); - changed = true; - } - if (StrUtil.isBlank(document.getKnowledgeScopeName()) && StrUtil.isNotBlank(draft.knowledgeScopeName())) { - document.setKnowledgeScopeName(draft.knowledgeScopeName()); - changed = true; - } - if (StrUtil.isBlank(document.getBusinessCategory()) && StrUtil.isNotBlank(draft.businessCategory())) { - document.setBusinessCategory(draft.businessCategory()); - changed = true; - } - if (StrUtil.isBlank(document.getDocumentTags()) && StrUtil.isNotBlank(draft.documentTags())) { - document.setDocumentTags(draft.documentTags()); - changed = true; - } - if (changed) { - documentMapper.updateById(document); - } - } - private List extractSectionTitles(List structureNodes) { if (CollUtil.isEmpty(structureNodes)) { return List.of(); @@ -294,84 +259,6 @@ public class DocumentProfileServiceImpl implements DocumentProfileService { return builder.toString().trim(); } - private String inferKnowledgeScopeCode(SuperAgentDocument document, - List sectionTitles, - String parsedText) { - String combined = combinedText(document, parsedText, sectionTitles); - if (containsAny(combined, "上线观察", "值班规则", "观察时长", "运营")) { - return "operation_rule"; - } - if (containsAny(combined, "机器人", "知识召回", "意图识别", "策略设计")) { - return "robot_strategy"; - } - if (containsAny(combined, "安装", "部署", "默认密码", "访问地址")) { - return "deployment"; - } - if (containsAny(combined, "故障", "排查", "异常", "检查顺序")) { - return "troubleshooting"; - } - if (containsAny(combined, "产品简介", "核心特性", "技术规格", "产品概述")) { - return "product"; - } - return "general_document"; - } - - private String inferKnowledgeScopeName(String scopeCode) { - return switch (StrUtil.blankToDefault(scopeCode, "")) { - case "operation_rule" -> "运营规则"; - case "robot_strategy" -> "机器人策略"; - case "deployment" -> "安装部署"; - case "troubleshooting" -> "故障排查"; - case "product" -> "产品资料"; - default -> "通用文档"; - }; - } - - private String inferBusinessCategory(String documentType, String parsedText) { - if ("troubleshooting".equals(documentType)) { - return "故障排查"; - } - if ("rule".equals(documentType)) { - return "规则"; - } - if ("spec".equals(documentType)) { - return "规格说明"; - } - if ("manual".equals(documentType)) { - return containsAny(parsedText.toLowerCase(Locale.ROOT), "步骤", "操作", "部署") - ? "操作手册" - : "手册"; - } - return "介绍"; - } - - private String buildDocumentTags(SuperAgentDocument document, - String knowledgeScopeCode, - String documentType, - List coreTopics) { - LinkedHashSet tags = new LinkedHashSet<>(); - if (StrUtil.isNotBlank(document.getDocumentTags())) { - tags.addAll(List.of(document.getDocumentTags().split(","))); - } - addTag(tags, knowledgeScopeCode); - addTag(tags, documentType); - coreTopics.stream().limit(4).forEach(topic -> addTag(tags, topic)); - return tags.stream() - .map(String::trim) - .filter(StrUtil::isNotBlank) - .distinct() - .limit(8) - .collect(Collectors.joining(",")); - } - - private void addTag(Set tags, String tag) { - String normalized = StrUtil.blankToDefault(tag, "").trim(); - if (normalized.isBlank()) { - return; - } - tags.add(normalized); - } - private boolean containsAny(String text, String... values) { String normalized = StrUtil.blankToDefault(text, "").toLowerCase(Locale.ROOT); for (String value : values) { @@ -420,11 +307,7 @@ public class DocumentProfileServiceImpl implements DocumentProfileService { boolean graphFriendly, boolean supportsGraphOutline, boolean supportsItemLookup, - boolean supportsGraphAssist, - String knowledgeScopeCode, - String knowledgeScopeName, - String businessCategory, - String documentTags + boolean supportsGraphAssist ) { } } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentStrategyServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentStrategyServiceImpl.java index ac746bbf07cd5f3151e5483ce1a63078fb295c75..3fde9c1e9a5c842d315549eb98dbf728286d1097 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentStrategyServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentStrategyServiceImpl.java @@ -11,12 +11,14 @@ import org.javaup.ai.manage.data.SuperAgentDocumentBlock; import org.javaup.ai.manage.data.SuperAgentDocumentStrategyPlan; import org.javaup.ai.manage.data.SuperAgentDocumentStrategyStep; import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode; +import org.javaup.ai.manage.model.KnowledgeBaseIndexingOptions; import org.javaup.ai.manage.service.DocumentStrategyService; import org.javaup.ai.manage.service.DocumentStructureNodeService; import org.javaup.ai.manage.support.ChunkCandidate; import org.javaup.ai.manage.support.DocumentAnalysisResult; import org.javaup.ai.manage.support.DocumentStrategyPlanDraft; import org.javaup.ai.manage.support.DocumentStrategyStepDraft; +import org.javaup.ai.manage.support.KnowledgeBaseIndexingConfigResolver; import org.javaup.ai.manage.support.ParentBlockCandidate; import org.javaup.ai.prompt.PromptTemplateNames; import org.javaup.ai.prompt.PromptTemplateService; @@ -61,27 +63,24 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { private static final Pattern ENGLISH_WORD_PATTERN = Pattern.compile("[A-Za-z0-9]{2,}"); private static final Pattern CHINESE_KEYWORD_PATTERN = Pattern.compile("[\\p{IsHan}]{2,12}"); - private static final int PARENT_BLOCK_MAX_CHARS = 2200; - private static final int PARENT_BLOCK_OVERLAP_CHARS = 180; - private static final int PARENT_SEMANTIC_MAX_CHARS = 1600; - private static final int PARENT_SEMANTIC_MIN_CHARS = 480; - private final DocumentManageProperties properties; private final ObjectMapper objectMapper; private final ObjectProvider chatModelProvider; private final DocumentStructureNodeService structureNodeService; private final PromptTemplateService promptTemplateService; + private final KnowledgeBaseIndexingConfigResolver indexingConfigResolver; @Override public DocumentStrategyPlanDraft recommendStrategy(SuperAgentDocument document, DocumentAnalysisResult analysisResult) { List reasonList = new ArrayList<>(); DocumentFileTypeEnum fileType = DocumentFileTypeEnum.getRc(document.getFileType()); + KnowledgeBaseIndexingOptions indexingOptions = indexingConfigResolver.resolve(document); boolean structureRecommended = shouldUseStructure(fileType, analysisResult); - boolean recursiveRecommended = shouldUseRecursive(analysisResult); - boolean semanticRecommended = shouldUseSemantic(analysisResult); - boolean llmRecommended = shouldUseLlm(analysisResult); + boolean recursiveRecommended = shouldUseRecursive(analysisResult, indexingOptions); + boolean semanticRecommended = shouldUseSemantic(analysisResult, indexingOptions); + boolean llmRecommended = shouldUseLlm(analysisResult, indexingOptions); List parentStrategyTypes = new ArrayList<>(); Map parentReasonMap = new LinkedHashMap<>(); @@ -190,13 +189,14 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { document == null ? null : document.getId(), document == null ? null : document.getLastParseTaskId() ); - List parentSeedList = buildParentSeedList(orderedBlocks, parentSteps, structureNodes); + KnowledgeBaseIndexingOptions indexingOptions = indexingConfigResolver.resolve(document); + List parentSeedList = buildParentSeedList(orderedBlocks, parentSteps, structureNodes, indexingOptions); List parentBlockList = new ArrayList<>(); for (ChunkCandidate parentSeed : cleanupChunkList(parentSeedList)) { if (parentSeed == null || StrUtil.isBlank(parentSeed.getText())) { continue; } - List childSeedList = buildChildSeedList(parentSeed, childSteps, blockMap); + List childSeedList = buildChildSeedList(parentSeed, childSteps, blockMap, indexingOptions); List finalChildren = cleanupChunkList(childSeedList); if (finalChildren.isEmpty()) { finalChildren = List.of(cloneChunkCandidate(parentSeed, parentSeed.getText().trim())); @@ -220,25 +220,28 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { private List buildParentSeedList(List documentBlocks, List parentSteps, - List structureNodes) { + List structureNodes, + KnowledgeBaseIndexingOptions indexingOptions) { if (containsStructureStep(parentSteps)) { - List structureSeeds = buildBlockSectionParentSeeds(documentBlocks, structureNodes); + List structureSeeds = buildBlockSectionParentSeeds(documentBlocks, structureNodes, indexingOptions); List remainingSteps = stripStructureSteps(parentSteps); if (remainingSteps.isEmpty()) { return structureSeeds; } - return executePipeline(structureSeeds, remainingSteps, DocumentStrategyPipelineTypeEnum.PARENT); + return executePipeline(structureSeeds, remainingSteps, DocumentStrategyPipelineTypeEnum.PARENT, indexingOptions); } - List parentSeeds = buildBlockWindowParentSeeds(documentBlocks, PARENT_BLOCK_MAX_CHARS); + List parentSeeds = buildBlockWindowParentSeeds(documentBlocks, + resolveRecursiveMaxChars(DocumentStrategyPipelineTypeEnum.PARENT, indexingOptions)); List remainingSteps = stripRecursiveSteps(parentSteps); return remainingSteps.isEmpty() ? parentSeeds - : executePipeline(parentSeeds, remainingSteps, DocumentStrategyPipelineTypeEnum.PARENT); + : executePipeline(parentSeeds, remainingSteps, DocumentStrategyPipelineTypeEnum.PARENT, indexingOptions); } private List buildChildSeedList(ChunkCandidate parentSeed, List childSteps, - Map blockMap) { + Map blockMap, + KnowledgeBaseIndexingOptions indexingOptions) { List blockSeeds = buildBlockChildSeeds(parentSeed, blockMap); if (blockSeeds.isEmpty()) { blockSeeds = List.of(cloneChunkCandidate(parentSeed, parentSeed.getText())); @@ -246,7 +249,7 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { List remainingSteps = stripStructureSteps(childSteps); return remainingSteps.isEmpty() ? blockSeeds - : executePipeline(blockSeeds, remainingSteps, DocumentStrategyPipelineTypeEnum.CHILD); + : executePipeline(blockSeeds, remainingSteps, DocumentStrategyPipelineTypeEnum.CHILD, indexingOptions); } private boolean containsStructureStep(List steps) { @@ -288,7 +291,8 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { } private List buildBlockSectionParentSeeds(List documentBlocks, - List structureNodes) { + List structureNodes, + KnowledgeBaseIndexingOptions indexingOptions) { List seeds = new ArrayList<>(); List currentGroup = new ArrayList<>(); String currentSectionKey = ""; @@ -297,7 +301,7 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { boolean sectionChanged = !currentGroup.isEmpty() && !StrUtil.equals(currentSectionKey, sectionKey); boolean startsNewTitleSection = isTitleBlock(block) && sectionChanged; if (sectionChanged || startsNewTitleSection) { - appendParentSeedsFromBlockGroup(seeds, currentGroup, structureNodes); + appendParentSeedsFromBlockGroup(seeds, currentGroup, structureNodes, indexingOptions); currentGroup = new ArrayList<>(); } if (currentGroup.isEmpty()) { @@ -305,22 +309,26 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { } currentGroup.add(block); } - appendParentSeedsFromBlockGroup(seeds, currentGroup, structureNodes); - return seeds.isEmpty() ? buildBlockWindowParentSeeds(documentBlocks, PARENT_BLOCK_MAX_CHARS, structureNodes) : seeds; + appendParentSeedsFromBlockGroup(seeds, currentGroup, structureNodes, indexingOptions); + return seeds.isEmpty() + ? buildBlockWindowParentSeeds(documentBlocks, resolveRecursiveMaxChars(DocumentStrategyPipelineTypeEnum.PARENT, indexingOptions), structureNodes) + : seeds; } private void appendParentSeedsFromBlockGroup(List seeds, List blockGroup, - List structureNodes) { + List structureNodes, + KnowledgeBaseIndexingOptions indexingOptions) { if (blockGroup == null || blockGroup.isEmpty()) { return; } String text = joinBlockTexts(blockGroup); - if (text.length() <= PARENT_BLOCK_MAX_CHARS) { + int parentMaxChars = resolveRecursiveMaxChars(DocumentStrategyPipelineTypeEnum.PARENT, indexingOptions); + if (text.length() <= parentMaxChars) { seeds.add(toParentSeed(blockGroup, structureNodes)); return; } - seeds.addAll(buildBlockWindowParentSeeds(blockGroup, PARENT_BLOCK_MAX_CHARS, structureNodes)); + seeds.addAll(buildBlockWindowParentSeeds(blockGroup, parentMaxChars, structureNodes)); } private List buildBlockWindowParentSeeds(List documentBlocks, int maxChars) { @@ -339,7 +347,9 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { continue; } if (blockText.length() > maxChars) { - appendParentSeedsFromBlockGroup(seeds, currentBlocks, structureNodes); + if (!currentBlocks.isEmpty()) { + seeds.add(toParentSeed(currentBlocks, structureNodes)); + } currentBlocks = new ArrayList<>(); currentChars = 0; for (String splitText : recursiveSplit(blockText, maxChars, 0)) { @@ -864,7 +874,8 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { private List executePipeline(List sourceList, List orderedSteps, - DocumentStrategyPipelineTypeEnum pipelineType) { + DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { List currentChunks = cleanupChunkList(sourceList); for (SuperAgentDocumentStrategyStep step : orderedSteps) { DocumentStrategyTypeEnum strategyType = DocumentStrategyTypeEnum.getRc(step.getStrategyType()); @@ -873,9 +884,9 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { } currentChunks = switch (strategyType) { case STRUCTURE -> currentChunks; - case RECURSIVE -> applyRecursiveChunking(currentChunks, pipelineType); - case SEMANTIC -> applySemanticChunking(currentChunks, pipelineType); - case LLM -> applyLlmChunking(currentChunks, pipelineType); + case RECURSIVE -> applyRecursiveChunking(currentChunks, pipelineType, indexingOptions); + case SEMANTIC -> applySemanticChunking(currentChunks, pipelineType, indexingOptions); + case LLM -> applyLlmChunking(currentChunks, pipelineType, indexingOptions); }; currentChunks = cleanupChunkList(currentChunks); } @@ -919,37 +930,38 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { || analysisResult.getHeadingCount() >= 2); } - private boolean shouldUseRecursive(DocumentAnalysisResult analysisResult) { + private boolean shouldUseRecursive(DocumentAnalysisResult analysisResult, + KnowledgeBaseIndexingOptions indexingOptions) { - return analysisResult.getCharCount() >= properties.getChunk().getRecursiveMaxChars() - || analysisResult.getMaxParagraphLength() >= properties.getChunk().getRecursiveMaxChars(); + int recursiveMaxChars = resolveRecursiveMaxChars(DocumentStrategyPipelineTypeEnum.CHILD, indexingOptions); + return analysisResult.getCharCount() >= recursiveMaxChars + || analysisResult.getMaxParagraphLength() >= recursiveMaxChars; } - private boolean shouldUseSemantic(DocumentAnalysisResult analysisResult) { + private boolean shouldUseSemantic(DocumentAnalysisResult analysisResult, + KnowledgeBaseIndexingOptions indexingOptions) { - return analysisResult.getCharCount() >= properties.getChunk().getSemanticMinChars() + return analysisResult.getCharCount() >= resolveSemanticMinChars(DocumentStrategyPipelineTypeEnum.CHILD, indexingOptions) && analysisResult.getParagraphCount() >= 3 && analysisResult.getContentQualityLevel() >= DocumentContentQualityLevelEnum.MEDIUM.getCode(); } - private boolean shouldUseLlm(DocumentAnalysisResult analysisResult) { + private boolean shouldUseLlm(DocumentAnalysisResult analysisResult, + KnowledgeBaseIndexingOptions indexingOptions) { return Boolean.TRUE.equals(properties.getChunk().getRecommendLlmWhenLowQuality()) && Boolean.TRUE.equals(properties.getChunk().getLlmEnabled()) && chatModelProvider.getIfAvailable() != null && analysisResult.getContentQualityLevel().equals(DocumentContentQualityLevelEnum.LOW.getCode()) - && analysisResult.getCharCount() >= properties.getChunk().getSemanticMinChars(); - } - - private List applyRecursiveChunking(List sourceList) { - return applyRecursiveChunking(sourceList, DocumentStrategyPipelineTypeEnum.CHILD); + && analysisResult.getCharCount() >= resolveSemanticMinChars(DocumentStrategyPipelineTypeEnum.CHILD, indexingOptions); } private List applyRecursiveChunking(List sourceList, - DocumentStrategyPipelineTypeEnum pipelineType) { + DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { List resultList = new ArrayList<>(); - int maxChars = resolveRecursiveMaxChars(pipelineType); - int overlapChars = resolveRecursiveOverlap(maxChars, pipelineType); + int maxChars = resolveRecursiveMaxChars(pipelineType, indexingOptions); + int overlapChars = resolveRecursiveOverlap(maxChars, pipelineType, indexingOptions); for (ChunkCandidate candidate : sourceList) { List splitTextList = recursiveSplit(candidate.getText(), maxChars, overlapChars); @@ -960,14 +972,11 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { return resultList; } - private List applySemanticChunking(List sourceList) { - return applySemanticChunking(sourceList, DocumentStrategyPipelineTypeEnum.CHILD); - } - private List applySemanticChunking(List sourceList, - DocumentStrategyPipelineTypeEnum pipelineType) { + DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { List resultList = new ArrayList<>(); - int semanticMinChars = resolveSemanticMinChars(pipelineType); + int semanticMinChars = resolveSemanticMinChars(pipelineType, indexingOptions); for (ChunkCandidate candidate : sourceList) { if (StrUtil.isBlank(candidate.getText()) || candidate.getText().length() <= semanticMinChars) { @@ -976,17 +985,14 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { continue; } - resultList.addAll(semanticSplit(candidate, pipelineType)); + resultList.addAll(semanticSplit(candidate, pipelineType, indexingOptions)); } return resultList; } - private List applyLlmChunking(List sourceList) { - return applyLlmChunking(sourceList, DocumentStrategyPipelineTypeEnum.CHILD); - } - private List applyLlmChunking(List sourceList, - DocumentStrategyPipelineTypeEnum pipelineType) { + DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { ChatModel chatModel = chatModelProvider.getIfAvailable(); if (!Boolean.TRUE.equals(properties.getChunk().getLlmEnabled()) || chatModel == null) { throw new IllegalStateException("当前切块方案包含 LLM 策略,但 LLM 切块未启用或 ChatModel 不可用。"); @@ -998,7 +1004,7 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { continue; } - int llmMaxChars = resolveLlmMaxChars(pipelineType); + int llmMaxChars = resolveLlmMaxChars(pipelineType, indexingOptions); List sourceTextList = candidate.getText().length() > llmMaxChars ? recursiveSplit(candidate.getText(), llmMaxChars, 0) : List.of(candidate.getText()); @@ -1017,7 +1023,8 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { } private List semanticSplit(ChunkCandidate candidate, - DocumentStrategyPipelineTypeEnum pipelineType) { + DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { List resultList = new ArrayList<>(); List sentenceList = splitSentences(candidate.getText()); if (sentenceList.size() <= 1) { @@ -1028,8 +1035,9 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { StringBuilder currentChunk = new StringBuilder(); Set currentTokenSet = new LinkedHashSet<>(); - int semanticMinChars = resolveSemanticMinChars(pipelineType); - int semanticMaxChars = resolveSemanticMaxChars(pipelineType); + int semanticMinChars = resolveSemanticMinChars(pipelineType, indexingOptions); + int semanticMaxChars = resolveSemanticMaxChars(pipelineType, indexingOptions); + double similarityThreshold = resolveSemanticSimilarityThreshold(indexingOptions); for (String sentence : sentenceList) { @@ -1038,7 +1046,7 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { boolean exceedMaxChars = currentChunk.length() + sentence.length() > semanticMaxChars; double similarity = currentTokenSet.isEmpty() ? 1D : jaccard(currentTokenSet, sentenceTokenSet); boolean semanticBreak = currentChunk.length() >= semanticMinChars - && similarity < properties.getChunk().getSemanticSimilarityThreshold(); + && similarity < similarityThreshold; if (currentChunk.length() > 0 && (exceedMaxChars || semanticBreak)) { @@ -1177,15 +1185,13 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { return suffix.trim(); } - private int resolveRecursiveOverlap(int maxChars) { - return resolveRecursiveOverlap(maxChars, DocumentStrategyPipelineTypeEnum.CHILD); - } - - private int resolveRecursiveOverlap(int maxChars, DocumentStrategyPipelineTypeEnum pipelineType) { + private int resolveRecursiveOverlap(int maxChars, + DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { if (pipelineType == DocumentStrategyPipelineTypeEnum.PARENT) { - return Math.min(PARENT_BLOCK_OVERLAP_CHARS, Math.max(0, maxChars - 1)); + return Math.min(indexingOptions.getChunk().getParentBlockOverlapChars(), Math.max(0, maxChars - 1)); } - Integer configuredOverlap = properties.getChunk().getRecursiveOverlapChars(); + Integer configuredOverlap = indexingOptions.getChunk().getChildRecursiveOverlapChars(); if (configuredOverlap == null || configuredOverlap <= 0) { return 0; } @@ -1193,27 +1199,35 @@ public class DocumentStrategyServiceImpl implements DocumentStrategyService { return Math.min(configuredOverlap, Math.max(0, maxChars - 1)); } - private int resolveRecursiveMaxChars(DocumentStrategyPipelineTypeEnum pipelineType) { + private int resolveRecursiveMaxChars(DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { return pipelineType == DocumentStrategyPipelineTypeEnum.PARENT - ? PARENT_BLOCK_MAX_CHARS - : properties.getChunk().getRecursiveMaxChars(); + ? indexingOptions.getChunk().getParentBlockMaxChars() + : indexingOptions.getChunk().getChildRecursiveMaxChars(); } - private int resolveSemanticMaxChars(DocumentStrategyPipelineTypeEnum pipelineType) { + private int resolveSemanticMaxChars(DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { return pipelineType == DocumentStrategyPipelineTypeEnum.PARENT - ? Math.max(PARENT_SEMANTIC_MAX_CHARS, properties.getChunk().getSemanticMaxChars()) - : properties.getChunk().getSemanticMaxChars(); + ? indexingOptions.getChunk().getParentSemanticMaxChars() + : indexingOptions.getChunk().getChildSemanticMaxChars(); } - private int resolveSemanticMinChars(DocumentStrategyPipelineTypeEnum pipelineType) { + private int resolveSemanticMinChars(DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { return pipelineType == DocumentStrategyPipelineTypeEnum.PARENT - ? Math.max(PARENT_SEMANTIC_MIN_CHARS, properties.getChunk().getSemanticMinChars()) - : properties.getChunk().getSemanticMinChars(); + ? indexingOptions.getChunk().getParentSemanticMinChars() + : indexingOptions.getChunk().getChildSemanticMinChars(); + } + + private double resolveSemanticSimilarityThreshold(KnowledgeBaseIndexingOptions indexingOptions) { + return indexingOptions.getChunk().getChildSemanticSimilarityThreshold(); } - private int resolveLlmMaxChars(DocumentStrategyPipelineTypeEnum pipelineType) { + private int resolveLlmMaxChars(DocumentStrategyPipelineTypeEnum pipelineType, + KnowledgeBaseIndexingOptions indexingOptions) { return pipelineType == DocumentStrategyPipelineTypeEnum.PARENT - ? Math.max(properties.getChunk().getLlmMaxChars(), PARENT_BLOCK_MAX_CHARS) + ? Math.max(properties.getChunk().getLlmMaxChars(), indexingOptions.getChunk().getParentBlockMaxChars()) : properties.getChunk().getLlmMaxChars(); } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentStructureNodeServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentStructureNodeServiceImpl.java index 8761a46145d1f09f35db445d8277661f9495f6a1..f7d7a4caebc09faaa216f1def9d3d4a8bc2923b5 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentStructureNodeServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/DocumentStructureNodeServiceImpl.java @@ -81,7 +81,7 @@ public class DocumentStructureNodeServiceImpl implements DocumentStructureNodeSe LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(SuperAgentDocumentStructureNode::getDocumentId, documentId) .eq(SuperAgentDocumentStructureNode::getStatus, BusinessStatus.YES.getCode()) - .orderByAsc(SuperAgentDocumentStructureNode::getNodeNo); + .orderByAsc(SuperAgentDocumentStructureNode::getNodeNo, SuperAgentDocumentStructureNode::getId); if (parseTaskId != null) { wrapper.eq(SuperAgentDocumentStructureNode::getParseTaskId, parseTaskId); } @@ -97,6 +97,49 @@ public class DocumentStructureNodeServiceImpl implements DocumentStructureNodeSe return result; } + @Override + public List listChildren(Long documentId, Long parseTaskId, Long parentNodeId) { + if (documentId == null || parentNodeId == null) { + return List.of(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SuperAgentDocumentStructureNode::getDocumentId, documentId) + .eq(SuperAgentDocumentStructureNode::getParentNodeId, parentNodeId) + .eq(SuperAgentDocumentStructureNode::getStatus, BusinessStatus.YES.getCode()) + .orderByAsc(SuperAgentDocumentStructureNode::getNodeNo, SuperAgentDocumentStructureNode::getId); + if (parseTaskId != null) { + wrapper.eq(SuperAgentDocumentStructureNode::getParseTaskId, parseTaskId); + } + return structureNodeMapper.selectList(wrapper); + } + + @Override + public SuperAgentDocumentStructureNode findById(Long documentId, Long parseTaskId, Long nodeId) { + if (documentId == null || nodeId == null) { + return null; + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SuperAgentDocumentStructureNode::getDocumentId, documentId) + .eq(SuperAgentDocumentStructureNode::getId, nodeId) + .eq(SuperAgentDocumentStructureNode::getStatus, BusinessStatus.YES.getCode()); + if (parseTaskId != null) { + wrapper.eq(SuperAgentDocumentStructureNode::getParseTaskId, parseTaskId); + } + return structureNodeMapper.selectOne(wrapper); + } + + @Override + public SuperAgentDocumentStructureNode findPreviousSibling(Long documentId, Long parseTaskId, Long nodeId) { + SuperAgentDocumentStructureNode node = findById(documentId, parseTaskId, nodeId); + return node == null ? null : findById(documentId, parseTaskId, node.getPrevSiblingNodeId()); + } + + @Override + public SuperAgentDocumentStructureNode findNextSibling(Long documentId, Long parseTaskId, Long nodeId) { + SuperAgentDocumentStructureNode node = findById(documentId, parseTaskId, nodeId); + return node == null ? null : findById(documentId, parseTaskId, node.getNextSiblingNodeId()); + } + @Override public void deleteByDocumentId(Long documentId) { if (documentId == null) { diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/ElasticsearchKnowledgeRouteIndexService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/ElasticsearchKnowledgeRouteIndexService.java index 77387b87149437503400ac3d208efcd8f17494f2..0b7393fb0c73be15f9e2412bc968aeae08d04f0d 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/ElasticsearchKnowledgeRouteIndexService.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/ElasticsearchKnowledgeRouteIndexService.java @@ -2,6 +2,7 @@ package org.javaup.ai.manage.service.impl; import cn.hutool.core.util.StrUtil; import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.FieldValue; import co.elastic.clients.elasticsearch._types.Refresh; import co.elastic.clients.elasticsearch._types.query_dsl.TextQueryType; import co.elastic.clients.elasticsearch.core.BulkRequest; @@ -34,11 +35,13 @@ import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @@ -87,18 +90,31 @@ public class ElasticsearchKnowledgeRouteIndexService implements KnowledgeRouteIn } @Override - public List search(String routingText, String entityType, int size) { + public List search(String routingText, String entityType, int size, Collection knowledgeBaseIds) { if (StrUtil.isBlank(routingText) || StrUtil.isBlank(entityType)) { return List.of(); } refreshIfNeeded(); List entityTerms = extractEntityTerms(routingText); + List selectedKnowledgeBaseIds = knowledgeBaseIds == null + ? List.of() + : knowledgeBaseIds.stream() + .filter(Objects::nonNull) + .distinct() + .map(FieldValue::of) + .toList(); try { SearchResponse response = elasticsearchClient.search(search -> search .index(properties.getElasticsearch().getRouteIndexName()) .size(Math.max(1, Math.min(size, 10))) .query(query -> query.bool(bool -> { bool.filter(filter -> filter.term(term -> term.field("entityType").value(entityType))); + if (!selectedKnowledgeBaseIds.isEmpty()) { + bool.filter(filter -> filter.terms(terms -> terms + .field("knowledgeBaseId") + .terms(value -> value.value(selectedKnowledgeBaseIds)) + )); + } bool.should(should -> should.matchPhrase(matchPhrase -> matchPhrase .field("displayName") .query(routingText) @@ -128,11 +144,12 @@ public class ElasticsearchKnowledgeRouteIndexService implements KnowledgeRouteIn } hits.add(new RouteLexicalHit( source.getRouteId(), - source.getEntityCode(), + source.getEntityId(), source.getEntityType(), source.getDocumentId(), - source.getScopeCode(), - source.getTopicCode(), + source.getKnowledgeBaseId(), + source.getScopeId(), + source.getTopicId(), source.getDocumentName(), hit.score() == null ? 0D : hit.score() )); @@ -221,24 +238,28 @@ public class ElasticsearchKnowledgeRouteIndexService implements KnowledgeRouteIn .stream() .collect(Collectors.toMap(SuperAgentDocumentProfile::getDocumentId, item -> item, (left, right) -> right)); Map> topicByScope = topics.stream() - .collect(Collectors.groupingBy(SuperAgentKnowledgeTopicNode::getScopeCode)); + .collect(Collectors.groupingBy(topic -> routeKey(topic.getKnowledgeBaseId(), topic.getScopeId()))); + Map topicById = topics.stream() + .filter(topic -> topic.getId() != null) + .collect(Collectors.toMap(SuperAgentKnowledgeTopicNode::getId, topic -> topic, (left, right) -> left, LinkedHashMap::new)); Map> relationByTopic = topicDocumentRelationMapper.selectList( new LambdaQueryWrapper() .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode())) .stream() - .collect(Collectors.groupingBy(SuperAgentTopicDocumentRelation::getTopicCode)); + .collect(Collectors.groupingBy(relation -> routeKey(relation.getKnowledgeBaseId(), relation.getTopicId()))); for (SuperAgentKnowledgeScopeNode scope : scopes) { List scopeTags = new ArrayList<>(); - topicByScope.getOrDefault(scope.getScopeCode(), List.of()).forEach(topic -> { + topicByScope.getOrDefault(routeKey(scope.getKnowledgeBaseId(), scope.getId()), List.of()).forEach(topic -> { addUnique(scopeTags, topic.getTopicName()); parseCommaText(topic.getAliases()).forEach(item -> addUnique(scopeTags, item)); }); records.add(KnowledgeRouteIndexRecord.builder() - .routeId("scope:" + scope.getScopeCode()) + .routeId("scope:" + safeIdPart(scope.getKnowledgeBaseId()) + ":" + safeIdPart(scope.getId())) .entityType("scope") - .entityCode(scope.getScopeCode()) - .scopeCode(scope.getScopeCode()) + .entityId(scope.getId()) + .knowledgeBaseId(scope.getKnowledgeBaseId()) + .scopeId(scope.getId()) .scopeName(scope.getScopeName()) .displayName(safeText(scope.getScopeName())) .descriptionText(safeText(scope.getDescription())) @@ -246,7 +267,7 @@ public class ElasticsearchKnowledgeRouteIndexService implements KnowledgeRouteIn .examplesText(safeText(scope.getExamples())) .summaryText(safeText(scope.getDescription())) .routeText(join(scope.getScopeName(), scope.getDescription(), scope.getAliases(), scope.getExamples())) - .entityTerms(extractEntityTerms(join(scope.getScopeCode(), scope.getScopeName(), scope.getAliases()))) + .entityTerms(extractEntityTerms(join(scope.getScopeName(), scope.getAliases()))) .tags(scopeTags) .build()); } @@ -256,11 +277,12 @@ public class ElasticsearchKnowledgeRouteIndexService implements KnowledgeRouteIn parseJsonArray(topic.getExamples()).forEach(item -> addUnique(tags, item)); parseCommaText(topic.getAliases()).forEach(item -> addUnique(tags, item)); records.add(KnowledgeRouteIndexRecord.builder() - .routeId("topic:" + topic.getTopicCode()) + .routeId("topic:" + safeIdPart(topic.getKnowledgeBaseId()) + ":" + safeIdPart(topic.getId())) .entityType("topic") - .entityCode(topic.getTopicCode()) - .scopeCode(topic.getScopeCode()) - .topicCode(topic.getTopicCode()) + .entityId(topic.getId()) + .knowledgeBaseId(topic.getKnowledgeBaseId()) + .scopeId(topic.getScopeId()) + .topicId(topic.getId()) .topicName(topic.getTopicName()) .displayName(safeText(topic.getTopicName())) .descriptionText(safeText(topic.getDescription())) @@ -268,41 +290,30 @@ public class ElasticsearchKnowledgeRouteIndexService implements KnowledgeRouteIn .examplesText(safeText(topic.getExamples())) .summaryText(join(topic.getAnswerShape(), topic.getExecutionPreference())) .routeText(join( - topic.getTopicCode(), topic.getTopicName(), topic.getDescription(), topic.getAliases(), topic.getExamples(), topic.getAnswerShape(), topic.getExecutionPreference())) - .entityTerms(extractEntityTerms(join(topic.getTopicCode(), topic.getTopicName(), topic.getAliases()))) + .entityTerms(extractEntityTerms(join(topic.getTopicName(), topic.getAliases()))) .tags(tags) .build()); } - Map topicDocumentMap = new LinkedHashMap<>(); - for (SuperAgentKnowledgeTopicNode topic : topics) { - for (SuperAgentTopicDocumentRelation relation : relationByTopic.getOrDefault(topic.getTopicCode(), List.of())) { - topicDocumentMap.put(relation.getDocumentId(), topic); - } - } - for (SuperAgentDocument document : documents) { SuperAgentDocumentProfile profile = profileMap.get(document.getId()); List tags = new ArrayList<>(); - parseCommaText(document.getDocumentTags()).forEach(item -> addUnique(tags, item)); if (profile != null) { parseJsonArray(profile.getCoreTopics()).forEach(item -> addUnique(tags, item)); parseJsonArray(profile.getExampleQuestions()).forEach(item -> addUnique(tags, item)); } - relationByTopic.forEach((topicCode, relations) -> relations.stream() - .filter(relation -> document.getId().equals(relation.getDocumentId())) + relationByTopic.forEach((topicKey, relations) -> relations.stream() + .filter(relation -> document.getId().equals(relation.getDocumentId()) + && Objects.equals(document.getKnowledgeBaseId(), relation.getKnowledgeBaseId())) .findFirst() .ifPresent(relation -> { - SuperAgentKnowledgeTopicNode topic = topics.stream() - .filter(item -> topicCode.equals(item.getTopicCode())) - .findFirst() - .orElse(null); + SuperAgentKnowledgeTopicNode topic = topicById.get(relation.getTopicId()); if (topic != null) { addUnique(tags, topic.getTopicName()); parseCommaText(topic.getAliases()).forEach(item -> addUnique(tags, item)); @@ -311,12 +322,10 @@ public class ElasticsearchKnowledgeRouteIndexService implements KnowledgeRouteIn records.add(KnowledgeRouteIndexRecord.builder() .routeId("document:" + document.getId()) .entityType("document") - .entityCode(String.valueOf(document.getId())) + .entityId(document.getId()) .documentId(document.getId()) - .scopeCode(safeText(document.getKnowledgeScopeCode())) - .scopeName(safeText(document.getKnowledgeScopeName())) + .knowledgeBaseId(document.getKnowledgeBaseId()) .documentName(safeText(document.getDocumentName())) - .businessCategory(safeText(document.getBusinessCategory())) .displayName(safeText(document.getDocumentName())) .descriptionText(profile == null ? "" : safeText(profile.getDocumentType())) .aliasesText("") @@ -324,22 +333,26 @@ public class ElasticsearchKnowledgeRouteIndexService implements KnowledgeRouteIn .summaryText(profile == null ? "" : safeText(profile.getDocumentSummary())) .routeText(join( document.getDocumentName(), - document.getKnowledgeScopeCode(), - document.getKnowledgeScopeName(), - document.getBusinessCategory(), - document.getDocumentTags(), profile == null ? "" : profile.getDocumentSummary(), profile == null ? "" : profile.getCoreTopics(), profile == null ? "" : profile.getExampleQuestions(), profile == null ? "" : profile.getDocumentType() )) - .entityTerms(extractEntityTerms(join(document.getDocumentName(), document.getDocumentTags(), document.getKnowledgeScopeName()))) + .entityTerms(extractEntityTerms(document.getDocumentName())) .tags(tags) .build()); } return records; } + private String routeKey(Long knowledgeBaseId, Long id) { + return safeIdPart(knowledgeBaseId) + ":" + safeIdPart(id); + } + + private String safeIdPart(Long value) { + return value == null ? "none" : String.valueOf(value); + } + private List extractEntityTerms(String text) { if (StrUtil.isBlank(text)) { return List.of(); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagBuildServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagBuildServiceImpl.java index 676ec8cfe951caaac74c8f2ce2c483e779010430..04de0d01889d873bd720ae1f14db68acab1550cd 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagBuildServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagBuildServiceImpl.java @@ -17,6 +17,7 @@ import org.javaup.ai.manage.mapper.SuperAgentKgCommunityMapper; import org.javaup.ai.manage.mapper.SuperAgentKgEntityMapper; import org.javaup.ai.manage.mapper.SuperAgentKgEvidenceMapper; import org.javaup.ai.manage.mapper.SuperAgentKgRelationMapper; +import org.javaup.ai.manage.model.KnowledgeBaseIndexingOptions; import org.javaup.ai.manage.model.graph.GraphRagBuildResult; import org.javaup.ai.manage.model.graph.GraphRagCommunityReportAdvice; import org.javaup.ai.manage.model.graph.GraphRagCommunityReportContext; @@ -30,6 +31,7 @@ import org.javaup.ai.manage.service.GraphRagCommunityReportAdvisor; import org.javaup.ai.manage.service.GraphRagCrossDocumentIndexService; import org.javaup.ai.manage.service.GraphRagEntityResolutionAdvisor; import org.javaup.ai.manage.service.GraphRagExtractionAdvisor; +import org.javaup.ai.manage.support.KnowledgeBaseIndexingConfigResolver; import org.javaup.ai.ragtools.client.RagToolsClient; import org.javaup.ai.ragtools.model.RagToolsGraphExtractRequest; import org.javaup.ai.ragtools.model.RagToolsGraphExtractResponse; @@ -118,6 +120,8 @@ public class GraphRagBuildServiceImpl implements GraphRagBuildService { private final GraphRagCrossDocumentIndexService crossDocumentIndexService; + private final KnowledgeBaseIndexingConfigResolver indexingConfigResolver; + @Autowired public GraphRagBuildServiceImpl(SuperAgentKgEntityMapper entityMapper, SuperAgentKgRelationMapper relationMapper, @@ -133,7 +137,8 @@ public class GraphRagBuildServiceImpl implements GraphRagBuildService { ObjectProvider extractionAdvisorProvider, ObjectProvider communityReportAdvisorProvider, ObjectProvider entityResolutionAdvisorProvider, - ObjectProvider crossDocumentIndexServiceProvider) { + ObjectProvider crossDocumentIndexServiceProvider, + ObjectProvider indexingConfigResolverProvider) { this( entityMapper, relationMapper, @@ -149,7 +154,8 @@ public class GraphRagBuildServiceImpl implements GraphRagBuildService { extractionAdvisorProvider == null ? null : (GraphRagExtractionAdvisor) extractionAdvisorProvider.getIfAvailable(), communityReportAdvisorProvider == null ? null : (GraphRagCommunityReportAdvisor) communityReportAdvisorProvider.getIfAvailable(), entityResolutionAdvisorProvider == null ? null : (GraphRagEntityResolutionAdvisor) entityResolutionAdvisorProvider.getIfAvailable(), - crossDocumentIndexServiceProvider == null ? null : crossDocumentIndexServiceProvider.getIfAvailable() + crossDocumentIndexServiceProvider == null ? null : crossDocumentIndexServiceProvider.getIfAvailable(), + indexingConfigResolverProvider == null ? null : indexingConfigResolverProvider.getIfAvailable() ); } @@ -179,6 +185,7 @@ public class GraphRagBuildServiceImpl implements GraphRagBuildService { (GraphRagExtractionAdvisor) null, (GraphRagCommunityReportAdvisor) null, (GraphRagEntityResolutionAdvisor) null, + null, null ); } @@ -198,6 +205,42 @@ public class GraphRagBuildServiceImpl implements GraphRagBuildService { GraphRagCommunityReportAdvisor communityReportAdvisor, GraphRagEntityResolutionAdvisor entityResolutionAdvisor, GraphRagCrossDocumentIndexService crossDocumentIndexService) { + this( + entityMapper, + relationMapper, + evidenceMapper, + communityMapper, + ragToolsClient, + objectMapper, + uidGenerator, + buildProperties, + redisLeaseManager, + checkpointService, + transactionTemplate, + extractionAdvisor, + communityReportAdvisor, + entityResolutionAdvisor, + crossDocumentIndexService, + null + ); + } + + GraphRagBuildServiceImpl(SuperAgentKgEntityMapper entityMapper, + SuperAgentKgRelationMapper relationMapper, + SuperAgentKgEvidenceMapper evidenceMapper, + SuperAgentKgCommunityMapper communityMapper, + RagToolsClient ragToolsClient, + ObjectMapper objectMapper, + UidGenerator uidGenerator, + GraphRagBuildProperties buildProperties, + RedisLeaseManager redisLeaseManager, + GraphRagBuildCheckpointService checkpointService, + TransactionTemplate transactionTemplate, + GraphRagExtractionAdvisor extractionAdvisor, + GraphRagCommunityReportAdvisor communityReportAdvisor, + GraphRagEntityResolutionAdvisor entityResolutionAdvisor, + GraphRagCrossDocumentIndexService crossDocumentIndexService, + KnowledgeBaseIndexingConfigResolver indexingConfigResolver) { this.entityMapper = entityMapper; this.relationMapper = relationMapper; this.evidenceMapper = evidenceMapper; @@ -213,6 +256,7 @@ public class GraphRagBuildServiceImpl implements GraphRagBuildService { this.communityReportAdvisor = communityReportAdvisor; this.entityResolutionAdvisor = entityResolutionAdvisor; this.crossDocumentIndexService = crossDocumentIndexService; + this.indexingConfigResolver = indexingConfigResolver; } @Override @@ -220,6 +264,17 @@ public class GraphRagBuildServiceImpl implements GraphRagBuildService { if (documentId == null || taskId == null) { return GraphRagBuildResult.builder().build(); } + KnowledgeBaseIndexingOptions.GraphRagBuildOptions graphRagOptions = graphRagBuildOptions(documentId); + if (!Boolean.TRUE.equals(graphRagOptions.getGraphRagBuildEnabled())) { + checkpointService.markRunning(documentId, taskId, "DISABLED_BY_KB_CONFIG", 0, maxAttempts(), metadata( + "graphRagBuildEnabled", false + )); + GraphRagBuildResult result = replaceGraph(documentId, taskId, new RagToolsGraphExtractResponse()); + refreshCrossDocumentIndex(documentId, taskId); + checkpointService.markSuccess(documentId, taskId, result, 0, maxAttempts()); + log.info("知识库配置已关闭 GraphRAG 构建,跳过实体关系抽取: documentId={}, taskId={}", documentId, taskId); + return result; + } if (CollUtil.isEmpty(chunks)) { GraphRagBuildResult result = replaceGraph(documentId, taskId, new RagToolsGraphExtractResponse()); refreshCrossDocumentIndex(documentId, taskId); @@ -1197,6 +1252,13 @@ public class GraphRagBuildServiceImpl implements GraphRagBuildService { return Math.max(1, buildProperties.getMaxAttempts()); } + private KnowledgeBaseIndexingOptions.GraphRagBuildOptions graphRagBuildOptions(Long documentId) { + if (indexingConfigResolver == null) { + return KnowledgeBaseIndexingOptions.defaults().getGraphRag(); + } + return indexingConfigResolver.resolveByDocumentId(documentId).getGraphRag(); + } + private Duration leaseTtl() { return Duration.ofSeconds(Math.max(1, buildProperties.getLeaseTtlSeconds())); } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagCrossDocumentIndexServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagCrossDocumentIndexServiceImpl.java index 3ef81d93ac32e69738bab8fe9e899e8bbcfe135c..2827eef7d0df7d562962e532d7d30ab73ba9584b 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagCrossDocumentIndexServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagCrossDocumentIndexServiceImpl.java @@ -17,6 +17,8 @@ import org.javaup.ai.manage.data.SuperAgentKgEvidence; import org.javaup.ai.manage.data.SuperAgentKgRelation; import org.javaup.ai.manage.data.SuperAgentKgRelationGroup; import org.javaup.ai.manage.data.SuperAgentKgRelationGroupMember; +import org.javaup.ai.manage.data.SuperAgentKnowledgeTopicNode; +import org.javaup.ai.manage.data.SuperAgentTopicDocumentRelation; import org.javaup.ai.manage.mapper.SuperAgentDocumentMapper; import org.javaup.ai.manage.mapper.SuperAgentKgCanonicalEntityGroupMapper; import org.javaup.ai.manage.mapper.SuperAgentKgCanonicalEntityMemberMapper; @@ -27,10 +29,13 @@ import org.javaup.ai.manage.mapper.SuperAgentKgEvidenceMapper; import org.javaup.ai.manage.mapper.SuperAgentKgRelationGroupMapper; import org.javaup.ai.manage.mapper.SuperAgentKgRelationGroupMemberMapper; import org.javaup.ai.manage.mapper.SuperAgentKgRelationMapper; +import org.javaup.ai.manage.mapper.SuperAgentKnowledgeTopicNodeMapper; +import org.javaup.ai.manage.mapper.SuperAgentTopicDocumentRelationMapper; import org.javaup.ai.manage.model.graph.GraphRagCrossDocumentIndexBuildResult; import org.javaup.ai.manage.service.GraphRagCrossDocumentIndexService; import org.javaup.ai.manage.support.GraphRagCrossDocumentIndex; import org.javaup.ai.manage.support.GraphRagCrossDocumentIndexSupport; +import org.javaup.ai.manage.support.RaptorScopeSupport; import org.javaup.enums.BusinessStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -54,7 +59,6 @@ import java.util.stream.Collectors; @Slf4j public class GraphRagCrossDocumentIndexServiceImpl implements GraphRagCrossDocumentIndexService { - private static final String SCOPE_PREFIX_KNOWLEDGE = "knowledge:"; private static final String DERIVED_INDEX_SOURCE_TYPE = "java.cross_document_index.v1"; private final SuperAgentDocumentMapper documentMapper; @@ -67,6 +71,8 @@ public class GraphRagCrossDocumentIndexServiceImpl implements GraphRagCrossDocum private final SuperAgentKgRelationGroupMemberMapper relationGroupMemberMapper; private final SuperAgentKgCrossDocumentCommunityMapper communityMapper; private final SuperAgentKgCrossDocumentCommunityMemberMapper communityMemberMapper; + private final SuperAgentKnowledgeTopicNodeMapper topicNodeMapper; + private final SuperAgentTopicDocumentRelationMapper topicDocumentRelationMapper; private final GraphRagCrossDocumentIndexSupport indexSupport; private final UidGenerator uidGenerator; private final ObjectMapper objectMapper; @@ -81,6 +87,8 @@ public class GraphRagCrossDocumentIndexServiceImpl implements GraphRagCrossDocum SuperAgentKgRelationGroupMemberMapper relationGroupMemberMapper, SuperAgentKgCrossDocumentCommunityMapper communityMapper, SuperAgentKgCrossDocumentCommunityMemberMapper communityMemberMapper, + SuperAgentKnowledgeTopicNodeMapper topicNodeMapper, + SuperAgentTopicDocumentRelationMapper topicDocumentRelationMapper, GraphRagCrossDocumentIndexSupport indexSupport, UidGenerator uidGenerator, ObjectMapper objectMapper) { @@ -94,6 +102,8 @@ public class GraphRagCrossDocumentIndexServiceImpl implements GraphRagCrossDocum this.relationGroupMemberMapper = relationGroupMemberMapper; this.communityMapper = communityMapper; this.communityMemberMapper = communityMemberMapper; + this.topicNodeMapper = topicNodeMapper; + this.topicDocumentRelationMapper = topicDocumentRelationMapper; this.indexSupport = indexSupport; this.uidGenerator = uidGenerator; this.objectMapper = objectMapper; @@ -146,9 +156,6 @@ public class GraphRagCrossDocumentIndexServiceImpl implements GraphRagCrossDocum Set taskIdSet = new LinkedHashSet<>(taskIds); members = members.stream().filter(member -> taskIdSet.contains(member.getTaskId())).toList(); } - if (members.isEmpty() && !GLOBAL_SCOPE_KEY.equals(scopeKey)) { - return loadIndexByScope(GLOBAL_SCOPE_KEY, documentIds, taskIds); - } if (members.isEmpty()) { return GraphRagCrossDocumentIndex.empty(); } @@ -284,13 +291,35 @@ public class GraphRagCrossDocumentIndexServiceImpl implements GraphRagCrossDocum Map scopeByDocumentId = new LinkedHashMap<>(); for (SuperAgentDocument document : documentMap.values()) { - String scopeCode = StrUtil.blankToDefault(document.getKnowledgeScopeCode(), "").trim(); - if (StrUtil.isNotBlank(scopeCode)) { - scopeByDocumentId.put(document.getId(), SCOPE_PREFIX_KNOWLEDGE + scopeCode); + if (document.getKnowledgeBaseId() != null) { + scopeByDocumentId.put(document.getId(), RaptorScopeSupport.knowledgeBaseScopeKey(document.getKnowledgeBaseId())); } } + appendGroupedScopes(scopes, entities, relations, evidences, scopeByDocumentId); + + Map> relationScopeKeysByDocumentId = scopeByDocumentId(documentMap.keySet()); + LinkedHashSet relationScopeKeys = relationScopeKeysByDocumentId.values().stream() + .flatMap(List::stream) + .collect(Collectors.toCollection(LinkedHashSet::new)); + for (String scopeKey : relationScopeKeys) { + Map scopedDocumentMap = new LinkedHashMap<>(); + relationScopeKeysByDocumentId.forEach((documentId, scopeKeys) -> { + if (scopeKeys.contains(scopeKey)) { + scopedDocumentMap.put(documentId, scopeKey); + } + }); + appendGroupedScopes(scopes, entities, relations, evidences, scopedDocumentMap); + } + return scopes; + } + + private void appendGroupedScopes(LinkedHashMap scopes, + List entities, + List relations, + List evidences, + Map scopeByDocumentId) { if (scopeByDocumentId.isEmpty()) { - return scopes; + return; } Map> entitiesByScope = entities.stream() .filter(entity -> scopeByDocumentId.containsKey(entity.getDocumentId())) @@ -308,22 +337,59 @@ public class GraphRagCrossDocumentIndexServiceImpl implements GraphRagCrossDocum .toList(); scopes.put(entry.getKey(), ScopeDataset.of(entry.getValue(), scopedRelations, scopedEvidences)); } - return scopes; } private String resolveLoadScopeKey(List documentIds) { Map documents = listDocuments(new LinkedHashSet<>(documentIds)); - LinkedHashSet scopeCodes = documents.values().stream() - .map(SuperAgentDocument::getKnowledgeScopeCode) - .filter(StrUtil::isNotBlank) - .map(String::trim) + LinkedHashSet knowledgeBaseIds = documents.values().stream() + .map(SuperAgentDocument::getKnowledgeBaseId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + Map> scopeKeysByDocumentId = scopeByDocumentId(new LinkedHashSet<>(documentIds)); + LinkedHashSet sharedScopeKeys = scopeKeysByDocumentId.values().stream() + .flatMap(List::stream) .collect(Collectors.toCollection(LinkedHashSet::new)); - if (scopeCodes.size() == 1) { - return SCOPE_PREFIX_KNOWLEDGE + scopeCodes.iterator().next(); + if (knowledgeBaseIds.size() == 1 && sharedScopeKeys.size() == 1) { + return sharedScopeKeys.iterator().next(); + } + if (knowledgeBaseIds.size() == 1) { + return RaptorScopeSupport.knowledgeBaseScopeKey(knowledgeBaseIds.iterator().next()); } return GLOBAL_SCOPE_KEY; } + private Map> scopeByDocumentId(Collection documentIds) { + if (CollUtil.isEmpty(documentIds)) { + return Map.of(); + } + List relations = topicDocumentRelationMapper.selectList(new LambdaQueryWrapper() + .in(SuperAgentTopicDocumentRelation::getDocumentId, documentIds) + .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode())); + if (relations.isEmpty()) { + return Map.of(); + } + Map topicById = topicNodeMapper.selectList(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode())) + .stream() + .filter(topic -> topic.getId() != null) + .collect(Collectors.toMap( + SuperAgentKnowledgeTopicNode::getId, + topic -> topic, + (left, right) -> left, + LinkedHashMap::new)); + Map> grouped = new LinkedHashMap<>(); + for (SuperAgentTopicDocumentRelation relation : relations) { + SuperAgentKnowledgeTopicNode topic = topicById.get(relation.getTopicId()); + if (topic == null || topic.getKnowledgeBaseId() == null || topic.getScopeId() == null) { + continue; + } + grouped.computeIfAbsent(relation.getDocumentId(), ignored -> new LinkedHashSet<>()) + .add(RaptorScopeSupport.knowledgeScopeKey(topic.getKnowledgeBaseId(), topic.getScopeId())); + } + return grouped.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> List.copyOf(entry.getValue()), (left, right) -> left, LinkedHashMap::new)); + } + private List listEntities(List documentIds, List taskIds) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(SuperAgentKgEntity::getStatus, BusinessStatus.YES.getCode()); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagSearchServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagSearchServiceImpl.java index c42671acf4573ea351d9479cb2cc7adb7970f8eb..5c98dabcec27894101a52698a44b6b2ca1507ea1 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagSearchServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/GraphRagSearchServiceImpl.java @@ -57,16 +57,6 @@ public class GraphRagSearchServiceImpl implements GraphRagSearchService { private static final int CATALOG_COMMUNITY_LIMIT = 40; private static final String JAVA_QUERY_PROFILE_SOURCE = "java.graph_query_profile.v2"; private static final String ADVISOR_QUERY_PROFILE_SOURCE = "llm.controlled.query_plan.v1"; - private static final Set ANSWER_ACTION_RELATION_TYPES = Set.of( - "APPROVES", - "RESPONSIBLE_FOR", - "EXECUTES", - "REVOKES", - "OWNS", - "MANAGES", - "OPERATES", - "MAINTAINS" - ); private static final Set WEAK_SEMANTIC_RELATION_TYPES = Set.of( "RECORDS", "ASSOCIATED_WITH", @@ -310,15 +300,38 @@ public class GraphRagSearchServiceImpl implements GraphRagSearchService { if (result == null) { return 0; } + boolean hasSourceQuote = result.getEvidenceId() != null && StrUtil.isNotBlank(result.getQuoteText()); if (result.getRelationId() != null) { + if (hasSourceQuote) { + return 5; + } + if (result.getEvidenceId() != null) { + return 4; + } return 3; } if (result.getEntityId() != null) { - return 2; + return hasSourceQuote ? 2 : 1; + } + if (isCommunityResult(result)) { + if (hasSourceQuote && StrUtil.isNotBlank(result.getRelationGroupKey())) { + return 3; + } + return hasSourceQuote ? 2 : 0; } return 1; } + private boolean isCommunityResult(GraphRagSearchResult result) { + if (result == null) { + return false; + } + return result.getCommunityId() != null + || StrUtil.isNotBlank(result.getCrossDocumentCommunityKey()) + || StrUtil.isNotBlank(result.getCommunityTitle()) + || StrUtil.isNotBlank(result.getCommunitySummary()); + } + private GraphRagCrossDocumentIndex loadCrossDocumentIndex(List documentIds, List taskIds, List entities, @@ -909,7 +922,8 @@ public class GraphRagSearchServiceImpl implements GraphRagSearchService { } String relationType = normalizedRelationType(relation); double penalty = 0.18D + Math.max(0, hopCount - 2) * 0.08D; - if (hasAnswerTypeEndpointMatch(source, target, queryProfile) && ANSWER_ACTION_RELATION_TYPES.contains(relationType)) { + if (hasAnswerTypeEndpointMatch(source, target, queryProfile) + && relationMatchesQueryProfile(relation, relationType, queryProfile)) { penalty -= 0.14D; } if (WEAK_SEMANTIC_RELATION_TYPES.contains(relationType) @@ -928,21 +942,13 @@ public class GraphRagSearchServiceImpl implements GraphRagSearchService { return 0D; } String relationType = normalizedRelationType(relation); - if (ANSWER_ACTION_RELATION_TYPES.contains(relationType)) { - double boost = hopCount > 1 ? 0.46D : 0.36D; - if (queryProfile.relationTypes().contains(relationType) - || queryProfile.relationIds().contains(relation.getId())) { - boost += 0.12D; - } - return boost; - } - if (WEAK_SEMANTIC_RELATION_TYPES.contains(relationType)) { - return queryProfile.relationTypes().contains(relationType) ? 0.12D : 0.03D; + boolean plannedRelation = relationMatchesQueryProfile(relation, relationType, queryProfile); + if (WEAK_SEMANTIC_RELATION_TYPES.contains(relationType) && !plannedRelation) { + return 0.03D; } double boost = hopCount > 1 ? 0.24D : 0.18D; - if (queryProfile.relationTypes().contains(relationType) - || queryProfile.relationIds().contains(relation.getId())) { - boost += 0.08D; + if (plannedRelation) { + boost += 0.16D; } return boost; } @@ -959,6 +965,16 @@ public class GraphRagSearchServiceImpl implements GraphRagSearchService { return 0.30D; } + private boolean relationMatchesQueryProfile(SuperAgentKgRelation relation, + String relationType, + QueryProfile queryProfile) { + if (relation == null || queryProfile == null) { + return false; + } + return queryProfile.relationTypes().contains(relationType) + || queryProfile.relationIds().contains(relation.getId()); + } + private boolean hasAnswerTypeEndpointMatch(SuperAgentKgEntity source, SuperAgentKgEntity target, QueryProfile queryProfile) { @@ -1018,7 +1034,7 @@ public class GraphRagSearchServiceImpl implements GraphRagSearchService { boost += 0.20D; } if (relationGroupEndpointMatchesAnswerType(group, queryProfile)) { - boost += ANSWER_ACTION_RELATION_TYPES.contains(relationType) ? 0.18D : 0.10D; + boost += 0.10D; } if (queryProfile.communityQuestion()) { boost += Math.min(0.12D, group.documentCount() * 0.03D + group.evidenceCount() * 0.01D); @@ -1970,7 +1986,7 @@ public class GraphRagSearchServiceImpl implements GraphRagSearchService { reasons.add("queryRelationId"); } if (relationGroupEndpointMatchesAnswerType(group, queryProfile)) { - score += ANSWER_ACTION_RELATION_TYPES.contains(relationType) ? 0.16D : 0.10D; + score += 0.10D; reasons.add("answerTypeEndpoint"); } double groupQueryBoost = relationGroupQueryBoost(group, queryProfile); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeBaseManageServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeBaseManageServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..fc195c03fe7599671ee67cfe8e9adbf0dc46ef2f --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeBaseManageServiceImpl.java @@ -0,0 +1,296 @@ +package org.javaup.ai.manage.service.impl; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baidu.fsg.uid.UidGenerator; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.javaup.ai.manage.data.SuperAgentDocument; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; +import org.javaup.ai.manage.dto.KnowledgeBaseConfigUpdateDto; +import org.javaup.ai.manage.dto.KnowledgeBaseDeleteDto; +import org.javaup.ai.manage.dto.KnowledgeBaseDetailDto; +import org.javaup.ai.manage.dto.KnowledgeBaseSaveDto; +import org.javaup.ai.manage.mapper.SuperAgentDocumentMapper; +import org.javaup.ai.manage.mapper.SuperAgentKnowledgeBaseMapper; +import org.javaup.ai.manage.service.KnowledgeBaseManageService; +import org.javaup.ai.manage.vo.KnowledgeBaseItemVo; +import org.javaup.ai.manage.vo.KnowledgeBaseOptionVo; +import org.javaup.enums.BaseCode; +import org.javaup.enums.BusinessStatus; +import org.javaup.enums.DocumentIndexStatusEnum; +import org.javaup.exception.SuperAgentFrameException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class KnowledgeBaseManageServiceImpl implements KnowledgeBaseManageService { + + private final SuperAgentKnowledgeBaseMapper knowledgeBaseMapper; + private final SuperAgentDocumentMapper documentMapper; + private final UidGenerator uidGenerator; + private final ObjectMapper objectMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public KnowledgeBaseItemVo save(KnowledgeBaseSaveDto dto) { + validateSave(dto); + Long id = parseOptionalLong(dto.getId()); + SuperAgentKnowledgeBase entity = id == null ? null : knowledgeBaseMapper.selectById(id); + if (id != null && (entity == null || !Objects.equals(entity.getStatus(), BusinessStatus.YES.getCode()))) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "知识库不存在或已停用。"); + } + ensureUniqueBaseName(id, safeText(dto.getBaseName())); + if (entity == null) { + entity = new SuperAgentKnowledgeBase(); + entity.setId(uidGenerator.getUid()); + entity.setStatus(BusinessStatus.YES.getCode()); + } + entity.setBaseName(safeText(dto.getBaseName())); + entity.setDescription(safeText(dto.getDescription())); + entity.setEmbeddingModel(safeText(dto.getEmbeddingModel())); + entity.setRetrievalConfigJson(validateJson(dto.getRetrievalConfigJson(), "retrievalConfigJson")); + entity.setGraphRagConfigJson(validateJson(dto.getGraphRagConfigJson(), "graphRagConfigJson")); + entity.setRaptorConfigJson(validateJson(dto.getRaptorConfigJson(), "raptorConfigJson")); + entity.setMetadataFilterJson(validateJson(dto.getMetadataFilterJson(), "metadataFilterJson")); + entity.setIsDefault(parseInteger(dto.getIsDefault(), 0)); + entity.setSortOrder(parseInteger(dto.getSortOrder(), 0)); + if (Objects.equals(entity.getIsDefault(), 1)) { + clearOtherDefaults(entity.getId()); + } + if (entity.getCreateTime() == null) { + knowledgeBaseMapper.insert(entity); + } + else { + knowledgeBaseMapper.updateById(entity); + } + return toItemVo(entity, countDocumentsByBaseId(List.of(entity.getId())), countRetrievableDocumentsByBaseId(List.of(entity.getId()))); + } + + @Override + public boolean delete(KnowledgeBaseDeleteDto dto) { + Long id = parseRequiredLong(dto == null ? null : dto.getId(), "id"); + return knowledgeBaseMapper.update(null, new LambdaUpdateWrapper() + .eq(SuperAgentKnowledgeBase::getId, id) + .eq(SuperAgentKnowledgeBase::getStatus, BusinessStatus.YES.getCode()) + .set(SuperAgentKnowledgeBase::getStatus, BusinessStatus.NO.getCode())) > 0; + } + + @Override + public List list() { + List bases = knowledgeBaseMapper.selectList(baseListWrapper()); + Map documentCounts = countDocumentsByBaseId(bases.stream().map(SuperAgentKnowledgeBase::getId).toList()); + Map retrievableCounts = countRetrievableDocumentsByBaseId(bases.stream().map(SuperAgentKnowledgeBase::getId).toList()); + return bases.stream() + .map(base -> toItemVo(base, documentCounts, retrievableCounts)) + .toList(); + } + + @Override + public KnowledgeBaseItemVo detail(KnowledgeBaseDetailDto dto) { + SuperAgentKnowledgeBase entity = requireEnabled(parseRequiredLong(dto == null ? null : dto.getId(), "id")); + return toItemVo(entity, countDocumentsByBaseId(List.of(entity.getId())), countRetrievableDocumentsByBaseId(List.of(entity.getId()))); + } + + @Override + public KnowledgeBaseItemVo updateConfig(KnowledgeBaseConfigUpdateDto dto) { + SuperAgentKnowledgeBase entity = requireEnabled(parseRequiredLong(dto == null ? null : dto.getId(), "id")); + entity.setRetrievalConfigJson(validateJson(dto.getRetrievalConfigJson(), "retrievalConfigJson")); + entity.setGraphRagConfigJson(validateJson(dto.getGraphRagConfigJson(), "graphRagConfigJson")); + entity.setRaptorConfigJson(validateJson(dto.getRaptorConfigJson(), "raptorConfigJson")); + entity.setMetadataFilterJson(validateJson(dto.getMetadataFilterJson(), "metadataFilterJson")); + knowledgeBaseMapper.updateById(entity); + return toItemVo(entity, countDocumentsByBaseId(List.of(entity.getId())), countRetrievableDocumentsByBaseId(List.of(entity.getId()))); + } + + @Override + public List listOptions() { + List bases = knowledgeBaseMapper.selectList(baseListWrapper()); + Map retrievableCounts = countRetrievableDocumentsByBaseId(bases.stream().map(SuperAgentKnowledgeBase::getId).toList()); + return bases.stream() + .map(base -> new KnowledgeBaseOptionVo( + String.valueOf(base.getId()), + safeText(base.getBaseName()), + safeText(base.getDescription()), + String.valueOf(nullToZero(base.getIsDefault())), + String.valueOf(retrievableCounts.getOrDefault(base.getId(), 0L)) + )) + .toList(); + } + + @Override + public List listEnabledByIds(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return List.of(); + } + return knowledgeBaseMapper.selectList(new LambdaQueryWrapper() + .in(SuperAgentKnowledgeBase::getId, ids.stream().filter(Objects::nonNull).toList()) + .eq(SuperAgentKnowledgeBase::getStatus, BusinessStatus.YES.getCode()) + .orderByAsc(SuperAgentKnowledgeBase::getSortOrder, SuperAgentKnowledgeBase::getId)); + } + + @Override + public List listAllEnabled() { + return knowledgeBaseMapper.selectList(baseListWrapper()); + } + + @Override + public SuperAgentKnowledgeBase requireEnabled(Long id) { + if (id == null || id <= 0) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "knowledgeBaseId 不能为空。"); + } + SuperAgentKnowledgeBase entity = knowledgeBaseMapper.selectOne(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeBase::getId, id) + .eq(SuperAgentKnowledgeBase::getStatus, BusinessStatus.YES.getCode()) + .last("LIMIT 1")); + if (entity == null) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "知识库不存在或已停用。"); + } + return entity; + } + + private LambdaQueryWrapper baseListWrapper() { + return new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeBase::getStatus, BusinessStatus.YES.getCode()) + .orderByAsc(SuperAgentKnowledgeBase::getSortOrder, SuperAgentKnowledgeBase::getId); + } + + private void clearOtherDefaults(Long currentId) { + knowledgeBaseMapper.update(null, new LambdaUpdateWrapper() + .ne(SuperAgentKnowledgeBase::getId, currentId) + .eq(SuperAgentKnowledgeBase::getStatus, BusinessStatus.YES.getCode()) + .set(SuperAgentKnowledgeBase::getIsDefault, 0)); + } + + private Map countDocumentsByBaseId(List baseIds) { + if (CollUtil.isEmpty(baseIds)) { + return Map.of(); + } + return documentMapper.selectList(new LambdaQueryWrapper() + .in(SuperAgentDocument::getKnowledgeBaseId, baseIds) + .eq(SuperAgentDocument::getStatus, BusinessStatus.YES.getCode())) + .stream() + .collect(Collectors.groupingBy( + SuperAgentDocument::getKnowledgeBaseId, + LinkedHashMap::new, + Collectors.counting() + )); + } + + private Map countRetrievableDocumentsByBaseId(List baseIds) { + if (CollUtil.isEmpty(baseIds)) { + return Map.of(); + } + return documentMapper.selectList(new LambdaQueryWrapper() + .in(SuperAgentDocument::getKnowledgeBaseId, baseIds) + .eq(SuperAgentDocument::getStatus, BusinessStatus.YES.getCode()) + .eq(SuperAgentDocument::getIndexStatus, DocumentIndexStatusEnum.BUILD_SUCCESS.getCode()) + .isNotNull(SuperAgentDocument::getLastIndexTaskId)) + .stream() + .collect(Collectors.groupingBy( + SuperAgentDocument::getKnowledgeBaseId, + LinkedHashMap::new, + Collectors.counting() + )); + } + + private KnowledgeBaseItemVo toItemVo(SuperAgentKnowledgeBase entity, + Map documentCounts, + Map retrievableCounts) { + return new KnowledgeBaseItemVo( + String.valueOf(entity.getId()), + safeText(entity.getBaseName()), + safeText(entity.getDescription()), + safeText(entity.getEmbeddingModel()), + safeText(entity.getRetrievalConfigJson()), + safeText(entity.getGraphRagConfigJson()), + safeText(entity.getRaptorConfigJson()), + safeText(entity.getMetadataFilterJson()), + String.valueOf(nullToZero(entity.getIsDefault())), + String.valueOf(nullToZero(entity.getSortOrder())), + String.valueOf(documentCounts.getOrDefault(entity.getId(), 0L)), + String.valueOf(retrievableCounts.getOrDefault(entity.getId(), 0L)) + ); + } + + private void validateSave(KnowledgeBaseSaveDto dto) { + if (dto == null || safeText(dto.getBaseName()).isBlank()) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "baseName 不能为空。"); + } + } + + private void ensureUniqueBaseName(Long currentId, String baseName) { + SuperAgentKnowledgeBase sameName = knowledgeBaseMapper.selectOne(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeBase::getBaseName, baseName) + .eq(SuperAgentKnowledgeBase::getStatus, BusinessStatus.YES.getCode()) + .last("LIMIT 1")); + if (sameName != null && !Objects.equals(sameName.getId(), currentId)) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "知识库名称已存在。"); + } + } + + private String validateJson(String rawJson, String fieldName) { + String text = safeText(rawJson); + if (text.isBlank()) { + return null; + } + try { + objectMapper.readTree(text); + return text; + } + catch (JsonProcessingException | RuntimeException exception) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), fieldName + " 不是合法 JSON。"); + } + } + + private Long parseRequiredLong(String rawValue, String fieldName) { + Long value = parseOptionalLong(rawValue); + if (value == null || value <= 0) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), fieldName + "不能为空。"); + } + return value; + } + + private Long parseOptionalLong(String rawValue) { + if (StrUtil.isBlank(rawValue)) { + return null; + } + try { + return Long.valueOf(rawValue.trim()); + } + catch (NumberFormatException exception) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "id 格式非法。"); + } + } + + private Integer parseInteger(String rawValue, Integer fallback) { + if (StrUtil.isBlank(rawValue)) { + return fallback; + } + try { + return Integer.valueOf(rawValue.trim()); + } + catch (NumberFormatException exception) { + return fallback; + } + } + + private int nullToZero(Integer value) { + return value == null ? 0 : value; + } + + private String safeText(String text) { + return text == null ? "" : text.trim(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeBaseRetrievalScopeServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeBaseRetrievalScopeServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b6863ef571359043de2dde0f23700929cdd3fc39 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeBaseRetrievalScopeServiceImpl.java @@ -0,0 +1,136 @@ +package org.javaup.ai.manage.service.impl; + +import cn.hutool.core.collection.CollUtil; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; +import org.javaup.ai.chatagent.rag.service.KnowledgeBaseRuntimeConfigResolver; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; +import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.ai.manage.service.DocumentKnowledgeService; +import org.javaup.ai.manage.service.KnowledgeBaseManageService; +import org.javaup.ai.manage.service.KnowledgeBaseRetrievalScopeService; +import org.javaup.enums.BaseCode; +import org.javaup.enums.ChatQueryMode; +import org.javaup.enums.KnowledgeBaseSelectionMode; +import org.javaup.exception.SuperAgentFrameException; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Service +public class KnowledgeBaseRetrievalScopeServiceImpl implements KnowledgeBaseRetrievalScopeService { + + private final KnowledgeBaseManageService knowledgeBaseManageService; + private final DocumentKnowledgeService documentKnowledgeService; + private final KnowledgeBaseRuntimeConfigResolver runtimeConfigResolver; + + public KnowledgeBaseRetrievalScopeServiceImpl(KnowledgeBaseManageService knowledgeBaseManageService, + DocumentKnowledgeService documentKnowledgeService, + KnowledgeBaseRuntimeConfigResolver runtimeConfigResolver) { + this.knowledgeBaseManageService = knowledgeBaseManageService; + this.documentKnowledgeService = documentKnowledgeService; + this.runtimeConfigResolver = runtimeConfigResolver; + } + + @Override + public KnowledgeBaseSelectionSnapshot resolve(ChatQueryMode chatMode, + KnowledgeBaseSelectionMode selectionMode, + Collection selectedKnowledgeBaseIds) { + KnowledgeBaseSelectionMode resolvedMode = selectionMode == null ? KnowledgeBaseSelectionMode.NONE : selectionMode; + if (chatMode == ChatQueryMode.OPEN_CHAT || resolvedMode == KnowledgeBaseSelectionMode.NONE) { + return KnowledgeBaseSelectionSnapshot.none(runtimeConfigResolver.resolve(List.of())); + } + List selectedBases = switch (resolvedMode) { + case ALL -> selectAllWithRetrievableDocuments(); + case SELECTED -> selectExplicit(selectedKnowledgeBaseIds); + case NONE -> List.of(); + }; + if (selectedBases.isEmpty()) { + return KnowledgeBaseSelectionSnapshot.builder() + .selectionMode(resolvedMode) + .ragRuntimeOptions(runtimeConfigResolver.resolve(List.of())) + .build(); + } + List selectedBaseIds = selectedBases.stream().map(SuperAgentKnowledgeBase::getId).toList(); + List allowedDocuments = documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(selectedBaseIds); + RagRuntimeOptions options = runtimeConfigResolver.resolve(selectedBases); + return KnowledgeBaseSelectionSnapshot.builder() + .selectionMode(resolvedMode) + .selectedKnowledgeBases(selectedBases) + .selectedKnowledgeBaseIds(selectedBaseIds) + .selectedKnowledgeBaseNames(selectedBases.stream().map(SuperAgentKnowledgeBase::getBaseName).toList()) + .allowedDocuments(allowedDocuments) + .allowedDocumentIds(allowedDocuments.stream() + .map(KnowledgeDocumentDescriptor::getDocumentId) + .filter(Objects::nonNull) + .distinct() + .toList()) + .allowedTaskIds(allowedDocuments.stream() + .map(KnowledgeDocumentDescriptor::getLastIndexTaskId) + .filter(Objects::nonNull) + .distinct() + .toList()) + .ragRuntimeOptions(options) + .build(); + } + + private List selectAllWithRetrievableDocuments() { + List enabledBases = knowledgeBaseManageService.listAllEnabled(); + if (enabledBases.isEmpty()) { + return List.of(); + } + List baseIds = enabledBases.stream().map(SuperAgentKnowledgeBase::getId).toList(); + LinkedHashSet nonEmptyBaseIds = documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(baseIds) + .stream() + .map(KnowledgeDocumentDescriptor::getKnowledgeBaseId) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + return enabledBases.stream() + .filter(base -> nonEmptyBaseIds.contains(base.getId())) + .toList(); + } + + private List selectExplicit(Collection selectedKnowledgeBaseIds) { + List ids = selectedKnowledgeBaseIds == null + ? List.of() + : selectedKnowledgeBaseIds.stream() + .map(this::parseKnowledgeBaseId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (CollUtil.isEmpty(ids)) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "请选择至少一个知识库。"); + } + List bases = knowledgeBaseManageService.listEnabledByIds(ids); + Map byId = bases.stream() + .collect(Collectors.toMap(SuperAgentKnowledgeBase::getId, Function.identity())); + for (Long id : ids) { + if (!byId.containsKey(id)) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "知识库不存在或已停用: " + id); + } + } + return ids.stream().map(byId::get).toList(); + } + + private Long parseKnowledgeBaseId(String rawId) { + if (rawId == null || rawId.isBlank()) { + return null; + } + try { + Long id = Long.valueOf(rawId.trim()); + if (id <= 0) { + throw new NumberFormatException("must be positive"); + } + return id; + } + catch (NumberFormatException exception) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "knowledgeBaseId 格式非法。"); + } + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeManageServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeManageServiceImpl.java index f7fa3723e0b985a66f1679c3ec39226d0b9ff9fa..833806f76bc7a636132f502b6ebebad210ebe79f 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeManageServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeManageServiceImpl.java @@ -15,6 +15,7 @@ import org.javaup.ai.manage.dto.DocumentProfileDetailQueryDto; import org.javaup.ai.manage.dto.DocumentProfileRegenerateDto; import org.javaup.ai.manage.dto.KnowledgeRouteTraceQueryDto; import org.javaup.ai.manage.dto.KnowledgeScopeDeleteDto; +import org.javaup.ai.manage.dto.KnowledgeScopeQueryDto; import org.javaup.ai.manage.dto.KnowledgeScopeSaveDto; import org.javaup.ai.manage.dto.KnowledgeTopicDeleteDto; import org.javaup.ai.manage.dto.KnowledgeTopicQueryDto; @@ -28,6 +29,7 @@ import org.javaup.ai.manage.mapper.SuperAgentKnowledgeTopicNodeMapper; import org.javaup.ai.manage.mapper.SuperAgentKnowledgeRouteTraceMapper; import org.javaup.ai.manage.mapper.SuperAgentTopicDocumentRelationMapper; import org.javaup.ai.manage.service.DocumentProfileService; +import org.javaup.ai.manage.service.KnowledgeBaseManageService; import org.javaup.ai.manage.service.KnowledgeManageService; import org.javaup.ai.manage.vo.DocumentProfileVo; import org.javaup.ai.manage.vo.KnowledgeRouteTraceItemVo; @@ -42,6 +44,7 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -59,23 +62,32 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { private final SuperAgentKnowledgeRouteTraceMapper knowledgeRouteTraceMapper; private final SuperAgentDocumentMapper documentMapper; private final DocumentProfileService documentProfileService; + private final KnowledgeBaseManageService knowledgeBaseManageService; private final UidGenerator uidGenerator; @Override public KnowledgeScopeItemVo saveScope(KnowledgeScopeSaveDto dto) { validateScope(dto); - SuperAgentKnowledgeScopeNode entity = scopeNodeMapper.selectOne(new LambdaQueryWrapper() - .eq(SuperAgentKnowledgeScopeNode::getScopeCode, dto.getScopeCode().trim()) - .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode()) - .last("LIMIT 1")); + Long knowledgeBaseId = parseRequiredLong(dto.getKnowledgeBaseId(), "knowledgeBaseId"); + knowledgeBaseManageService.requireEnabled(knowledgeBaseId); + Long id = parseOptionalPositiveLong(dto.getId()); + Long parentScopeId = parseOptionalPositiveLong(dto.getParentScopeId()); + if (parentScopeId != null) { + requireScopeInBase(parentScopeId, knowledgeBaseId, "父级知识范围不存在或不属于当前知识库。"); + if (Objects.equals(id, parentScopeId)) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "父级知识范围不能是自己。"); + } + } + SuperAgentKnowledgeScopeNode entity = id == null ? null : requireScopeInBase(id, knowledgeBaseId, "知识范围不存在或不属于当前知识库。"); + ensureUniqueScopeName(knowledgeBaseId, safeText(dto.getScopeName()), id); if (entity == null) { entity = new SuperAgentKnowledgeScopeNode(); entity.setId(uidGenerator.getUid()); entity.setStatus(BusinessStatus.YES.getCode()); - entity.setScopeCode(dto.getScopeCode().trim()); + entity.setKnowledgeBaseId(knowledgeBaseId); } entity.setScopeName(safeText(dto.getScopeName())); - entity.setParentScopeCode(safeText(dto.getParentScopeCode())); + entity.setParentScopeId(parentScopeId); entity.setDescription(safeText(dto.getDescription())); entity.setAliases(safeText(dto.getAliases())); entity.setExamples(safeText(dto.getExamples())); @@ -91,21 +103,40 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { @Override public boolean deleteScope(KnowledgeScopeDeleteDto dto) { - String scopeCode = safeText(dto.getScopeCode()); - if (scopeCode.isBlank()) { - throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "scopeCode 不能为空。"); + Long id = parseRequiredLong(dto == null ? null : dto.getId(), "id"); + Long knowledgeBaseId = parseRequiredLong(dto.getKnowledgeBaseId(), "knowledgeBaseId"); + requireScopeInBase(id, knowledgeBaseId, "知识范围不存在或不属于当前知识库。"); + Long childCount = scopeNodeMapper.selectCount(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeScopeNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeScopeNode::getParentScopeId, id) + .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode())); + if (childCount != null && childCount > 0) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "请先删除或调整子级知识范围。"); + } + Long topicCount = topicNodeMapper.selectCount(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeTopicNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeTopicNode::getScopeId, id) + .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode())); + if (topicCount != null && topicCount > 0) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "请先删除或调整该范围下的知识主题。"); } return scopeNodeMapper.update(null, new LambdaUpdateWrapper() - .eq(SuperAgentKnowledgeScopeNode::getScopeCode, scopeCode) + .eq(SuperAgentKnowledgeScopeNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeScopeNode::getId, id) .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode()) .set(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.NO.getCode())) > 0; } @Override - public List listScopes() { - return scopeNodeMapper.selectList(new LambdaQueryWrapper() - .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode()) - .orderByAsc(SuperAgentKnowledgeScopeNode::getSortOrder, SuperAgentKnowledgeScopeNode::getId)) + public List listScopes(KnowledgeScopeQueryDto dto) { + Long knowledgeBaseId = parseOptionalPositiveLong(dto == null ? null : dto.getKnowledgeBaseId()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode()) + .orderByAsc(SuperAgentKnowledgeScopeNode::getSortOrder, SuperAgentKnowledgeScopeNode::getId); + if (knowledgeBaseId != null) { + wrapper.eq(SuperAgentKnowledgeScopeNode::getKnowledgeBaseId, knowledgeBaseId); + } + return scopeNodeMapper.selectList(wrapper) .stream() .map(this::toScopeVo) .toList(); @@ -114,18 +145,21 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { @Override public KnowledgeTopicItemVo saveTopic(KnowledgeTopicSaveDto dto) { validateTopic(dto); - SuperAgentKnowledgeTopicNode entity = topicNodeMapper.selectOne(new LambdaQueryWrapper() - .eq(SuperAgentKnowledgeTopicNode::getTopicCode, dto.getTopicCode().trim()) - .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode()) - .last("LIMIT 1")); + Long knowledgeBaseId = parseRequiredLong(dto.getKnowledgeBaseId(), "knowledgeBaseId"); + knowledgeBaseManageService.requireEnabled(knowledgeBaseId); + Long id = parseOptionalPositiveLong(dto.getId()); + Long scopeId = parseRequiredLong(dto.getScopeId(), "scopeId"); + requireScopeInBase(scopeId, knowledgeBaseId, "所属知识范围不存在或不属于当前知识库。"); + SuperAgentKnowledgeTopicNode entity = id == null ? null : requireTopicInBase(id, knowledgeBaseId, "知识主题不存在或不属于当前知识库。"); + ensureUniqueTopicName(knowledgeBaseId, scopeId, safeText(dto.getTopicName()), id); if (entity == null) { entity = new SuperAgentKnowledgeTopicNode(); entity.setId(uidGenerator.getUid()); entity.setStatus(BusinessStatus.YES.getCode()); - entity.setTopicCode(dto.getTopicCode().trim()); + entity.setKnowledgeBaseId(knowledgeBaseId); } entity.setTopicName(safeText(dto.getTopicName())); - entity.setScopeCode(safeText(dto.getScopeCode())); + entity.setScopeId(scopeId); entity.setDescription(safeText(dto.getDescription())); entity.setAliases(safeText(dto.getAliases())); entity.setExamples(safeText(dto.getExamples())); @@ -143,24 +177,35 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { @Override public boolean deleteTopic(KnowledgeTopicDeleteDto dto) { - String topicCode = safeText(dto.getTopicCode()); - if (topicCode.isBlank()) { - throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "topicCode 不能为空。"); + Long id = parseRequiredLong(dto == null ? null : dto.getId(), "id"); + Long knowledgeBaseId = parseRequiredLong(dto.getKnowledgeBaseId(), "knowledgeBaseId"); + requireTopicInBase(id, knowledgeBaseId, "知识主题不存在或不属于当前知识库。"); + Long relationCount = topicDocumentRelationMapper.selectCount(new LambdaQueryWrapper() + .eq(SuperAgentTopicDocumentRelation::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentTopicDocumentRelation::getTopicId, id) + .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode())); + if (relationCount != null && relationCount > 0) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "请先移除该主题下的文档关联。"); } return topicNodeMapper.update(null, new LambdaUpdateWrapper() - .eq(SuperAgentKnowledgeTopicNode::getTopicCode, topicCode) + .eq(SuperAgentKnowledgeTopicNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeTopicNode::getId, id) .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode()) .set(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.NO.getCode())) > 0; } @Override public List listTopics(KnowledgeTopicQueryDto dto) { - String scopeCode = dto == null ? "" : safeText(dto.getScopeCode()); + Long scopeId = parseOptionalPositiveLong(dto == null ? null : dto.getScopeId()); + Long knowledgeBaseId = parseOptionalPositiveLong(dto == null ? null : dto.getKnowledgeBaseId()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode()) .orderByAsc(SuperAgentKnowledgeTopicNode::getSortOrder, SuperAgentKnowledgeTopicNode::getId); - if (scopeCode != null && !scopeCode.isBlank()) { - wrapper.eq(SuperAgentKnowledgeTopicNode::getScopeCode, scopeCode); + if (knowledgeBaseId != null) { + wrapper.eq(SuperAgentKnowledgeTopicNode::getKnowledgeBaseId, knowledgeBaseId); + } + if (scopeId != null) { + wrapper.eq(SuperAgentKnowledgeTopicNode::getScopeId, scopeId); } return topicNodeMapper.selectList(wrapper).stream().map(this::toTopicVo).toList(); } @@ -191,12 +236,16 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { @Override public List listTopicDocuments(TopicDocumentRelationListQueryDto dto) { - String topicCode = dto == null ? "" : safeText(dto.getTopicCode()); + Long topicId = parseOptionalPositiveLong(dto == null ? null : dto.getTopicId()); + Long knowledgeBaseId = parseOptionalPositiveLong(dto == null ? null : dto.getKnowledgeBaseId()); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode()) .orderByDesc(SuperAgentTopicDocumentRelation::getRelationScore, SuperAgentTopicDocumentRelation::getId); - if (topicCode != null && !topicCode.isBlank()) { - wrapper.eq(SuperAgentTopicDocumentRelation::getTopicCode, topicCode); + if (knowledgeBaseId != null) { + wrapper.eq(SuperAgentTopicDocumentRelation::getKnowledgeBaseId, knowledgeBaseId); + } + if (topicId != null) { + wrapper.eq(SuperAgentTopicDocumentRelation::getTopicId, topicId); } return topicDocumentRelationMapper.selectList(wrapper).stream() .map(this::toRelationVo) @@ -205,20 +254,26 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { @Override public TopicDocumentRelationItemVo saveTopicDocumentRelation(TopicDocumentRelationSaveDto dto) { - String topicCode = safeText(dto.getTopicCode()); + Long knowledgeBaseId = parseRequiredLong(dto.getKnowledgeBaseId(), "knowledgeBaseId"); + knowledgeBaseManageService.requireEnabled(knowledgeBaseId); + Long topicId = parseRequiredLong(dto.getTopicId(), "topicId"); + requireTopicInBase(topicId, knowledgeBaseId, "知识主题不存在或不属于当前知识库。"); Long documentId = parseRequiredLong(dto.getDocumentId(), "documentId"); - if (topicCode.isBlank()) { - throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "topicCode 不能为空。"); + SuperAgentDocument document = documentMapper.selectById(documentId); + if (document == null || !Long.valueOf(knowledgeBaseId).equals(document.getKnowledgeBaseId())) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "文档不属于当前知识库。"); } SuperAgentTopicDocumentRelation relation = topicDocumentRelationMapper.selectOne(new LambdaQueryWrapper() - .eq(SuperAgentTopicDocumentRelation::getTopicCode, topicCode) + .eq(SuperAgentTopicDocumentRelation::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentTopicDocumentRelation::getTopicId, topicId) .eq(SuperAgentTopicDocumentRelation::getDocumentId, documentId) .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode()) .last("LIMIT 1")); if (relation == null) { relation = new SuperAgentTopicDocumentRelation(); relation.setId(uidGenerator.getUid()); - relation.setTopicCode(topicCode); + relation.setKnowledgeBaseId(knowledgeBaseId); + relation.setTopicId(topicId); relation.setDocumentId(documentId); relation.setStatus(BusinessStatus.YES.getCode()); } @@ -236,13 +291,12 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { @Override public boolean removeTopicDocumentRelation(TopicDocumentRelationRemoveDto dto) { - String topicCode = safeText(dto.getTopicCode()); + Long knowledgeBaseId = parseRequiredLong(dto.getKnowledgeBaseId(), "knowledgeBaseId"); + Long topicId = parseRequiredLong(dto.getTopicId(), "topicId"); Long documentId = parseRequiredLong(dto.getDocumentId(), "documentId"); - if (topicCode.isBlank()) { - throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "topicCode 不能为空。"); - } return topicDocumentRelationMapper.update(null, new LambdaUpdateWrapper() - .eq(SuperAgentTopicDocumentRelation::getTopicCode, topicCode) + .eq(SuperAgentTopicDocumentRelation::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentTopicDocumentRelation::getTopicId, topicId) .eq(SuperAgentTopicDocumentRelation::getDocumentId, documentId) .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode()) .set(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.NO.getCode())) > 0; @@ -304,23 +358,23 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { } private void validateScope(KnowledgeScopeSaveDto dto) { - if (dto == null || safeText(dto.getScopeCode()).isBlank() || safeText(dto.getScopeName()).isBlank()) { - throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "scopeCode 和 scopeName 不能为空。"); + if (dto == null || safeText(dto.getScopeName()).isBlank()) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "scopeName 不能为空。"); } } private void validateTopic(KnowledgeTopicSaveDto dto) { - if (dto == null || safeText(dto.getTopicCode()).isBlank() || safeText(dto.getTopicName()).isBlank() || safeText(dto.getScopeCode()).isBlank()) { - throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "topicCode、topicName、scopeCode 不能为空。"); + if (dto == null || safeText(dto.getTopicName()).isBlank() || parseOptionalPositiveLong(dto.getScopeId()) == null) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "topicName 和 scopeId 不能为空。"); } } private KnowledgeScopeItemVo toScopeVo(SuperAgentKnowledgeScopeNode node) { return new KnowledgeScopeItemVo( String.valueOf(node.getId()), - safeText(node.getScopeCode()), + node.getKnowledgeBaseId() == null ? "" : String.valueOf(node.getKnowledgeBaseId()), safeText(node.getScopeName()), - safeText(node.getParentScopeCode()), + node.getParentScopeId() == null ? "" : String.valueOf(node.getParentScopeId()), safeText(node.getDescription()), safeText(node.getAliases()), safeText(node.getExamples()), @@ -331,9 +385,9 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { private KnowledgeTopicItemVo toTopicVo(SuperAgentKnowledgeTopicNode node) { return new KnowledgeTopicItemVo( String.valueOf(node.getId()), - safeText(node.getTopicCode()), + node.getKnowledgeBaseId() == null ? "" : String.valueOf(node.getKnowledgeBaseId()), safeText(node.getTopicName()), - safeText(node.getScopeCode()), + node.getScopeId() == null ? "" : String.valueOf(node.getScopeId()), safeText(node.getDescription()), safeText(node.getAliases()), safeText(node.getExamples()), @@ -362,20 +416,69 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { private TopicDocumentRelationItemVo toRelationVo(SuperAgentTopicDocumentRelation relation) { SuperAgentDocument document = documentMapper.selectById(relation.getDocumentId()); + SuperAgentKnowledgeTopicNode topic = relation.getTopicId() == null ? null : topicNodeMapper.selectById(relation.getTopicId()); + SuperAgentKnowledgeScopeNode scope = topic == null || topic.getScopeId() == null ? null : scopeNodeMapper.selectById(topic.getScopeId()); return new TopicDocumentRelationItemVo( - safeText(relation.getTopicCode()), + relation.getKnowledgeBaseId() == null ? "" : String.valueOf(relation.getKnowledgeBaseId()), + relation.getTopicId() == null ? "" : String.valueOf(relation.getTopicId()), + topic == null ? "" : safeText(topic.getTopicName()), + scope == null ? "" : String.valueOf(scope.getId()), + scope == null ? "" : safeText(scope.getScopeName()), String.valueOf(relation.getDocumentId()), document == null ? "" : safeText(document.getDocumentName()), - document == null ? "" : safeText(document.getKnowledgeScopeCode()), - document == null ? "" : safeText(document.getKnowledgeScopeName()), - document == null ? "" : safeText(document.getBusinessCategory()), - document == null ? "" : safeText(document.getDocumentTags()), relation.getRelationScore() == null ? "0.0000" : relation.getRelationScore().toPlainString(), safeText(relation.getRelationSource()), safeText(relation.getReason()) ); } + private SuperAgentKnowledgeScopeNode requireScopeInBase(Long scopeId, Long knowledgeBaseId, String message) { + SuperAgentKnowledgeScopeNode scope = scopeNodeMapper.selectOne(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeScopeNode::getId, scopeId) + .eq(SuperAgentKnowledgeScopeNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode()) + .last("LIMIT 1")); + if (scope == null) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), message); + } + return scope; + } + + private SuperAgentKnowledgeTopicNode requireTopicInBase(Long topicId, Long knowledgeBaseId, String message) { + SuperAgentKnowledgeTopicNode topic = topicNodeMapper.selectOne(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeTopicNode::getId, topicId) + .eq(SuperAgentKnowledgeTopicNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode()) + .last("LIMIT 1")); + if (topic == null) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), message); + } + return topic; + } + + private void ensureUniqueScopeName(Long knowledgeBaseId, String scopeName, Long currentId) { + SuperAgentKnowledgeScopeNode existing = scopeNodeMapper.selectOne(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeScopeNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeScopeNode::getScopeName, scopeName) + .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode()) + .last("LIMIT 1")); + if (existing != null && !Objects.equals(existing.getId(), currentId)) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "知识范围名称已存在。"); + } + } + + private void ensureUniqueTopicName(Long knowledgeBaseId, Long scopeId, String topicName, Long currentId) { + SuperAgentKnowledgeTopicNode existing = topicNodeMapper.selectOne(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeTopicNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeTopicNode::getScopeId, scopeId) + .eq(SuperAgentKnowledgeTopicNode::getTopicName, topicName) + .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode()) + .last("LIMIT 1")); + if (existing != null && !Objects.equals(existing.getId(), currentId)) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "知识主题名称已存在。"); + } + } + private Long parseRequiredLong(String rawValue, String fieldName) { if (StrUtil.isBlank(rawValue)) { throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), fieldName + "不能为空。"); @@ -392,6 +495,19 @@ public class KnowledgeManageServiceImpl implements KnowledgeManageService { } } + private Long parseOptionalPositiveLong(String rawValue) { + if (StrUtil.isBlank(rawValue)) { + return null; + } + try { + Long value = Long.valueOf(rawValue.trim()); + return value > 0 ? value : null; + } + catch (NumberFormatException exception) { + throw new SuperAgentFrameException(BaseCode.PARAMETER_ERROR.getCode(), "id 格式非法。"); + } + } + private Integer parseInteger(String rawValue, Integer fallback) { if (StrUtil.isBlank(rawValue)) { return fallback; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeRouteServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeRouteServiceImpl.java index 074248c2b1909ec323dc9f22b33696fb4abe4241..d3822fa16d18d2473a057dfffacaed52dd59b219 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeRouteServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/KnowledgeRouteServiceImpl.java @@ -17,7 +17,9 @@ import org.javaup.ai.manage.mapper.SuperAgentKnowledgeRouteTraceMapper; import org.javaup.ai.manage.mapper.SuperAgentKnowledgeScopeNodeMapper; import org.javaup.ai.manage.mapper.SuperAgentKnowledgeTopicNodeMapper; import org.javaup.ai.manage.mapper.SuperAgentTopicDocumentRelationMapper; +import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; import org.javaup.ai.manage.model.route.DocumentRouteCandidate; +import org.javaup.ai.manage.model.route.KnowledgeRouteContext; import org.javaup.ai.manage.model.route.KnowledgeRouteDecision; import org.javaup.ai.manage.model.route.ScopeRouteCandidate; import org.javaup.ai.manage.model.route.TopicRouteCandidate; @@ -25,6 +27,7 @@ import org.javaup.ai.manage.service.KnowledgeRouteIndexService; import org.javaup.ai.manage.service.KnowledgeRouteService; import org.javaup.enums.BusinessStatus; import org.javaup.enums.DocumentIndexStatusEnum; +import org.javaup.enums.KnowledgeBaseSelectionMode; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; @@ -69,8 +72,8 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { private final UidGenerator uidGenerator; @Override - public KnowledgeRouteDecision route(String question, String rewriteQuestion) { - RouteQueryContext queryContext = buildQueryContext(question, rewriteQuestion); + public KnowledgeRouteDecision route(KnowledgeRouteContext context) { + RouteQueryContext queryContext = buildQueryContext(context); KnowledgeRouteDecision decision = new KnowledgeRouteDecision(); if (queryContext.queryTerms().isEmpty()) { decision.setRouteStatus("FAILED"); @@ -98,8 +101,8 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { ? "没有找到可用候选文档" : resolveDecisionReason(documentCandidates, confidence)); log.info("知识范围路由完成: question='{}', rewriteQuestion='{}', scopeCount={}, topicCount={}, documentCount={}, confidence={}, topDocument='{}'", - StrUtil.blankToDefault(question, ""), - StrUtil.blankToDefault(rewriteQuestion, ""), + StrUtil.blankToDefault(queryContext.originalQuestion(), ""), + StrUtil.blankToDefault(queryContext.rewriteQuestion(), ""), scopeCandidates.size(), topicCandidates.size(), documentCandidates.size(), @@ -112,11 +115,10 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { public void recordShadowRoute(String conversationId, long exchangeId, Long selectedDocumentId, - String question, - String rewriteQuestion) { + KnowledgeRouteContext context) { try { - KnowledgeRouteDecision decision = route(question, rewriteQuestion); - saveTrace(conversationId, exchangeId, selectedDocumentId, question, rewriteQuestion, "shadow", decision); + KnowledgeRouteDecision decision = route(context); + saveTrace(conversationId, exchangeId, selectedDocumentId, context, "shadow", decision); } catch (Exception exception) { log.warn("记录知识路由影子结果失败: conversationId={}, exchangeId={}", conversationId, exchangeId, exception); @@ -126,14 +128,13 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { @Override public void recordAutoRoute(String conversationId, long exchangeId, - String question, - String rewriteQuestion, + KnowledgeRouteContext context, KnowledgeRouteDecision decision) { try { Long selectedDocumentId = decision == null || decision.topDocument() == null || StrUtil.isBlank(decision.topDocument().getDocumentId()) ? null : Long.valueOf(decision.topDocument().getDocumentId()); - saveTrace(conversationId, exchangeId, selectedDocumentId, question, rewriteQuestion, "auto", decision); + saveTrace(conversationId, exchangeId, selectedDocumentId, context, "auto", decision); } catch (Exception exception) { log.warn("记录知识路由 AUTO 结果失败: conversationId={}, exchangeId={}", conversationId, exchangeId, exception); @@ -143,17 +144,20 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { private void saveTrace(String conversationId, long exchangeId, Long selectedDocumentId, - String question, - String rewriteQuestion, + KnowledgeRouteContext context, String mode, KnowledgeRouteDecision decision) { SuperAgentKnowledgeRouteTrace trace = new SuperAgentKnowledgeRouteTrace(); trace.setId(uidGenerator.getUid()); trace.setConversationId(conversationId); trace.setExchangeId(exchangeId); - trace.setQuestion(question); - trace.setRewriteQuestion(rewriteQuestion); + trace.setQuestion(context == null ? "" : context.getQuestion()); + trace.setRewriteQuestion(context == null ? "" : context.getRewriteQuestion()); trace.setMode(mode); + trace.setKnowledgeBaseSelectionMode(context == null || context.getKnowledgeBaseSelectionMode() == null ? KnowledgeBaseSelectionMode.NONE.name() : context.getKnowledgeBaseSelectionMode().name()); + trace.setSelectedKnowledgeBaseIdsJson(writeStringJson(context == null ? List.of() : context.getSelectedKnowledgeBaseIds().stream().map(String::valueOf).toList())); + trace.setSelectedKnowledgeBaseNamesJson(writeStringJson(context == null ? List.of() : context.getSelectedKnowledgeBaseNames())); + trace.setAllowedDocumentIdsJson(writeStringJson(context == null ? List.of() : context.getAllowedDocumentIds().stream().map(String::valueOf).toList())); trace.setTopScopesJson(writeScopeJson(decision == null ? List.of() : decision.getScopes())); trace.setTopTopicsJson(writeTopicJson(decision == null ? List.of() : decision.getTopics())); trace.setTopDocumentsJson(writeDocumentJson(decision == null ? List.of() : decision.getDocuments())); @@ -188,50 +192,36 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { } private List rankScopes(RouteQueryContext queryContext) { - List nodes = scopeNodeMapper.selectList(new LambdaQueryWrapper() - .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode())); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeScopeNode::getStatus, BusinessStatus.YES.getCode()); + if (queryContext.selectedKnowledgeBaseIds() != null && !queryContext.selectedKnowledgeBaseIds().isEmpty()) { + wrapper.in(SuperAgentKnowledgeScopeNode::getKnowledgeBaseId, queryContext.selectedKnowledgeBaseIds()); + } + List nodes = scopeNodeMapper.selectList(wrapper); if (nodes.isEmpty()) { - return deriveScopesFromDocuments(queryContext); + return List.of(); } List routeTexts = nodes.stream() .map(node -> join(node.getScopeName(), node.getDescription(), node.getAliases(), node.getExamples())) .toList(); List semanticScores = computeSemanticScores(queryContext, routeTexts); - Map lexicalScores = searchLexicalScores(queryContext.routingText(), "scope", 5).stream() - .collect(Collectors.toMap(KnowledgeRouteIndexService.RouteLexicalHit::entityCode, KnowledgeRouteIndexService.RouteLexicalHit::score, (left, right) -> left)); + Map lexicalScores = searchLexicalScores(queryContext, "scope", 5).stream() + .filter(hit -> hit.entityId() != null) + .collect(Collectors.toMap(KnowledgeRouteIndexService.RouteLexicalHit::entityId, KnowledgeRouteIndexService.RouteLexicalHit::score, (left, right) -> left)); return buildScopeCandidates(queryContext, nodes, routeTexts, semanticScores, lexicalScores); } - private List deriveScopesFromDocuments(RouteQueryContext queryContext) { - List documents = listRetrievableDocuments(); - Map accumulatorMap = new LinkedHashMap<>(); - for (SuperAgentDocument document : documents) { - if (StrUtil.isBlank(document.getKnowledgeScopeCode()) && StrUtil.isBlank(document.getKnowledgeScopeName())) { - continue; - } - String code = firstNonBlank(document.getKnowledgeScopeCode(), "general_document"); - String name = firstNonBlank(document.getKnowledgeScopeName(), "通用文档"); - String routeText = join(code, name, document.getBusinessCategory(), document.getDocumentTags()); - double score = keywordEntityAssist(queryContext.queryTerms(), routeText); - double semanticScore = semanticScore(queryContext, routeText); - ScopeAccumulator accumulator = accumulatorMap.computeIfAbsent(code, key -> new ScopeAccumulator(code, name)); - if (score + semanticMainScore(semanticScore) > accumulator.maxScore) { - accumulator.maxScore = score + semanticMainScore(semanticScore); - accumulator.reason = buildReason(queryContext.queryTerms(), routeText, semanticScore); - } - } - return accumulatorMap.values().stream() - .filter(item -> item.maxScore > 0D || queryContext.semanticEnabled()) - .map(item -> new ScopeRouteCandidate(item.scopeCode, item.scopeName, scoreToBigDecimal(item.maxScore), item.reason)) - .sorted((left, right) -> right.getScore().compareTo(left.getScore())) - .limit(5) - .toList(); - } - private List rankTopics(RouteQueryContext queryContext, List scopeCandidates) { - List nodes = topicNodeMapper.selectList(new LambdaQueryWrapper() - .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode())); - Set preferredScopes = scopeCandidates.stream().map(ScopeRouteCandidate::getScopeCode).collect(Collectors.toSet()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode()); + if (queryContext.selectedKnowledgeBaseIds() != null && !queryContext.selectedKnowledgeBaseIds().isEmpty()) { + wrapper.in(SuperAgentKnowledgeTopicNode::getKnowledgeBaseId, queryContext.selectedKnowledgeBaseIds()); + } + List nodes = topicNodeMapper.selectList(wrapper); + Set preferredScopes = scopeCandidates.stream() + .map(ScopeRouteCandidate::getScopeId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); if (nodes.isEmpty()) { return deriveTopicsFromProfiles(queryContext, preferredScopes); } @@ -246,23 +236,24 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { )) .toList(); List semanticScores = computeSemanticScores(queryContext, routeTexts); - Map lexicalScores = searchLexicalScores(queryContext.routingText(), "topic", 8).stream() - .collect(Collectors.toMap(KnowledgeRouteIndexService.RouteLexicalHit::entityCode, KnowledgeRouteIndexService.RouteLexicalHit::score, (left, right) -> left)); + Map lexicalScores = searchLexicalScores(queryContext, "topic", 8).stream() + .filter(hit -> hit.entityId() != null) + .collect(Collectors.toMap(KnowledgeRouteIndexService.RouteLexicalHit::entityId, KnowledgeRouteIndexService.RouteLexicalHit::score, (left, right) -> left)); List candidates = new ArrayList<>(nodes.size()); for (int index = 0; index < nodes.size(); index++) { SuperAgentKnowledgeTopicNode node = nodes.get(index); String routeText = routeTexts.get(index); double score = semanticMainScore(semanticScores.get(index)) - + lexicalAssist(lexicalScores.get(node.getTopicCode())) + + lexicalAssist(lexicalScores.get(node.getId())) + keywordEntityAssist(queryContext.queryTerms(), routeText); - if (!preferredScopes.isEmpty() && preferredScopes.contains(node.getScopeCode())) { + if (!preferredScopes.isEmpty() && preferredScopes.contains(node.getScopeId())) { score += 8D; } if (score > 0D || queryContext.semanticEnabled()) { candidates.add(new TopicRouteCandidate( - node.getTopicCode(), + node.getId(), node.getTopicName(), - node.getScopeCode(), + node.getScopeId(), scoreToBigDecimal(score), buildReason(queryContext.queryTerms(), routeText, semanticScores.get(index)) )); @@ -274,24 +265,23 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { .toList(); } - private List deriveTopicsFromProfiles(RouteQueryContext queryContext, Set preferredScopes) { - List profiles = documentProfileMapper.selectList(new LambdaQueryWrapper() - .eq(SuperAgentDocumentProfile::getStatus, BusinessStatus.YES.getCode()) - .eq(SuperAgentDocumentProfile::getProfileStatus, 2)); + private List deriveTopicsFromProfiles(RouteQueryContext queryContext, Set preferredScopes) { Map accumulatorMap = new LinkedHashMap<>(); - Map documentMap = listRetrievableDocuments().stream() + Map documentMap = listRetrievableDocuments(queryContext).stream() .collect(Collectors.toMap(SuperAgentDocument::getId, item -> item)); + if (documentMap.isEmpty()) { + return List.of(); + } + List profiles = documentProfileMapper.selectList(new LambdaQueryWrapper() + .eq(SuperAgentDocumentProfile::getStatus, BusinessStatus.YES.getCode()) + .eq(SuperAgentDocumentProfile::getProfileStatus, 2) + .in(SuperAgentDocumentProfile::getDocumentId, documentMap.keySet())); for (SuperAgentDocumentProfile profile : profiles) { - SuperAgentDocument document = documentMap.get(profile.getDocumentId()); - String scopeCode = document == null ? "" : StrUtil.blankToDefault(document.getKnowledgeScopeCode(), ""); for (String topic : parseJsonArray(profile.getCoreTopics())) { String routeText = join(topic, profile.getDocumentSummary(), profile.getExampleQuestions()); double score = keywordEntityAssist(queryContext.queryTerms(), routeText); double semanticScore = semanticScore(queryContext, routeText); - if (!preferredScopes.isEmpty() && preferredScopes.contains(scopeCode)) { - score += 6D; - } - TopicAccumulator accumulator = accumulatorMap.computeIfAbsent(topic, key -> new TopicAccumulator(topic, scopeCode)); + TopicAccumulator accumulator = accumulatorMap.computeIfAbsent(topic, TopicAccumulator::new); double finalScore = score + semanticMainScore(semanticScore); if (finalScore > accumulator.maxScore) { accumulator.maxScore = finalScore; @@ -301,7 +291,7 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { } return accumulatorMap.values().stream() .filter(item -> item.maxScore > 0D || queryContext.semanticEnabled()) - .map(item -> new TopicRouteCandidate(normalizeCode(item.topicName), item.topicName, item.scopeCode, scoreToBigDecimal(item.maxScore), item.reason)) + .map(item -> new TopicRouteCandidate(null, item.topicName, null, scoreToBigDecimal(item.maxScore), item.reason)) .sorted((left, right) -> right.getScore().compareTo(left.getScore())) .limit(8) .toList(); @@ -310,7 +300,7 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { private List rankDocuments(RouteQueryContext queryContext, List scopeCandidates, List topicCandidates) { - List documents = listRetrievableDocuments(); + List documents = listRetrievableDocuments(queryContext); if (documents.isEmpty()) { return List.of(); } @@ -319,19 +309,25 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { .eq(SuperAgentDocumentProfile::getProfileStatus, 2)) .stream() .collect(Collectors.toMap(SuperAgentDocumentProfile::getDocumentId, item -> item, (left, right) -> right)); - Map> topicRelationMap = topicDocumentRelationMapper.selectList( - new LambdaQueryWrapper() - .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode())) + LambdaQueryWrapper relationWrapper = new LambdaQueryWrapper() + .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode()); + if (queryContext.selectedKnowledgeBaseIds() != null && !queryContext.selectedKnowledgeBaseIds().isEmpty()) { + relationWrapper.in(SuperAgentTopicDocumentRelation::getKnowledgeBaseId, queryContext.selectedKnowledgeBaseIds()); + } + if (queryContext.allowedDocumentIds() != null && !queryContext.allowedDocumentIds().isEmpty()) { + relationWrapper.in(SuperAgentTopicDocumentRelation::getDocumentId, queryContext.allowedDocumentIds()); + } + Map> topicRelationMap = topicDocumentRelationMapper.selectList(relationWrapper) .stream() - .collect(Collectors.groupingBy(SuperAgentTopicDocumentRelation::getTopicCode, + .filter(relation -> relation.getTopicId() != null) + .collect(Collectors.groupingBy(SuperAgentTopicDocumentRelation::getTopicId, Collectors.toMap(SuperAgentTopicDocumentRelation::getDocumentId, item -> item, (left, right) -> right))); - String topScopeCode = scopeCandidates.isEmpty() ? "" : scopeCandidates.get(0).getScopeCode(); - String topTopicCode = topicCandidates.isEmpty() ? "" : topicCandidates.get(0).getTopicCode(); + Long topTopicId = topicCandidates.isEmpty() ? null : topicCandidates.get(0).getTopicId(); List materials = documents.stream() .map(document -> buildDocumentRouteMaterial(document, profileMap.get(document.getId()))) .toList(); List semanticScores = computeSemanticScores(queryContext, materials.stream().map(DocumentRouteMaterial::routeText).toList()); - Map lexicalScores = searchLexicalScores(queryContext.routingText(), "document", 5).stream() + Map lexicalScores = searchLexicalScores(queryContext, "document", 5).stream() .filter(hit -> hit.documentId() != null) .collect(Collectors.toMap(KnowledgeRouteIndexService.RouteLexicalHit::documentId, KnowledgeRouteIndexService.RouteLexicalHit::score, (left, right) -> left)); return documents.stream() @@ -339,8 +335,7 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { queryContext, document, profileMap.get(document.getId()), - topScopeCode, - topTopicCode, + topTopicId, topicRelationMap, materials, semanticScores, @@ -355,29 +350,19 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { private DocumentRouteCandidate buildDocumentCandidate(RouteQueryContext queryContext, SuperAgentDocument document, SuperAgentDocumentProfile profile, - String topScopeCode, - String topTopicCode, - Map> topicRelationMap, + Long topTopicId, + Map> topicRelationMap, List materials, List semanticScores, Map lexicalScores) { int materialIndex = findMaterialIndex(materials, document.getId()); - String routeText = materialIndex >= 0 ? materials.get(materialIndex).routeText() : join( - document.getDocumentName(), - document.getKnowledgeScopeName(), - document.getKnowledgeScopeCode(), - document.getBusinessCategory(), - document.getDocumentTags() - ); + String routeText = materialIndex >= 0 ? materials.get(materialIndex).routeText() : document.getDocumentName(); double semanticScore = materialIndex >= 0 && materialIndex < semanticScores.size() ? semanticScores.get(materialIndex) : 0D; double score = semanticMainScore(semanticScore) + lexicalAssist(lexicalScores.get(document.getId())) + keywordEntityAssist(queryContext.queryTerms(), routeText); - if (StrUtil.isNotBlank(topScopeCode) && topScopeCode.equals(document.getKnowledgeScopeCode())) { - score += 15D; - } - if (StrUtil.isNotBlank(topTopicCode)) { - Map relationMap = topicRelationMap.get(topTopicCode); + if (topTopicId != null) { + Map relationMap = topicRelationMap.get(topTopicId); if (relationMap != null) { SuperAgentTopicDocumentRelation relation = relationMap.get(document.getId()); if (relation != null && relation.getRelationScore() != null) { @@ -390,10 +375,6 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { String.valueOf(document.getId()), document.getDocumentName(), document.getLastIndexTaskId() == null ? "" : String.valueOf(document.getLastIndexTaskId()), - StrUtil.blankToDefault(document.getKnowledgeScopeCode(), ""), - StrUtil.blankToDefault(document.getKnowledgeScopeName(), ""), - StrUtil.blankToDefault(document.getBusinessCategory(), ""), - StrUtil.blankToDefault(document.getDocumentTags(), ""), BigDecimal.ZERO, "未命中路由关键词" ); @@ -402,25 +383,35 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { String.valueOf(document.getId()), document.getDocumentName(), document.getLastIndexTaskId() == null ? "" : String.valueOf(document.getLastIndexTaskId()), - StrUtil.blankToDefault(document.getKnowledgeScopeCode(), ""), - StrUtil.blankToDefault(document.getKnowledgeScopeName(), ""), - StrUtil.blankToDefault(document.getBusinessCategory(), ""), - StrUtil.blankToDefault(document.getDocumentTags(), ""), scoreToBigDecimal(score), buildReason(queryContext.queryTerms(), routeText, semanticScore) ); } - private RouteQueryContext buildQueryContext(String question, String rewriteQuestion) { + private RouteQueryContext buildQueryContext(KnowledgeRouteContext context) { + String question = context == null ? "" : context.getQuestion(); + String rewriteQuestion = context == null ? "" : context.getRewriteQuestion(); String routingText = buildRoutingText(question, rewriteQuestion); List queryTerms = tokenize(routingText); float[] queryEmbedding = embedSingle(routingText); + List selectedKnowledgeBaseIds = context == null || context.getSelectedKnowledgeBaseIds() == null + ? List.of() + : context.getSelectedKnowledgeBaseIds().stream().filter(Objects::nonNull).distinct().toList(); + List allowedDocumentIds = context == null || context.getAllowedDocumentIds() == null + ? List.of() + : context.getAllowedDocumentIds().stream().filter(Objects::nonNull).distinct().toList(); + List allowedDocuments = context == null || context.getAllowedDocuments() == null + ? List.of() + : context.getAllowedDocuments(); return new RouteQueryContext( StrUtil.blankToDefault(question, ""), StrUtil.blankToDefault(rewriteQuestion, ""), routingText, queryTerms, - queryEmbedding + queryEmbedding, + selectedKnowledgeBaseIds, + allowedDocumentIds, + allowedDocuments ); } @@ -440,17 +431,17 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { List nodes, List routeTexts, List semanticScores, - Map lexicalScores) { + Map lexicalScores) { List candidates = new ArrayList<>(nodes.size()); for (int index = 0; index < nodes.size(); index++) { SuperAgentKnowledgeScopeNode node = nodes.get(index); String routeText = routeTexts.get(index); double finalScore = semanticMainScore(semanticScores.get(index)) - + lexicalAssist(lexicalScores.get(node.getScopeCode())) + + lexicalAssist(lexicalScores.get(node.getId())) + keywordEntityAssist(queryContext.queryTerms(), routeText); if (finalScore > 0D || semanticScores.get(index) > 0D) { candidates.add(new ScopeRouteCandidate( - node.getScopeCode(), + node.getId(), node.getScopeName(), scoreToBigDecimal(finalScore), buildReason(queryContext.queryTerms(), routeText, semanticScores.get(index)) @@ -468,10 +459,6 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { document.getId(), join( document.getDocumentName(), - document.getKnowledgeScopeName(), - document.getKnowledgeScopeCode(), - document.getBusinessCategory(), - document.getDocumentTags(), profile == null ? "" : profile.getDocumentSummary(), profile == null ? "" : profile.getCoreTopics(), profile == null ? "" : profile.getExampleQuestions(), @@ -500,11 +487,22 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { } private List listRetrievableDocuments() { - return documentMapper.selectList(new LambdaQueryWrapper() + return listRetrievableDocuments(null); + } + + private List listRetrievableDocuments(RouteQueryContext queryContext) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(SuperAgentDocument::getStatus, BusinessStatus.YES.getCode()) .eq(SuperAgentDocument::getIndexStatus, DocumentIndexStatusEnum.BUILD_SUCCESS.getCode()) .isNotNull(SuperAgentDocument::getLastIndexTaskId) - .orderByAsc(SuperAgentDocument::getId)); + .orderByAsc(SuperAgentDocument::getId); + if (queryContext != null && queryContext.allowedDocumentIds() != null && !queryContext.allowedDocumentIds().isEmpty()) { + wrapper.in(SuperAgentDocument::getId, queryContext.allowedDocumentIds()); + } + else if (queryContext != null && queryContext.selectedKnowledgeBaseIds() != null && !queryContext.selectedKnowledgeBaseIds().isEmpty()) { + wrapper.in(SuperAgentDocument::getKnowledgeBaseId, queryContext.selectedKnowledgeBaseIds()); + } + return documentMapper.selectList(wrapper); } private double lexicalScore(List queryTerms, String content) { @@ -537,12 +535,31 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { return score; } - private List searchLexicalScores(String routingText, String entityType, int size) { + private List searchLexicalScores(RouteQueryContext queryContext, String entityType, int size) { KnowledgeRouteIndexService routeIndexService = knowledgeRouteIndexServiceProvider.getIfAvailable(); if (routeIndexService == null) { return List.of(); } - return routeIndexService.search(routingText, entityType, size); + List hits = routeIndexService.search( + queryContext.routingText(), + entityType, + size, + queryContext.selectedKnowledgeBaseIds() + ); + if (hits == null || hits.isEmpty()) { + return hits == null ? List.of() : hits; + } + if (queryContext.selectedKnowledgeBaseIds() != null && !queryContext.selectedKnowledgeBaseIds().isEmpty()) { + hits = hits.stream() + .filter(hit -> hit.knowledgeBaseId() == null || queryContext.selectedKnowledgeBaseIds().contains(hit.knowledgeBaseId())) + .toList(); + } + if (queryContext.allowedDocumentIds() == null || queryContext.allowedDocumentIds().isEmpty()) { + return hits == null ? List.of() : hits; + } + return hits.stream() + .filter(hit -> hit.documentId() == null || queryContext.allowedDocumentIds().contains(hit.documentId())) + .toList(); } private List tokenize(String text) { @@ -616,10 +633,6 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { return StrUtil.blankToDefault(fallback, ""); } - private String normalizeCode(String value) { - return normalize(value).replaceAll("[^a-z0-9]+", "_"); - } - private float[] embedSingle(String text) { if (StrUtil.isBlank(text)) { return null; @@ -795,13 +808,13 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { private String writeScopeJson(List candidates) { return candidates == null || candidates.isEmpty() ? "[]" : candidates.stream() - .map(item -> "{\"scopeCode\":\"" + item.getScopeCode() + "\",\"scopeName\":\"" + item.getScopeName() + "\",\"score\":\"" + item.getScore() + "\",\"reason\":\"" + escapeJson(item.getReason()) + "\"}") + .map(item -> "{\"scopeId\":\"" + nullToEmpty(item.getScopeId()) + "\",\"scopeName\":\"" + escapeJson(item.getScopeName()) + "\",\"score\":\"" + item.getScore() + "\",\"reason\":\"" + escapeJson(item.getReason()) + "\"}") .collect(Collectors.joining(",", "[", "]")); } private String writeTopicJson(List candidates) { return candidates == null || candidates.isEmpty() ? "[]" : candidates.stream() - .map(item -> "{\"topicCode\":\"" + item.getTopicCode() + "\",\"topicName\":\"" + item.getTopicName() + "\",\"scopeCode\":\"" + item.getScopeCode() + "\",\"score\":\"" + item.getScore() + "\",\"reason\":\"" + escapeJson(item.getReason()) + "\"}") + .map(item -> "{\"topicId\":\"" + nullToEmpty(item.getTopicId()) + "\",\"topicName\":\"" + escapeJson(item.getTopicName()) + "\",\"scopeId\":\"" + nullToEmpty(item.getScopeId()) + "\",\"score\":\"" + item.getScore() + "\",\"reason\":\"" + escapeJson(item.getReason()) + "\"}") .collect(Collectors.joining(",", "[", "]")); } @@ -811,39 +824,42 @@ public class KnowledgeRouteServiceImpl implements KnowledgeRouteService { .collect(Collectors.joining(",", "[", "]")); } - private String escapeJson(String text) { - return StrUtil.blankToDefault(text, "").replace("\"", "\\\""); + private String writeStringJson(List values) { + if (values == null || values.isEmpty()) { + return "[]"; + } + return values.stream() + .filter(Objects::nonNull) + .map(value -> "\"" + escapeJson(value) + "\"") + .collect(Collectors.joining(",", "[", "]")); } - private static final class ScopeAccumulator { - private final String scopeCode; - private final String scopeName; - private double maxScore; - private String reason = ""; - - private ScopeAccumulator(String scopeCode, String scopeName) { - this.scopeCode = scopeCode; - this.scopeName = scopeName; - } + private String escapeJson(String text) { + return StrUtil.blankToDefault(text, "").replace("\"", "\\\""); } private static final class TopicAccumulator { private final String topicName; - private final String scopeCode; private double maxScore; private String reason = ""; - private TopicAccumulator(String topicName, String scopeCode) { + private TopicAccumulator(String topicName) { this.topicName = topicName; - this.scopeCode = scopeCode; } } + private String nullToEmpty(Long value) { + return value == null ? "" : String.valueOf(value); + } + private record RouteQueryContext(String originalQuestion, String rewriteQuestion, String routingText, List queryTerms, - float[] queryEmbedding) { + float[] queryEmbedding, + List selectedKnowledgeBaseIds, + List allowedDocumentIds, + List allowedDocuments) { private boolean semanticEnabled() { return queryEmbedding != null && queryEmbedding.length > 0; } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/RaptorBuildServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/RaptorBuildServiceImpl.java index fe7d2abdcb60de9acf532ef353ae6b3e282ea7bf..611994af395c2e14ef1e0388e661d192bca01e83 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/RaptorBuildServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/RaptorBuildServiceImpl.java @@ -11,16 +11,22 @@ import lombok.extern.slf4j.Slf4j; import org.javaup.ai.manage.config.DocumentManageProperties; import org.javaup.ai.manage.data.SuperAgentDocument; import org.javaup.ai.manage.data.SuperAgentDocumentChunk; +import org.javaup.ai.manage.data.SuperAgentKnowledgeTopicNode; import org.javaup.ai.manage.data.SuperAgentRaptorNode; +import org.javaup.ai.manage.data.SuperAgentTopicDocumentRelation; import org.javaup.ai.manage.mapper.SuperAgentDocumentChunkMapper; import org.javaup.ai.manage.mapper.SuperAgentDocumentMapper; +import org.javaup.ai.manage.mapper.SuperAgentKnowledgeTopicNodeMapper; import org.javaup.ai.manage.mapper.SuperAgentRaptorNodeMapper; +import org.javaup.ai.manage.mapper.SuperAgentTopicDocumentRelationMapper; +import org.javaup.ai.manage.model.KnowledgeBaseIndexingOptions; import org.javaup.ai.manage.model.raptor.RaptorBuildResult; import org.javaup.ai.manage.model.raptor.RaptorQualityReport; import org.javaup.ai.manage.service.RaptorBuildService; import org.javaup.ai.manage.service.RaptorQualityService; import org.javaup.ai.manage.service.RaptorSummaryIndexService; import org.javaup.ai.manage.support.DocumentPgVectorConstants; +import org.javaup.ai.manage.support.KnowledgeBaseIndexingConfigResolver; import org.javaup.ai.manage.support.MybatisBatchExecutor; import org.javaup.ai.manage.support.RaptorDatasetBuildSupport; import org.javaup.ai.manage.support.RaptorScopeSupport; @@ -105,6 +111,10 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { private final SuperAgentDocumentChunkMapper chunkMapper; + private final SuperAgentKnowledgeTopicNodeMapper topicNodeMapper; + + private final SuperAgentTopicDocumentRelationMapper topicDocumentRelationMapper; + private final RagToolsClient ragToolsClient; private final ObjectMapper objectMapper; @@ -124,6 +134,8 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { private final ObjectProvider raptorSummaryIndexServiceProvider; + private final KnowledgeBaseIndexingConfigResolver indexingConfigResolver; + @Value("${spring.ai.openai.embedding.options.model:}") private String embeddingModelName; @@ -143,6 +155,11 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { @Transactional(rollbackFor = Exception.class) public RaptorBuildResult rebuildDocumentTree(Long documentId, Long taskId, List chunks) { deleteByTask(documentId, taskId); + KnowledgeBaseIndexingOptions.RaptorBuildOptions raptorOptions = raptorBuildOptionsByDocumentId(documentId); + if (!Boolean.TRUE.equals(raptorOptions.getRaptorBuildEnabled())) { + log.info("知识库配置已关闭 RAPTOR 构建,跳过文档摘要树构建: documentId={}, taskId={}", documentId, taskId); + return RaptorBuildResult.builder().build(); + } if (documentId == null || taskId == null || CollUtil.isEmpty(chunks)) { return RaptorBuildResult.builder().build(); } @@ -151,14 +168,15 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { log.info("开始构建 RAPTOR 摘要树,documentId={}, taskId={}, chunkCount={}", documentId, taskId, chunks.size()); String scopeKey = RaptorScopeSupport.documentScopeKey(documentId); long pythonStartedNanos = System.nanoTime(); - RagToolsRaptorBuildResponse response = ragToolsClient.buildRaptor(buildRequest(documentId, taskId, chunks)); + RagToolsRaptorBuildResponse response = ragToolsClient.buildRaptor(buildRequest(documentId, taskId, chunks, raptorOptions)); log.info("Python RAPTOR 构建调用完成,documentId={}, taskId={}, scopeType={}, scopeKey={}, costMillis={}", documentId, taskId, RaptorScopeSupport.SCOPE_TYPE_DOCUMENT, scopeKey, elapsedMillis(pythonStartedNanos)); if (response == null) { throw new IllegalStateException("Python RAPTOR 构建接口返回为空。"); } - logLlmSummaryFallbacks(documentId, taskId, response.getNodes()); - RaptorQualityReport sourceQualityReport = raptorQualityService.evaluatePythonNodes(response.getNodes(), qualityFloor()); + logLlmSummaryFallbacks(documentId, taskId, response.getNodes(), Boolean.TRUE.equals(raptorOptions.getRaptorLlmSummaryEnabled())); + double qualityFloor = qualityFloor(raptorOptions); + RaptorQualityReport sourceQualityReport = raptorQualityService.evaluatePythonNodes(response.getNodes(), qualityFloor); log.info("Python RAPTOR 原始摘要质量评测完成,documentId={}, taskId={}, nodeCount={}, avgQuality={}, minQuality={}, p10Quality={}, medianQuality={}, recommendedFloor={}, lowQualityCount={}", documentId, taskId, @@ -170,7 +188,7 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { sourceQualityReport.getRecommendedQualityFloor(), sourceQualityReport.getLowQualityNodeCount()); - Map idMap = allocateNodeIds(response.getNodes()); + Map idMap = allocateNodeIds(response.getNodes(), qualityFloor); List nodes = buildNodeEntities( documentId, taskId, @@ -180,14 +198,15 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { idMap, distinctChunkDocumentIds(chunks), distinctChunkTaskIds(chunks), - null + null, + qualityFloor ); if (CollUtil.isEmpty(nodes)) { log.info("RAPTOR 构建结果没有达到质量阈值的摘要节点,documentId={}, taskId={}, qualityFloor={}", - documentId, taskId, qualityFloor()); + documentId, taskId, qualityFloor); return RaptorBuildResult.builder() .sourceQualityReport(sourceQualityReport) - .savedQualityReport(raptorQualityService.evaluatePythonNodes(List.of(), qualityFloor())) + .savedQualityReport(raptorQualityService.evaluatePythonNodes(List.of(), qualityFloor)) .build(); } long insertStartedNanos = System.nanoTime(); @@ -221,14 +240,50 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { @Override @Transactional(rollbackFor = Exception.class) - public RaptorBuildResult rebuildKnowledgeScopeTree(String knowledgeScopeCode) { - String normalizedScopeCode = RaptorScopeSupport.normalizeScopeCode(knowledgeScopeCode); - if (StrUtil.isBlank(normalizedScopeCode)) { + public RaptorBuildResult rebuildKnowledgeScopeTree(Long knowledgeBaseId, Long scopeId) { + if (knowledgeBaseId == null || scopeId == null || scopeId <= 0) { + return RaptorBuildResult.builder().build(); + } + String scopeKey = RaptorScopeSupport.knowledgeScopeKey(knowledgeBaseId, scopeId); + KnowledgeBaseIndexingOptions.RaptorBuildOptions raptorOptions = raptorBuildOptionsByKnowledgeBaseId(knowledgeBaseId); + if (!Boolean.TRUE.equals(raptorOptions.getRaptorBuildEnabled())) { + deleteByScope(RaptorScopeSupport.SCOPE_TYPE_DATASET, scopeKey); + log.info("知识库配置已关闭 RAPTOR 构建,跳过 dataset-level 摘要树构建: knowledgeBaseId={}, scopeId={}, scopeKey={}", + knowledgeBaseId, scopeId, scopeKey); + return RaptorBuildResult.builder().build(); + } + List topicIds = topicNodeMapper.selectList(new LambdaQueryWrapper() + .eq(SuperAgentKnowledgeTopicNode::getKnowledgeBaseId, knowledgeBaseId) + .eq(SuperAgentKnowledgeTopicNode::getScopeId, scopeId) + .eq(SuperAgentKnowledgeTopicNode::getStatus, BusinessStatus.YES.getCode())) + .stream() + .map(SuperAgentKnowledgeTopicNode::getId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (topicIds.isEmpty()) { + deleteByScope(RaptorScopeSupport.SCOPE_TYPE_DATASET, scopeKey); + log.info("跳过 RAPTOR dataset-level 构建,知识范围没有可用主题: scopeKey={}", scopeKey); + return RaptorBuildResult.builder().build(); + } + List documentIds = topicDocumentRelationMapper.selectList(new LambdaQueryWrapper() + .eq(SuperAgentTopicDocumentRelation::getKnowledgeBaseId, knowledgeBaseId) + .in(SuperAgentTopicDocumentRelation::getTopicId, topicIds) + .eq(SuperAgentTopicDocumentRelation::getStatus, BusinessStatus.YES.getCode())) + .stream() + .map(SuperAgentTopicDocumentRelation::getDocumentId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (documentIds.isEmpty()) { + deleteByScope(RaptorScopeSupport.SCOPE_TYPE_DATASET, scopeKey); + log.info("跳过 RAPTOR dataset-level 构建,知识范围没有主题文档关联: scopeKey={}, topicCount={}", + scopeKey, topicIds.size()); return RaptorBuildResult.builder().build(); } - String scopeKey = RaptorScopeSupport.knowledgeScopeKey(normalizedScopeCode); List documents = documentMapper.selectList(new LambdaQueryWrapper() - .eq(SuperAgentDocument::getKnowledgeScopeCode, knowledgeScopeCode) + .in(SuperAgentDocument::getId, documentIds) + .eq(SuperAgentDocument::getKnowledgeBaseId, knowledgeBaseId) .eq(SuperAgentDocument::getIndexStatus, DocumentIndexStatusEnum.BUILD_SUCCESS.getCode()) .eq(SuperAgentDocument::getStatus, BusinessStatus.YES.getCode()) .isNotNull(SuperAgentDocument::getLastIndexTaskId) @@ -273,13 +328,14 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { long buildStartedNanos = System.nanoTime(); log.info("开始构建 RAPTOR dataset-level 摘要树,scopeKey={}, documentCount={}, originalChunkCount={}, inputMode={}, inputCount={}, reusableSummaryCount={}", scopeKey, sourceDocumentIds.size(), chunks.size(), datasetInputs.inputMode(), datasetInputs.inputs().size(), reusableSummaryNodes.size()); - RagToolsRaptorBuildResponse response = ragToolsClient.buildRaptor(buildDatasetRequest(DATASET_DOCUMENT_ID, DATASET_TASK_ID, datasetInputs)); + RagToolsRaptorBuildResponse response = ragToolsClient.buildRaptor(buildDatasetRequest(DATASET_DOCUMENT_ID, DATASET_TASK_ID, datasetInputs, raptorOptions)); if (response == null) { throw new IllegalStateException("Python RAPTOR dataset-level 构建接口返回为空。"); } - logLlmSummaryFallbacks(DATASET_DOCUMENT_ID, DATASET_TASK_ID, response.getNodes()); - RaptorQualityReport sourceQualityReport = raptorQualityService.evaluatePythonNodes(response.getNodes(), qualityFloor()); - Map idMap = allocateNodeIds(response.getNodes()); + logLlmSummaryFallbacks(DATASET_DOCUMENT_ID, DATASET_TASK_ID, response.getNodes(), Boolean.TRUE.equals(raptorOptions.getRaptorLlmSummaryEnabled())); + double qualityFloor = qualityFloor(raptorOptions); + RaptorQualityReport sourceQualityReport = raptorQualityService.evaluatePythonNodes(response.getNodes(), qualityFloor); + Map idMap = allocateNodeIds(response.getNodes(), qualityFloor); List nodes = buildNodeEntities( DATASET_DOCUMENT_ID, DATASET_TASK_ID, @@ -289,14 +345,15 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { idMap, sourceDocumentIds, sourceTaskIds, - datasetInputs + datasetInputs, + qualityFloor ); if (CollUtil.isEmpty(nodes)) { log.info("RAPTOR dataset-level 构建没有达到质量阈值的摘要节点,scopeKey={}, qualityFloor={}", - scopeKey, qualityFloor()); + scopeKey, qualityFloor); return RaptorBuildResult.builder() .sourceQualityReport(sourceQualityReport) - .savedQualityReport(raptorQualityService.evaluatePythonNodes(List.of(), qualityFloor())) + .savedQualityReport(raptorQualityService.evaluatePythonNodes(List.of(), qualityFloor)) .build(); } MybatisBatchExecutor.insertBatch(SuperAgentRaptorNode.class, nodes); @@ -321,7 +378,7 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { .reusableSummaryInputCount(datasetInputs.reusableSummaryInputCount()) .originalChunkInputCount(datasetInputs.originalChunkInputCount()) .sourceQualityReport(sourceQualityReport) - .savedQualityReport(raptorQualityService.evaluate(nodes, qualityFloor())) + .savedQualityReport(raptorQualityService.evaluate(nodes, qualityFloor)) .build(); log.info("RAPTOR dataset-level 摘要树构建完成: scopeKey={}, documentCount={}, inputMode={}, inputCount={}, nodeCount={}, levelCount={}, sourceChunkCount={}, costMillis={}", scopeKey, sourceDocumentIds.size(), result.getInputMode(), result.getInputCount(), result.getNodeCount(), result.getLevelCount(), result.getSourceChunkCount(), elapsedMillis(buildStartedNanos)); @@ -378,13 +435,16 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { } } - private RagToolsRaptorBuildRequest buildRequest(Long documentId, Long taskId, List chunks) { + private RagToolsRaptorBuildRequest buildRequest(Long documentId, + Long taskId, + List chunks, + KnowledgeBaseIndexingOptions.RaptorBuildOptions raptorOptions) { RagToolsRaptorBuildRequest request = new RagToolsRaptorBuildRequest(); request.setDocumentId(documentId); request.setTaskId(taskId); - request.setMaxClusterSize(maxClusterSize); - request.setMaxLevels(maxLevels); - request.setLlmSummaryEnabled(Boolean.TRUE.equals(llmSummaryEnabled)); + request.setMaxClusterSize(raptorOptions.getRaptorMaxClusterSize()); + request.setMaxLevels(raptorOptions.getRaptorMaxLevels()); + request.setLlmSummaryEnabled(Boolean.TRUE.equals(raptorOptions.getRaptorLlmSummaryEnabled())); List requestChunks = new ArrayList<>(); for (SuperAgentDocumentChunk chunk : chunks) { @@ -413,13 +473,14 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { private RagToolsRaptorBuildRequest buildDatasetRequest(Long documentId, Long taskId, - RaptorDatasetBuildSupport.DatasetInputs datasetInputs) { + RaptorDatasetBuildSupport.DatasetInputs datasetInputs, + KnowledgeBaseIndexingOptions.RaptorBuildOptions raptorOptions) { RagToolsRaptorBuildRequest request = new RagToolsRaptorBuildRequest(); request.setDocumentId(documentId); request.setTaskId(taskId); - request.setMaxClusterSize(maxClusterSize); - request.setMaxLevels(maxLevels); - request.setLlmSummaryEnabled(Boolean.TRUE.equals(llmSummaryEnabled)); + request.setMaxClusterSize(raptorOptions.getRaptorMaxClusterSize()); + request.setMaxLevels(raptorOptions.getRaptorMaxLevels()); + request.setLlmSummaryEnabled(Boolean.TRUE.equals(raptorOptions.getRaptorLlmSummaryEnabled())); List requestChunks = new ArrayList<>(); int chunkNo = 1; @@ -445,13 +506,14 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { return metadata; } - private Map allocateNodeIds(List extractedNodes) { + private Map allocateNodeIds(List extractedNodes, + double qualityFloor) { Map idMap = new LinkedHashMap<>(); if (CollUtil.isEmpty(extractedNodes)) { return idMap; } for (RagToolsRaptorBuildResponse.Node node : extractedNodes) { - if (node != null && StrUtil.isNotBlank(node.getId()) && summaryQualityScore(node) >= qualityFloor()) { + if (node != null && StrUtil.isNotBlank(node.getId()) && summaryQualityScore(node) >= qualityFloor) { idMap.put(node.getId(), uidGenerator.getUid()); } } @@ -466,7 +528,8 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { Map idMap, List sourceDocumentIds, List sourceTaskIds, - RaptorDatasetBuildSupport.DatasetInputs datasetInputs) { + RaptorDatasetBuildSupport.DatasetInputs datasetInputs, + double qualityFloor) { if (CollUtil.isEmpty(extractedNodes)) { return List.of(); } @@ -520,9 +583,9 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { "summaryQualityScore", summaryQualityScore(extracted), "sourceMetadata", extracted.getMetadata() ))); - if (summaryQualityScore(extracted) < qualityFloor()) { + if (summaryQualityScore(extracted) < qualityFloor) { log.info("跳过低质量 RAPTOR 摘要节点: documentId={}, taskId={}, sourceNodeId={}, qualityScore={}, floor={}", - documentId, taskId, extracted.getId(), summaryQualityScore(extracted), qualityFloor()); + documentId, taskId, extracted.getId(), summaryQualityScore(extracted), qualityFloor); continue; } node.setStatus(BusinessStatus.YES.getCode()); @@ -685,6 +748,30 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { return Math.min(configured, EMBEDDING_BATCH_SIZE_LIMIT); } + private KnowledgeBaseIndexingOptions.RaptorBuildOptions raptorBuildOptionsByDocumentId(Long documentId) { + if (indexingConfigResolver == null) { + return defaultRaptorBuildOptions(); + } + return indexingConfigResolver.resolveByDocumentId(documentId).getRaptor(); + } + + private KnowledgeBaseIndexingOptions.RaptorBuildOptions raptorBuildOptionsByKnowledgeBaseId(Long knowledgeBaseId) { + if (indexingConfigResolver == null) { + return defaultRaptorBuildOptions(); + } + return indexingConfigResolver.resolveByKnowledgeBaseId(knowledgeBaseId).getRaptor(); + } + + private KnowledgeBaseIndexingOptions.RaptorBuildOptions defaultRaptorBuildOptions() { + return KnowledgeBaseIndexingOptions.fromDefaults( + properties, + maxClusterSize, + maxLevels, + llmSummaryEnabled, + summaryQualityFloor + ).getRaptor(); + } + private long elapsedMillis(long startedNanos) { return (System.nanoTime() - startedNanos) / 1_000_000L; } @@ -728,8 +815,11 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { return metadata; } - private void logLlmSummaryFallbacks(Long documentId, Long taskId, List extractedNodes) { - if (!Boolean.TRUE.equals(llmSummaryEnabled) || CollUtil.isEmpty(extractedNodes)) { + private void logLlmSummaryFallbacks(Long documentId, + Long taskId, + List extractedNodes, + boolean llmSummaryRequested) { + if (!llmSummaryRequested || CollUtil.isEmpty(extractedNodes)) { return; } for (RagToolsRaptorBuildResponse.Node node : extractedNodes) { @@ -778,11 +868,12 @@ public class RaptorBuildServiceImpl implements RaptorBuildService { return Math.max(0D, Math.min(1D, extracted.getQualityScore())); } - private double qualityFloor() { - if (summaryQualityFloor == null) { + private double qualityFloor(KnowledgeBaseIndexingOptions.RaptorBuildOptions raptorOptions) { + Double configuredFloor = raptorOptions == null ? summaryQualityFloor : raptorOptions.getRaptorSummaryQualityFloor(); + if (configuredFloor == null) { return 0.42D; } - return Math.max(0D, Math.min(1D, summaryQualityFloor)); + return Math.max(0D, Math.min(1D, configuredFloor)); } private Object metadataValue(String json, String key) { diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/RaptorSearchServiceImpl.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/RaptorSearchServiceImpl.java index 46225fa4237bbd7c30dcb55db4a3645490bdd972..d18e6cf500100b3ede341339d5fac06f9768e7fe 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/RaptorSearchServiceImpl.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/impl/RaptorSearchServiceImpl.java @@ -42,6 +42,12 @@ import java.util.stream.IntStream; @Service public class RaptorSearchServiceImpl implements RaptorSearchService { + private static final String SOURCE_STATUS_SOURCE_CHUNK = "SOURCE_CHUNK"; + + private static final String SOURCE_STATUS_SOURCE_PARENT_BLOCK = "SOURCE_PARENT_BLOCK"; + + private static final String SOURCE_STATUS_SUMMARY_ONLY = "SUMMARY_ONLY"; + private static final String RAPTOR_RETRIEVE_SQL_TEMPLATE = """ SELECT id, @@ -112,20 +118,28 @@ public class RaptorSearchServiceImpl implements RaptorSearchService { List terms = extractTerms(question); Set allowedDocumentIds = new LinkedHashSet<>(documentIds); Set allowedTaskIds = new LinkedHashSet<>(taskIds); - Map resultMap = new LinkedHashMap<>(); + Map resultMap = new LinkedHashMap<>(); for (RaptorNodeHit hit : nodeHits) { SuperAgentRaptorNode node = nodeMap.get(hit.nodeId()); if (node == null) { continue; } List chunks = loadSourceChunks(node, sourceChunkTopK, allowedDocumentIds, allowedTaskIds); + if (chunks.isEmpty()) { + RaptorSearchResult summaryOnly = toSummaryOnlyResult(node, hit.score(), allowedDocumentIds, allowedTaskIds); + if (summaryOnly != null) { + resultMap.merge(resultKey(summaryOnly), summaryOnly, + (left, right) -> left.getScore() >= right.getScore() ? left : right); + } + continue; + } for (SuperAgentDocumentChunk chunk : chunks) { if (chunk == null || chunk.getId() == null) { continue; } double score = hit.score() + chunkEvidenceBoost(chunk, terms); RaptorSearchResult result = toResult(node, chunk, score); - resultMap.merge(chunk.getId(), result, + resultMap.merge(resultKey(result), result, (left, right) -> left.getScore() >= right.getScore() ? left : right); } } @@ -274,6 +288,7 @@ public class RaptorSearchServiceImpl implements RaptorSearchService { .raptorNodeTitle(node.getTitle()) .raptorNodeLevel(node.getNodeLevel()) .raptorSummary(node.getSummary()) + .sourceStatus(SOURCE_STATUS_SOURCE_CHUNK) .chunkId(chunk.getId()) .parentBlockId(chunk.getParentBlockId()) .chunkNo(chunk.getChunkNo()) @@ -288,6 +303,66 @@ public class RaptorSearchServiceImpl implements RaptorSearchService { .build(); } + private RaptorSearchResult toSummaryOnlyResult(SuperAgentRaptorNode node, + double score, + Set allowedDocumentIds, + Set allowedTaskIds) { + Long documentId = firstAllowedId(node.getDocumentId(), node.getSourceDocumentIdsJson(), allowedDocumentIds); + Long taskId = firstAllowedId(node.getTaskId(), node.getSourceTaskIdsJson(), allowedTaskIds); + if (documentId == null || taskId == null) { + return null; + } + Long parentBlockId = readLongList(node.getSourceParentBlockIdsJson()).stream() + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + String sourceStatus = parentBlockId == null ? SOURCE_STATUS_SUMMARY_ONLY : SOURCE_STATUS_SOURCE_PARENT_BLOCK; + return RaptorSearchResult.builder() + .documentId(documentId) + .taskId(taskId) + .raptorNodeId(node.getId()) + .raptorNodeTitle(node.getTitle()) + .raptorNodeLevel(node.getNodeLevel()) + .raptorSummary(node.getSummary()) + .sourceStatus(sourceStatus) + .parentBlockId(parentBlockId) + .title(node.getTitle()) + .sectionPath(node.getSectionPath()) + .pageRange(node.getPageRange()) + .sourceBlockIds(joinLongList(readLongList(node.getSourceParentBlockIdsJson()))) + .score(score) + .build(); + } + + private Long firstAllowedId(Long primaryId, String sourceIdsJson, Set allowedIds) { + if (primaryId != null && allowedIds.contains(primaryId)) { + return primaryId; + } + return readLongList(sourceIdsJson).stream() + .filter(allowedIds::contains) + .findFirst() + .orElse(null); + } + + private String resultKey(RaptorSearchResult result) { + if (result.getChunkId() != null) { + return "chunk:" + result.getChunkId(); + } + if (result.getParentBlockId() != null) { + return "parent:" + result.getRaptorNodeId() + ":" + result.getParentBlockId(); + } + return "summary:" + result.getRaptorNodeId() + ":" + result.getDocumentId() + ":" + result.getTaskId(); + } + + private String joinLongList(List values) { + if (values == null || values.isEmpty()) { + return ""; + } + return values.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + } + private double chunkEvidenceBoost(SuperAgentDocumentChunk chunk, List terms) { if (chunk == null || terms.isEmpty()) { return 0D; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/keyword/ElasticsearchDocumentKeywordSearchGateway.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/keyword/ElasticsearchDocumentKeywordSearchGateway.java index 808aa92695378eb0f5d72bc30756059b01d03dde..be5f8db32c7ce9a892d2c199d136bca63cec8734 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/keyword/ElasticsearchDocumentKeywordSearchGateway.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/service/keyword/ElasticsearchDocumentKeywordSearchGateway.java @@ -214,24 +214,9 @@ public class ElasticsearchDocumentKeywordSearchGateway implements DocumentKeywor bool.should(should -> should.multiMatch(multiMatch -> multiMatch .query(retrievalQuery) .fields("title^10", "sectionPath^8", "contentWithWeight^6", "questions^5", - "keywords^5", "documentName^4", "knowledgeScopeName^3", "chunkText^2") + "keywords^5", "documentName^4", "chunkText^2") .type(TextQueryType.BestFields) )); - if (filters != null && CollUtil.isNotEmpty(filters.getBusinessCategoryHints())) { - - bool.should(should -> should.multiMatch(multiMatch -> multiMatch - .query(String.join(" ", filters.getBusinessCategoryHints())) - .fields("businessCategory^5", "knowledgeScopeName^2") - .type(TextQueryType.BestFields) - )); - } - if (filters != null && CollUtil.isNotEmpty(filters.getDocumentTagHints())) { - bool.should(should -> should.multiMatch(multiMatch -> multiMatch - .query(String.join(" ", filters.getDocumentTagHints())) - .fields("documentTags^4", "keywords^3", "documentName^2", "contentWithWeight^2", "chunkText") - .type(TextQueryType.BestFields) - )); - } if (filters != null && CollUtil.isNotEmpty(filters.getDocumentNameHints())) { bool.should(should -> should.multiMatch(multiMatch -> multiMatch .query(String.join(" ", filters.getDocumentNameHints())) @@ -249,7 +234,7 @@ public class ElasticsearchDocumentKeywordSearchGateway implements DocumentKeywor if (CollUtil.isNotEmpty(queryContextHints)) { bool.should(should -> should.multiMatch(multiMatch -> multiMatch .query(String.join(" ", queryContextHints)) - .fields("documentName^2", "knowledgeScopeName^2", "title^3", "sectionPath^2", + .fields("documentName^2", "title^3", "sectionPath^2", "keywords^3", "questions^3", "contentWithWeight^2", "chunkText") .type(TextQueryType.BestFields) )); @@ -346,6 +331,8 @@ public class ElasticsearchDocumentKeywordSearchGateway implements DocumentKeywor .parentBlockId(chunk.getParentBlockId()) .chunkNo(chunk.getChunkNo()) .documentName(document == null ? "" : safeText(document.getDocumentName())) + .knowledgeBaseId(document == null ? null : document.getKnowledgeBaseId()) + .knowledgeBaseName(document == null ? "" : safeText(document.getKnowledgeBaseName())) .sectionPath(safeText(chunk.getSectionPath())) .structureNodeId(chunk.getStructureNodeId()) .structureNodeType(chunk.getStructureNodeType()) @@ -355,10 +342,6 @@ public class ElasticsearchDocumentKeywordSearchGateway implements DocumentKeywor .pageRange(safeText(chunk.getPageRange())) .bboxJson(safeText(chunk.getBboxJson())) .sourceBlockIds(safeText(chunk.getSourceBlockIds())) - .knowledgeScopeCode(document == null ? "" : safeText(document.getKnowledgeScopeCode())) - .knowledgeScopeName(document == null ? "" : safeText(document.getKnowledgeScopeName())) - .businessCategory(document == null ? "" : safeText(document.getBusinessCategory())) - .documentTags(splitTags(document == null ? "" : document.getDocumentTags())) .contentWithWeight(safeText(chunk.getContentWithWeight())) .chunkType(safeText(chunk.getChunkType())) .title(safeText(chunk.getTitle())) @@ -389,10 +372,8 @@ public class ElasticsearchDocumentKeywordSearchGateway implements DocumentKeywor metadata.put(DocumentKnowledgeMetadataKeys.BBOX_JSON, safeText(source.getBboxJson())); metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_BLOCK_IDS, safeText(source.getSourceBlockIds())); metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, safeText(source.getDocumentName())); - metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_SCOPE_CODE, safeText(source.getKnowledgeScopeCode())); - metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_SCOPE_NAME, safeText(source.getKnowledgeScopeName())); - metadata.put(DocumentKnowledgeMetadataKeys.BUSINESS_CATEGORY, safeText(source.getBusinessCategory())); - metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_TAGS, String.join(",", source.getDocumentTags())); + putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, source.getKnowledgeBaseId()); + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, safeText(source.getKnowledgeBaseName())); metadata.put(DocumentKnowledgeMetadataKeys.CONTENT_WITH_WEIGHT, safeText(source.getContentWithWeight())); metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, safeText(source.getChunkType())); metadata.put(DocumentKnowledgeMetadataKeys.TITLE, safeText(source.getTitle())); @@ -426,17 +407,6 @@ public class ElasticsearchDocumentKeywordSearchGateway implements DocumentKeywor return topK <= 0 ? 10 : Math.min(topK, 50); } - private List splitTags(String documentTags) { - if (StrUtil.isBlank(documentTags)) { - return List.of(); - } - return Arrays.stream(documentTags.split(",")) - .map(String::trim) - .filter(StrUtil::isNotBlank) - .distinct() - .toList(); - } - private List readStringArray(String text) { if (StrUtil.isBlank(text)) { return List.of(); diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/DocumentKnowledgeMetadataKeys.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/DocumentKnowledgeMetadataKeys.java index 9104e290e2873fbad12123c346302dde2637af1d..a418cd03599b1a7f0466ee28584f0131e0c8b737 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/DocumentKnowledgeMetadataKeys.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/DocumentKnowledgeMetadataKeys.java @@ -15,6 +15,8 @@ public final class DocumentKnowledgeMetadataKeys { public static final String SCORE = "score"; public static final String DOCUMENT_ID = "documentId"; public static final String DOCUMENT_NAME = "documentName"; + public static final String KNOWLEDGE_BASE_ID = "knowledgeBaseId"; + public static final String KNOWLEDGE_BASE_NAME = "knowledgeBaseName"; public static final String TASK_ID = "taskId"; public static final String PARENT_BLOCK_ID = "parentBlockId"; public static final String PARENT_BLOCK_NO = "parentBlockNo"; @@ -33,10 +35,6 @@ public final class DocumentKnowledgeMetadataKeys { public static final String PAGE_RANGE = "pageRange"; public static final String BBOX_JSON = "bboxJson"; public static final String SOURCE_BLOCK_IDS = "sourceBlockIds"; - public static final String KNOWLEDGE_SCOPE_CODE = "knowledgeScopeCode"; - public static final String KNOWLEDGE_SCOPE_NAME = "knowledgeScopeName"; - public static final String BUSINESS_CATEGORY = "businessCategory"; - public static final String DOCUMENT_TAGS = "documentTags"; public static final String TITLE = "title"; public static final String URL = "url"; public static final String TOOL_NAME = "toolName"; @@ -53,6 +51,23 @@ public final class DocumentKnowledgeMetadataKeys { public static final String RERANK_ERROR = "rerankError"; public static final String RERANK_CANDIDATE_COUNT = "rerankCandidateCount"; public static final String RERANK_TOP_K = "rerankTopK"; + public static final String FINAL_SELECTION_REASON = "finalSelectionReason"; + public static final String FINAL_SELECTION_RESERVE_TYPE = "finalSelectionReserveType"; + public static final String SOURCE_STRUCTURE_ANCHOR = "sourceStructureAnchor"; + public static final String STRUCTURE_ANCHOR_MATCH_TYPE = "structureAnchorMatchType"; + public static final String STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW = "structureAnchorBypassReserveWindow"; + public static final String STRUCTURE_ANCHOR_RAW_BODY = "structureAnchorRawBody"; + public static final String STRUCTURE_BODY_RESOLVED_FROM = "structureBodyResolvedFrom"; + public static final String STRUCTURE_BODY_CANDIDATE_KIND = "structureBodyCandidateKind"; + public static final String EVIDENCE_APPLICABILITY_STATUS = "evidenceApplicabilityStatus"; + public static final String EVIDENCE_APPLICABILITY_REASON = "evidenceApplicabilityReason"; + public static final String EVIDENCE_ROLE = "evidenceRole"; + public static final String EXPECTED_EVIDENCE_ROLES = "expectedEvidenceRoles"; + public static final String CONTEXT_IDENTITY = "contextIdentity"; + public static final String CITATION_IDENTITY = "citationIdentity"; + public static final String CITATION_EVIDENCE_TYPE = "citationEvidenceType"; + public static final String CONTEXT_ONLY = "contextOnly"; + public static final String SOURCE_EVIDENCE_RESOLVED = "sourceEvidenceResolved"; public static final String RETRIEVAL_INTENT = "retrievalIntent"; public static final String CHANNEL_WEIGHT = "channelWeight"; public static final String TABLE_ID = "tableId"; @@ -85,6 +100,7 @@ public final class DocumentKnowledgeMetadataKeys { public static final String KG_RELATION_GROUP_EVIDENCE_COUNT = "kgRelationGroupEvidenceCount"; public static final String KG_RELATION_GROUP_DOCUMENT_COUNT = "kgRelationGroupDocumentCount"; public static final String KG_EVIDENCE_ID = "kgEvidenceId"; + public static final String KG_EVIDENCE_GROUNDING_LEVEL = "kgEvidenceGroundingLevel"; public static final String KG_GRAPH_PATH = "kgGraphPath"; public static final String KG_HOP_COUNT = "kgHopCount"; public static final String KG_QUERY_PLAN_SOURCE = "kgQueryPlanSource"; @@ -96,6 +112,7 @@ public final class DocumentKnowledgeMetadataKeys { public static final String KG_COMMUNITY_ID = "kgCommunityId"; public static final String KG_COMMUNITY_TITLE = "kgCommunityTitle"; public static final String KG_COMMUNITY_SUMMARY = "kgCommunitySummary"; + public static final String KG_COMMUNITY_SUMMARY_ONLY = "kgCommunitySummaryOnly"; public static final String KG_CROSS_DOCUMENT_COMMUNITY_KEY = "kgCrossDocumentCommunityKey"; public static final String KG_CROSS_DOCUMENT_COMMUNITY_ENTITY_COUNT = "kgCrossDocumentCommunityEntityCount"; public static final String KG_CROSS_DOCUMENT_COMMUNITY_RELATION_GROUP_COUNT = "kgCrossDocumentCommunityRelationGroupCount"; @@ -126,6 +143,7 @@ public final class DocumentKnowledgeMetadataKeys { KG_RELATION_GROUP_EVIDENCE_COUNT, KG_RELATION_GROUP_DOCUMENT_COUNT, KG_EVIDENCE_ID, + KG_EVIDENCE_GROUNDING_LEVEL, KG_GRAPH_PATH, KG_HOP_COUNT, KG_QUERY_PLAN_SOURCE, @@ -137,6 +155,7 @@ public final class DocumentKnowledgeMetadataKeys { KG_COMMUNITY_ID, KG_COMMUNITY_TITLE, KG_COMMUNITY_SUMMARY, + KG_COMMUNITY_SUMMARY_ONLY, KG_CROSS_DOCUMENT_COMMUNITY_KEY, KG_CROSS_DOCUMENT_COMMUNITY_ENTITY_COUNT, KG_CROSS_DOCUMENT_COMMUNITY_RELATION_GROUP_COUNT, @@ -156,6 +175,7 @@ public final class DocumentKnowledgeMetadataKeys { public static final String RAPTOR_NODE_TITLE = "raptorNodeTitle"; public static final String RAPTOR_NODE_LEVEL = "raptorNodeLevel"; public static final String RAPTOR_SUMMARY = "raptorSummary"; + public static final String RAPTOR_SOURCE_STATUS = "raptorSourceStatus"; private DocumentKnowledgeMetadataKeys() { } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/DocumentStructureSignalExtractor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/DocumentStructureSignalExtractor.java index 8521f1de1c3bfe8f206acd4465a2540ec0fb2a85..f88c469ed7481a56aa3e753df33cc2724aaf71ef 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/DocumentStructureSignalExtractor.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/DocumentStructureSignalExtractor.java @@ -35,6 +35,7 @@ public class DocumentStructureSignalExtractor { private static final Pattern VERSION_FOOTER_PATTERN = Pattern.compile(".*(?:\\bV\\d+(?:\\.\\d+)*\\b|版本|修订|Rev\\.?\\s*\\d+).*", Pattern.CASE_INSENSITIVE); private static final Pattern INLINE_EXPLICIT_STEP_BOUNDARY_PATTERN = Pattern.compile("(?=(?:第\\s*[0-9一二三四五六七八九十百]+\\s*步|步骤\\s*[0-9一二三四五六七八九十百]+)\\s*[::、.])"); private static final Pattern TABLE_SPLIT_PATTERN = Pattern.compile("\\|"); + private static final Pattern ORDERED_MARKER_PATTERN = Pattern.compile("(?:^|\\s)(\\d{1,2})[、.]\\s+"); private final DocumentManageProperties properties; private final DocumentLineClassifier documentLineClassifier; @@ -151,6 +152,10 @@ public class DocumentStructureSignalExtractor { return signal(lineNo, rawText, normalized, logicalLine.indentLevel(), DocumentStructureSignalKind.QUOTE, "", normalized, null, null, List.of("quote"), 0.88D); } + if (containsMultipleOrderedItems(normalized)) { + return signal(lineNo, rawText, normalized, logicalLine.indentLevel(), DocumentStructureSignalKind.LIST_ITEM, "", normalized, null, null, + List.of("collapsed-ordered-list"), 0.94D); + } Matcher checkbox = CHECKBOX_PATTERN.matcher(normalized); if (checkbox.matches()) { return signal(lineNo, rawText, normalized, logicalLine.indentLevel(), DocumentStructureSignalKind.LIST_ITEM, "", checkbox.group(1).trim(), null, null, @@ -527,7 +532,25 @@ public class DocumentStructureSignalExtractor { return false; } String previous = safeText(previousNonBlank.normalizedText()); - return previous.endsWith(":") || previous.endsWith(":"); + return previous.endsWith(":") + || previous.endsWith(":") + || MARKDOWN_HEADING_PATTERN.matcher(previous).matches() + || DECIMAL_HEADING_PATTERN.matcher(previous).matches() + || CHAPTER_PATTERN.matcher(previous).matches() + || APPENDIX_PATTERN.matcher(previous).matches(); + } + + private boolean containsMultipleOrderedItems(String text) { + String normalized = safeText(text).replace('\n', ' '); + Matcher matcher = ORDERED_MARKER_PATTERN.matcher(normalized); + int count = 0; + while (matcher.find()) { + count++; + if (count >= 2) { + return true; + } + } + return false; } private boolean isNeighborSequence(Integer itemIndex, diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/GraphRagTypedChunkMetadataSupport.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/GraphRagTypedChunkMetadataSupport.java index aae0e68c7517bd5f603eb994e01a70e58c537f8a..84469af9495ec688f0989af1cb0a106a3b2adc8f 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/GraphRagTypedChunkMetadataSupport.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/GraphRagTypedChunkMetadataSupport.java @@ -53,10 +53,38 @@ public class GraphRagTypedChunkMetadataSupport { metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, SOURCE_TYPE_GRAPH_RAG); Map sourceMetadata = readSourceMetadata(sourceBlockIds); for (Map.Entry entry : sourceMetadata.entrySet()) { - if (entry.getValue() != null) { - metadata.put(entry.getKey(), entry.getValue()); + if (entry.getValue() == null) { + continue; } + if (isKnowledgeBaseMetadataKey(entry.getKey())) { + mergeKnowledgeBaseMetadata(metadata, entry.getKey(), entry.getValue()); + continue; + } + metadata.put(entry.getKey(), entry.getValue()); + } + } + + private void mergeKnowledgeBaseMetadata(Map metadata, String key, Object sourceValue) { + if (!isMeaningfulMetadataValue(sourceValue)) { + return; + } + Object existingValue = metadata.get(key); + if (isMeaningfulMetadataValue(existingValue)) { + return; + } + metadata.put(key, sourceValue); + } + + private boolean isKnowledgeBaseMetadataKey(String key) { + return DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID.equals(key) + || DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME.equals(key); + } + + private boolean isMeaningfulMetadataValue(Object value) { + if (value == null) { + return false; } + return !(value instanceof String text) || !text.isBlank(); } public Map readSourceMetadata(String sourceBlockIds) { diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/KnowledgeBaseIndexingConfigResolver.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/KnowledgeBaseIndexingConfigResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..44061f9577385016e39554d39e8ec187f19184db --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/KnowledgeBaseIndexingConfigResolver.java @@ -0,0 +1,263 @@ +package org.javaup.ai.manage.support; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.javaup.ai.manage.config.DocumentManageProperties; +import org.javaup.ai.manage.data.SuperAgentDocument; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; +import org.javaup.ai.manage.mapper.SuperAgentDocumentMapper; +import org.javaup.ai.manage.model.KnowledgeBaseIndexingOptions; +import org.javaup.ai.manage.service.KnowledgeBaseManageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class KnowledgeBaseIndexingConfigResolver { + + private final DocumentManageProperties properties; + private final SuperAgentDocumentMapper documentMapper; + private final KnowledgeBaseManageService knowledgeBaseManageService; + private final ObjectMapper objectMapper; + + @Value("${app.chat.rag.raptor-max-cluster-size:6}") + private Integer defaultRaptorMaxClusterSize = 6; + + @Value("${app.chat.rag.raptor-max-levels:3}") + private Integer defaultRaptorMaxLevels = 3; + + @Value("${app.chat.rag.raptor-llm-summary-enabled:true}") + private Boolean defaultRaptorLlmSummaryEnabled = Boolean.TRUE; + + @Value("${app.chat.rag.raptor-summary-quality-floor:0.42}") + private Double defaultRaptorSummaryQualityFloor = 0.42D; + + public KnowledgeBaseIndexingConfigResolver(DocumentManageProperties properties) { + this(properties, null, null); + } + + @Autowired + public KnowledgeBaseIndexingConfigResolver(DocumentManageProperties properties, + SuperAgentDocumentMapper documentMapper, + KnowledgeBaseManageService knowledgeBaseManageService) { + this.properties = properties == null ? new DocumentManageProperties() : properties; + this.documentMapper = documentMapper; + this.knowledgeBaseManageService = knowledgeBaseManageService; + this.objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public KnowledgeBaseIndexingOptions resolve(SuperAgentDocument document) { + if (document == null || document.getKnowledgeBaseId() == null) { + return defaults(); + } + return resolveByKnowledgeBaseId(document.getKnowledgeBaseId()); + } + + public KnowledgeBaseIndexingOptions resolveByDocumentId(Long documentId) { + if (documentId == null || documentId <= 0 || documentMapper == null) { + return defaults(); + } + SuperAgentDocument document = documentMapper.selectById(documentId); + return resolve(document); + } + + public KnowledgeBaseIndexingOptions resolveByKnowledgeBaseId(Long knowledgeBaseId) { + if (knowledgeBaseId == null || knowledgeBaseId <= 0 || knowledgeBaseManageService == null) { + return defaults(); + } + try { + return resolve(knowledgeBaseManageService.requireEnabled(knowledgeBaseId)); + } + catch (RuntimeException exception) { + log.warn("知识库解析/索引构建配置读取失败,使用全局默认值: knowledgeBaseId={}, message={}", + knowledgeBaseId, exception.getMessage()); + return defaults(); + } + } + + public KnowledgeBaseIndexingOptions resolve(SuperAgentKnowledgeBase knowledgeBase) { + KnowledgeBaseIndexingOptions options = defaults(); + if (knowledgeBase == null) { + return options; + } + RetrievalConfig retrievalConfig = parseJson(knowledgeBase.getRetrievalConfigJson(), RetrievalConfig.class, knowledgeBase); + GraphRagConfig graphRagConfig = parseJson(knowledgeBase.getGraphRagConfigJson(), GraphRagConfig.class, knowledgeBase); + RaptorConfig raptorConfig = parseJson(knowledgeBase.getRaptorConfigJson(), RaptorConfig.class, knowledgeBase); + applyIndexing(options.getChunk(), retrievalConfig == null ? null : retrievalConfig.getIndexing()); + applyGraphRagBuild(options.getGraphRag(), graphRagConfig == null ? null : graphRagConfig.getBuild()); + applyRaptorBuild(options.getRaptor(), raptorConfig == null ? null : raptorConfig.getBuild()); + normalize(options); + return options; + } + + private KnowledgeBaseIndexingOptions defaults() { + KnowledgeBaseIndexingOptions options = KnowledgeBaseIndexingOptions.fromDefaults( + properties, + defaultRaptorMaxClusterSize, + defaultRaptorMaxLevels, + defaultRaptorLlmSummaryEnabled, + defaultRaptorSummaryQualityFloor + ); + normalize(options); + return options; + } + + private void applyIndexing(KnowledgeBaseIndexingOptions.ChunkOptions target, IndexingConfig source) { + if (target == null || source == null) { + return; + } + copyIfPresent(source.getChildRecursiveMaxChars(), target::setChildRecursiveMaxChars); + copyIfPresent(source.getChildRecursiveOverlapChars(), target::setChildRecursiveOverlapChars); + copyIfPresent(source.getChildSemanticMaxChars(), target::setChildSemanticMaxChars); + copyIfPresent(source.getChildSemanticMinChars(), target::setChildSemanticMinChars); + copyIfPresent(source.getChildSemanticSimilarityThreshold(), target::setChildSemanticSimilarityThreshold); + copyIfPresent(source.getParentBlockMaxChars(), target::setParentBlockMaxChars); + copyIfPresent(source.getParentBlockOverlapChars(), target::setParentBlockOverlapChars); + copyIfPresent(source.getParentSemanticMaxChars(), target::setParentSemanticMaxChars); + copyIfPresent(source.getParentSemanticMinChars(), target::setParentSemanticMinChars); + } + + private void applyGraphRagBuild(KnowledgeBaseIndexingOptions.GraphRagBuildOptions target, GraphRagBuildConfig source) { + if (target == null || source == null) { + return; + } + copyIfPresent(source.getGraphRagBuildEnabled(), target::setGraphRagBuildEnabled); + } + + private void applyRaptorBuild(KnowledgeBaseIndexingOptions.RaptorBuildOptions target, RaptorBuildConfig source) { + if (target == null || source == null) { + return; + } + copyIfPresent(source.getRaptorBuildEnabled(), target::setRaptorBuildEnabled); + copyIfPresent(source.getRaptorMaxClusterSize(), target::setRaptorMaxClusterSize); + copyIfPresent(source.getRaptorMaxLevels(), target::setRaptorMaxLevels); + copyIfPresent(source.getRaptorLlmSummaryEnabled(), target::setRaptorLlmSummaryEnabled); + copyIfPresent(source.getRaptorSummaryQualityFloor(), target::setRaptorSummaryQualityFloor); + } + + private void normalize(KnowledgeBaseIndexingOptions options) { + KnowledgeBaseIndexingOptions.ChunkOptions chunk = options.getChunk(); + chunk.setChildRecursiveMaxChars(clampInt(chunk.getChildRecursiveMaxChars(), 100, 8000, 800)); + chunk.setChildRecursiveOverlapChars(clampInt(chunk.getChildRecursiveOverlapChars(), 0, chunk.getChildRecursiveMaxChars() - 1, 120)); + chunk.setChildSemanticMaxChars(clampInt(chunk.getChildSemanticMaxChars(), 100, 8000, 700)); + chunk.setChildSemanticMinChars(clampInt(chunk.getChildSemanticMinChars(), 80, chunk.getChildSemanticMaxChars(), 240)); + chunk.setChildSemanticSimilarityThreshold(clampDouble(chunk.getChildSemanticSimilarityThreshold(), 0D, 1D, 0.18D)); + chunk.setParentBlockMaxChars(clampInt(chunk.getParentBlockMaxChars(), 300, 20000, 2200)); + chunk.setParentBlockOverlapChars(clampInt(chunk.getParentBlockOverlapChars(), 0, chunk.getParentBlockMaxChars() - 1, 180)); + chunk.setParentSemanticMaxChars(clampInt(chunk.getParentSemanticMaxChars(), 300, 20000, 1600)); + chunk.setParentSemanticMinChars(clampInt(chunk.getParentSemanticMinChars(), 120, chunk.getParentSemanticMaxChars(), 480)); + + KnowledgeBaseIndexingOptions.GraphRagBuildOptions graphRag = options.getGraphRag(); + graphRag.setGraphRagBuildEnabled(Boolean.TRUE.equals(graphRag.getGraphRagBuildEnabled())); + + KnowledgeBaseIndexingOptions.RaptorBuildOptions raptor = options.getRaptor(); + raptor.setRaptorBuildEnabled(Boolean.TRUE.equals(raptor.getRaptorBuildEnabled())); + raptor.setRaptorMaxClusterSize(clampInt(raptor.getRaptorMaxClusterSize(), 2, 50, 6)); + raptor.setRaptorMaxLevels(clampInt(raptor.getRaptorMaxLevels(), 1, 8, 3)); + raptor.setRaptorLlmSummaryEnabled(Boolean.TRUE.equals(raptor.getRaptorLlmSummaryEnabled())); + raptor.setRaptorSummaryQualityFloor(clampDouble(raptor.getRaptorSummaryQualityFloor(), 0D, 1D, 0.42D)); + } + + private T parseJson(String rawJson, Class targetClass, SuperAgentKnowledgeBase knowledgeBase) { + if (StrUtil.isBlank(rawJson)) { + return null; + } + try { + return objectMapper.readValue(rawJson, targetClass); + } + catch (JsonProcessingException | RuntimeException exception) { + log.warn("知识库解析/索引构建配置 JSON 解析失败,将忽略该段配置: knowledgeBaseId={}, knowledgeBaseName={}, targetClass={}", + knowledgeBase == null ? null : knowledgeBase.getId(), + knowledgeBase == null ? "" : knowledgeBase.getBaseName(), + targetClass == null ? "" : targetClass.getSimpleName(), + exception); + return null; + } + } + + private void copyIfPresent(T value, java.util.function.Consumer setter) { + if (value != null) { + setter.accept(value); + } + } + + private int clampInt(Integer value, int min, int max, int defaultValue) { + int candidate = value == null ? defaultValue : value; + int effectiveMax = Math.max(min, max); + return Math.min(Math.max(candidate, min), effectiveMax); + } + + private double clampDouble(Double value, double min, double max, double defaultValue) { + double candidate = value == null ? defaultValue : value; + if (!Double.isFinite(candidate)) { + candidate = defaultValue; + } + return Math.min(Math.max(candidate, min), max); + } + + @Data + private static class RetrievalConfig { + + private IndexingConfig indexing; + } + + @Data + private static class IndexingConfig { + + private Integer childRecursiveMaxChars; + + private Integer childRecursiveOverlapChars; + + private Integer childSemanticMaxChars; + + private Integer childSemanticMinChars; + + private Double childSemanticSimilarityThreshold; + + private Integer parentBlockMaxChars; + + private Integer parentBlockOverlapChars; + + private Integer parentSemanticMaxChars; + + private Integer parentSemanticMinChars; + } + + @Data + private static class GraphRagConfig { + + private GraphRagBuildConfig build; + } + + @Data + private static class GraphRagBuildConfig { + + private Boolean graphRagBuildEnabled; + } + + @Data + private static class RaptorConfig { + + private RaptorBuildConfig build; + } + + @Data + private static class RaptorBuildConfig { + + private Boolean raptorBuildEnabled; + + private Integer raptorMaxClusterSize; + + private Integer raptorMaxLevels; + + private Boolean raptorLlmSummaryEnabled; + + private Double raptorSummaryQualityFloor; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/RaptorScopeSupport.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/RaptorScopeSupport.java index 06210c37aff9037827ca9f39547d74cd570f37e4..8cee7647408633d42a43d927976d3a4a4e21c470 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/RaptorScopeSupport.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/support/RaptorScopeSupport.java @@ -1,12 +1,10 @@ package org.javaup.ai.manage.support; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.StrUtil; import org.javaup.ai.manage.data.SuperAgentDocument; import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; public final class RaptorScopeSupport { @@ -23,8 +21,12 @@ public final class RaptorScopeSupport { return "document:" + documentId; } - public static String knowledgeScopeKey(String knowledgeScopeCode) { - return "knowledge:" + normalizeScopeCode(knowledgeScopeCode); + public static String knowledgeBaseScopeKey(Long knowledgeBaseId) { + return "kb:" + knowledgeBaseId; + } + + public static String knowledgeScopeKey(Long knowledgeBaseId, Long scopeId) { + return knowledgeBaseScopeKey(knowledgeBaseId) + ":scope:" + scopeId; } public static List searchScopeKeys(List documents) { @@ -33,23 +35,16 @@ public final class RaptorScopeSupport { } LinkedHashSet scopeKeys = new LinkedHashSet<>(); for (SuperAgentDocument document : documents) { - if (document == null || StrUtil.isBlank(document.getKnowledgeScopeCode())) { + if (document == null || document.getKnowledgeBaseId() == null) { continue; } - scopeKeys.add(knowledgeScopeKey(document.getKnowledgeScopeCode())); + scopeKeys.add(knowledgeBaseScopeKey(document.getKnowledgeBaseId())); } - scopeKeys.add(GLOBAL_SCOPE_KEY); return List.copyOf(scopeKeys); } public static boolean isDatasetScope(String scopeType) { - return SCOPE_TYPE_DATASET.equalsIgnoreCase(StrUtil.blankToDefault(scopeType, "")); + return SCOPE_TYPE_DATASET.equalsIgnoreCase(scopeType == null ? "" : scopeType); } - public static String normalizeScopeCode(String knowledgeScopeCode) { - return StrUtil.blankToDefault(knowledgeScopeCode, "") - .trim() - .toLowerCase(Locale.ROOT) - .replaceAll("[^a-z0-9._-]+", "_"); - } } diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/DocumentListItemVo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/DocumentListItemVo.java index 230b6099ca6991bf6ba9ce2366bcec9b1148480d..b992142ec7fb2ce87f3757bb32a92f35cc60744b 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/DocumentListItemVo.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/DocumentListItemVo.java @@ -47,13 +47,9 @@ public class DocumentListItemVo { private String parseErrorMsg; - private String knowledgeScopeCode; + private Long knowledgeBaseId; - private String knowledgeScopeName; - - private String businessCategory; - - private String documentTags; + private String knowledgeBaseName; private Long currentPlanId; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeBaseItemVo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeBaseItemVo.java new file mode 100644 index 0000000000000000000000000000000000000000..e178ae5616239405a2de25a2514a8572b7c26db8 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeBaseItemVo.java @@ -0,0 +1,35 @@ +package org.javaup.ai.manage.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KnowledgeBaseItemVo { + + private String id; + + private String baseName; + + private String description; + + private String embeddingModel; + + private String retrievalConfigJson; + + private String graphRagConfigJson; + + private String raptorConfigJson; + + private String metadataFilterJson; + + private String isDefault; + + private String sortOrder; + + private String documentCount; + + private String retrievableDocumentCount; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeBaseOptionVo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeBaseOptionVo.java new file mode 100644 index 0000000000000000000000000000000000000000..2a80d1cb27cf96ac216f488789b0bc68350487b1 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeBaseOptionVo.java @@ -0,0 +1,21 @@ +package org.javaup.ai.manage.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KnowledgeBaseOptionVo { + + private String id; + + private String baseName; + + private String description; + + private String isDefault; + + private String retrievableDocumentCount; +} diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeScopeItemVo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeScopeItemVo.java index 93fcae605153e4915598b65cde969ffcaa841869..2b320ca63b51b72f1eb1a7e9067eec79ca09701c 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeScopeItemVo.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeScopeItemVo.java @@ -16,11 +16,11 @@ public class KnowledgeScopeItemVo { private String id; - private String scopeCode; + private String knowledgeBaseId; private String scopeName; - private String parentScopeCode; + private String parentScopeId; private String description; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeTopicItemVo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeTopicItemVo.java index 7c0a3a6d9189ea74f0228806f72ad31bd0055476..b3831ea99a5cce201ba51c9abcd58eea3e51ae11 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeTopicItemVo.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/KnowledgeTopicItemVo.java @@ -16,11 +16,11 @@ public class KnowledgeTopicItemVo { private String id; - private String topicCode; + private String knowledgeBaseId; private String topicName; - private String scopeCode; + private String scopeId; private String description; diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/TopicDocumentRelationItemVo.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/TopicDocumentRelationItemVo.java index 67d0ec28146351e8e919dbe1466241d6932448c8..d1b1aed372af9e085a6d5a8d425a904e570e93c8 100644 --- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/TopicDocumentRelationItemVo.java +++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/manage/vo/TopicDocumentRelationItemVo.java @@ -14,19 +14,19 @@ import lombok.NoArgsConstructor; @AllArgsConstructor public class TopicDocumentRelationItemVo { - private String topicCode; + private String knowledgeBaseId; - private String documentId; + private String topicId; - private String documentName; + private String topicName; - private String knowledgeScopeCode; + private String scopeId; - private String knowledgeScopeName; + private String scopeName; - private String businessCategory; + private String documentId; - private String documentTags; + private String documentName; private String relationScore; diff --git a/super-agent-business/super-agent-business-chat/src/main/resources/application.yaml b/super-agent-business/super-agent-business-chat/src/main/resources/application.yaml index 86d218699aefb32c05b127ec774c41f8594594b1..f1dc11ecaeb421a5378c05166cd0dcf3218300b4 100644 --- a/super-agent-business/super-agent-business-chat/src/main/resources/application.yaml +++ b/super-agent-business/super-agent-business-chat/src/main/resources/application.yaml @@ -209,9 +209,9 @@ app: # 单轮最多拆分多少个子问题,避免一次提问被过度切碎。 max-sub-questions: 4 # 向量通道每个子问题默认召回数量。 - vector-top-k: 8 + vector-top-k: 10 # 关键词通道每个子问题默认召回数量。 - keyword-top-k: 8 + keyword-top-k: 10 # GraphRAG 通道每个子问题默认召回数量。 # 该通道基于 super_agent_kg_entity / relation / evidence / community, # 与 Document/Section/Item 文档结构图是两套概念。 @@ -242,10 +242,16 @@ app: # RAPTOR 摘要质量分低于该阈值时,Java 不保存该摘要节点,也不会写入向量或 BM25 索引。 raptor-summary-quality-floor: ${SUPER_AGENT_RAPTOR_SUMMARY_QUALITY_FLOOR:0.42} # 多通道合并后进入精排前,最多保留多少个候选片段。 - candidate-top-k: 10 + candidate-top-k: 40 + # 进入 rerank/cross-encoder 精排的候选数量。 + # 该值通常应大于 candidate-top-k 或与其保持一致,用于避免正确证据在精排前过早丢失。 + rerank-candidate-top-k: 24 + # final evidence policy 可以从 rerank 后尾部候选中补保留的窗口大小。 + # 用于保留同章节正文、GraphRAG quote、RAPTOR source chunk 等高价值证据。 + reserve-candidate-top-k: 30 # 最终注入 Prompt 的证据片段数量。 # 这一层已经是“融合/精排后的定稿证据数”,不是每个通道各自的 topK。 - final-top-k: 5 + final-top-k: 6 # 向量召回最小相似度,低于这个阈值的候选不会继续进入融合。 # 作用是防止“数据库总能返回 topK,但这些片段其实根本不够相关”。 min-vector-similarity: 0.45 @@ -419,6 +425,14 @@ app: # 语义相似度阈值。 # 用于判断相邻文本是否应该继续合并为同一个 chunk。 semantic-similarity-threshold: 0.18 + # 父块允许的最大字符数。父块是回答阶段的主要上下文单元。 + parent-block-max-chars: ${SUPER_AGENT_PARENT_BLOCK_MAX_CHARS:2200} + # 父块递归裁切时保留的重叠字符数。 + parent-block-overlap-chars: ${SUPER_AGENT_PARENT_BLOCK_OVERLAP_CHARS:180} + # 父块语义切分时的最大字符数。 + parent-semantic-max-chars: ${SUPER_AGENT_PARENT_SEMANTIC_MAX_CHARS:1600} + # 父块语义切分时的最小字符数。 + parent-semantic-min-chars: ${SUPER_AGENT_PARENT_SEMANTIC_MIN_CHARS:480} # 是否开启基于大模型的智能切块。 # 第一阶段默认关闭,避免成本和复杂度过高。 llm-enabled: false diff --git a/super-agent-business/super-agent-business-chat/src/main/resources/prompt/document-query-understanding.st b/super-agent-business/super-agent-business-chat/src/main/resources/prompt/document-query-understanding.st index 79cce5d8754fbc9ef2322bf59129d8b393229671..12da0f4f3a5924daf66eb2952cc2f6e567655c76 100644 --- a/super-agent-business/super-agent-business-chat/src/main/resources/prompt/document-query-understanding.st +++ b/super-agent-business/super-agent-business-chat/src/main/resources/prompt/document-query-understanding.st @@ -8,7 +8,10 @@ "queryType": "DOCUMENT_QA", "channels": ["GENERAL"], "entities": [], + "targetEntities": [], + "excludedEntities": [], "sectionAnchors": [], + "expectedEvidenceRoles": [], "tableOps": [], "negativeBoundary": false, "confidence": 0.82, @@ -32,6 +35,18 @@ channels 只能从以下值中选择: - GRAPH_RAG:GraphRAG 候选。 - RAPTOR:RAPTOR 摘要候选。 +expectedEvidenceRoles 只能从以下值中选择: +- SYMPTOM:用户问现象、表现、症状。 +- CAUSE:用户问原因、成因、为什么。 +- HANDLING_STEP:用户问处理步骤、处置流程、怎么处理。 +- CHECK_ORDER:用户问检查顺序、排查顺序。 +- THRESHOLD:用户问阈值、上限、下限。 +- RESPONSIBILITY:用户问负责人、责任、职责。 +- CONFIGURATION:用户问配置项、参数、策略项。 +- RELATION:用户问关系、依赖、影响。 +- SUMMARY:用户问总结、概述、摘要。 +- GENERAL:无法判断或普通问答。无法判断时也可以留空。 + 重要规则: 1. channels 是候选建议,不是硬路由。普通文档问答默认包含 GENERAL。 2. 不要因为出现“关系、总结、表格、步骤、检查顺序”等词就强制单通道。 @@ -42,7 +57,8 @@ channels 只能从以下值中选择: 7. GraphRAG 和 RAPTOR 只有在问题确实需要关系证据、跨文档关系、全局摘要时作为候选通道加入。 8. 表格问题只有在用户明确问表格数据、聚合、筛选、分组、排序时加入 TABLE。 9. confidence 使用 0 到 1 的小数。不确定时低于 0.72。 -10. 只返回 JSON,不要 Markdown,不要解释文本。 +10. expectedEvidenceRoles 是答案证据角色,不是检索硬路由;只输出上面的枚举。 +11. 只返回 JSON,不要 Markdown,不要解释文本。 原始问题: diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/DocumentScopeRetrievalChannelTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/DocumentScopeRetrievalChannelTest.java index 5241c6f6e73bd628546dead2539a15541f55b646..0126f335a80358d2b28aeab9d588fa5cf38b8a5c 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/DocumentScopeRetrievalChannelTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/DocumentScopeRetrievalChannelTest.java @@ -112,6 +112,11 @@ class DocumentScopeRetrievalChannelTest { return List.of(); } + @Override + public List listRetrievableDocumentsByKnowledgeBaseIds(java.util.Collection knowledgeBaseIds) { + return List.of(); + } + @Override public List vectorSearch(DocumentRetrieveRequest request) { this.vectorRequest = request; diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannelTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannelTest.java index 5ee70c79df77c46551ef659c5c63f289e49840e4..cf8e69950aae03e3c6c968d12e720bb79b8dae78 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannelTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannelTest.java @@ -118,7 +118,7 @@ class GraphRagRetrievalChannelTest { } @Test - void graphSearchUsesRetrievalQueryWithHistoryHintsButKeepsOriginalQuestionInEvidenceText() { + void graphSearchKeepsHistoryHintsOutOfFreshTopicQueryAndEvidenceText() { GraphRagSearchResult evidence = GraphRagSearchResult.builder() .documentId(100L) .taskId(900L) @@ -151,7 +151,9 @@ class GraphRagRetrievalChannelTest { RetrievalChannelResult result = channel.retrieve("这个相关部门是谁?", plan); - assertThat(graphRagSearchService.question).contains("这个相关部门是谁?", "审计系统", "AuditTrail", "权限审批"); + assertThat(graphRagSearchService.question) + .isEqualTo("这个相关部门是谁?") + .doesNotContain("审计系统", "AuditTrail", "权限审批"); assertThat(graphRagSearchService.documentIds).containsExactly(100L); assertThat(graphRagSearchService.taskIds).containsExactly(900L); assertThat(result.getDocuments()).hasSize(1); @@ -239,6 +241,80 @@ class GraphRagRetrievalChannelTest { .containsEntry(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 800L); } + @Test + void communitySummaryOnlyCandidateIsMarkedAsBackground() { + GraphRagSearchResult communitySummaryOnly = GraphRagSearchResult.builder() + .documentId(100L) + .taskId(900L) + .communityId(500L) + .communityTitle("跨文档社区摘要") + .communitySummary("社区摘要覆盖多个实体,但当前候选没有代表原文 quote。") + .crossDocumentCommunityKey("xdoc-community:summary-only") + .crossDocumentCommunityDocumentCount(2) + .crossDocumentCommunityRelationGroupCount(3) + .crossDocumentCommunityEvidenceCount(4) + .graphPath("跨文档社区:summary-only") + .score(0.88D) + .build(); + GraphRagRetrievalChannel channel = new GraphRagRetrievalChannel( + new StaticGraphRagSearchService(List.of(communitySummaryOnly)), + new StaticDocumentKnowledgeService(), + new ChatRagProperties(), + new DocumentRetrieveRequestFactory() + ); + + RetrievalChannelResult result = channel.retrieve( + "这个图谱社区能说明什么?", + ConversationExecutionPlan.builder().selectedDocumentId(100L).selectedTaskId(900L).build() + ); + + assertThat(result.getDocuments()).hasSize(1); + Document document = result.getDocuments().get(0); + assertThat(document.getId()).isEqualTo("graphrag-xcommunity-xdoc-community-summary-only-evidence-summary"); + assertThat(document.getText()) + .contains("社区报告边界") + .contains("不能单独支撑具体事实结论"); + assertThat(document.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL, "COMMUNITY_SUMMARY_ONLY") + .containsEntry(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY, true) + .doesNotContainKey(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID); + } + + @Test + void relationQuoteCandidateExposesGroundingLevel() { + GraphRagSearchResult relationEvidence = GraphRagSearchResult.builder() + .documentId(100L) + .taskId(900L) + .entityId(200L) + .entityName("PaymentService") + .relationId(300L) + .relationType("RESPONSIBLE_FOR") + .relatedEntityId(201L) + .relatedEntityName("OwnerTeam") + .evidenceId(400L) + .quoteText("PaymentService 由 OwnerTeam 负责维护。") + .sectionPath("服务职责") + .graphPath("一跳:PaymentService --RESPONSIBLE_FOR--> OwnerTeam") + .score(0.91D) + .build(); + GraphRagRetrievalChannel channel = new GraphRagRetrievalChannel( + new StaticGraphRagSearchService(List.of(relationEvidence)), + new StaticDocumentKnowledgeService(), + new ChatRagProperties(), + new DocumentRetrieveRequestFactory() + ); + + RetrievalChannelResult result = channel.retrieve( + "PaymentService 谁负责?", + ConversationExecutionPlan.builder().selectedDocumentId(100L).selectedTaskId(900L).build() + ); + + assertThat(result.getDocuments()).hasSize(1); + assertThat(result.getDocuments().get(0).getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL, "RELATION_STRONG_QUOTE") + .containsEntry(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY, false); + } + private record StaticGraphRagSearchService(List results) implements GraphRagSearchService { @Override @@ -286,13 +362,16 @@ class GraphRagRetrievalChannelTest { 100L, "星联智服全渠道客服平台上线与运营管理手册.md", 900L, - "", - "", - "", - "" + 1L, + "测试知识库" )); } + @Override + public List listRetrievableDocumentsByKnowledgeBaseIds(java.util.Collection knowledgeBaseIds) { + return listRetrievableDocuments(); + } + @Override public List vectorSearch(DocumentRetrieveRequest request) { return List.of(); diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannelTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannelTest.java new file mode 100644 index 0000000000000000000000000000000000000000..19285459a52c3870472e883df579192e6057d16e --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannelTest.java @@ -0,0 +1,108 @@ +package org.javaup.ai.chatagent.rag.retrieve.channel; + +import org.javaup.ai.chatagent.model.SearchReference; +import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.support.SearchReferenceMapper; +import org.javaup.ai.manage.model.DocumentRetrieveRequest; +import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.ai.manage.model.raptor.RaptorSearchResult; +import org.javaup.ai.manage.service.DocumentKnowledgeService; +import org.javaup.ai.manage.service.RaptorSearchService; +import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; + +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RaptorRetrievalChannelTest { + + @Test + void summaryOnlyResultIsMarkedAsWeakBackgroundEvidence() { + RaptorRetrievalChannel channel = new RaptorRetrievalChannel( + new StaticRaptorSearchService(List.of(RaptorSearchResult.builder() + .documentId(10L) + .taskId(20L) + .raptorNodeId(3001L) + .raptorNodeTitle("灰度上线观察摘要") + .raptorNodeLevel(1) + .raptorSummary("灰度上线需要观察核心质量指标。") + .sourceStatus("SUMMARY_ONLY") + .sectionPath("上线治理 / 灰度观察") + .score(0.82D) + .build())), + new StaticDocumentKnowledgeService(), + new ChatRagProperties() + ); + + RetrievalChannelResult result = channel.retrieve( + "总结灰度上线观察规则", + ConversationExecutionPlan.builder().selectedDocumentId(10L).selectedTaskId(20L).build() + ); + + assertThat(result.getDocuments()).hasSize(1); + Document document = result.getDocuments().get(0); + assertThat(document.getId()).isEqualTo("raptor-3001-summary"); + assertThat(document.getText()).contains("本证据仅作为摘要背景"); + assertThat(document.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID, 3001L) + .containsEntry(DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS, "SUMMARY_ONLY") + .containsEntry(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "RAPTOR_SUMMARY") + .containsEntry(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET, "灰度上线需要观察核心质量指标。"); + assertThat(document.getMetadata()).doesNotContainKey(DocumentKnowledgeMetadataKeys.CHUNK_ID); + + SearchReference reference = SearchReferenceMapper.fromDocument(document, 1, "总结灰度上线观察规则", 1); + assertThat(reference.getRaptorNodeId()).isEqualTo(3001L); + assertThat(reference.getRaptorSourceStatus()).isEqualTo("SUMMARY_ONLY"); + assertThat(reference.uniqueKey()).isEqualTo("RAPTOR:3001:SUMMARY_ONLY"); + } + + private record StaticRaptorSearchService(List results) implements RaptorSearchService { + + @Override + public List search(String question, + List documentIds, + List taskIds, + int topK, + int sourceChunkTopK) { + return results; + } + } + + private static class StaticDocumentKnowledgeService implements DocumentKnowledgeService { + + @Override + public List listRetrievableDocuments() { + return List.of(new KnowledgeDocumentDescriptor( + 10L, + "上线治理手册.md", + 20L, + 1L, + "测试知识库" + )); + } + + @Override + public List listRetrievableDocumentsByKnowledgeBaseIds(Collection knowledgeBaseIds) { + return listRetrievableDocuments(); + } + + @Override + public List vectorSearch(DocumentRetrieveRequest request) { + return List.of(); + } + + @Override + public List keywordSearch(DocumentRetrieveRequest request) { + return List.of(); + } + + @Override + public List elevateToParentBlocks(List childDocuments, int maxChars) { + return childDocuments; + } + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannelTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannelTest.java index ff53965b421df4f0b1f7f91d7c44680b1703030d..40b0770be7a95ca7b00c4f6214e1891c381a9db8 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannelTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannelTest.java @@ -149,13 +149,16 @@ class TableRetrievalChannelTest { 10L, "费用报销制度.xlsx", 20L, - "", - "", - "", - "" + 1L, + "测试知识库" )); } + @Override + public List listRetrievableDocumentsByKnowledgeBaseIds(java.util.Collection knowledgeBaseIds) { + return listRetrievableDocuments(); + } + @Override public List vectorSearch(DocumentRetrieveRequest request) { return List.of(); diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssemblerTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssemblerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..96fdedda4b33f17e1b4876b30efe96de34fe60a6 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssemblerTest.java @@ -0,0 +1,50 @@ +package org.javaup.ai.chatagent.rag.service; + +import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.AnswerHistoryContext; +import org.javaup.ai.chatagent.rag.model.EvidenceAnchor; +import org.javaup.ai.chatagent.rag.model.QueryType; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class AnswerHistoryContextAssemblerTest { + + @Test + void followUpUsesEvidenceAnchorsInsteadOfQuestionKeywords() { + ChatRagProperties properties = new ChatRagProperties(); + AnswerHistoryContextAssembler assembler = new AnswerHistoryContextAssembler(properties); + QueryUnderstandingResult understanding = QueryUnderstandingResult.builder() + .queryType(QueryType.FOLLOW_UP) + .confidence(0.88D) + .source("test") + .build(); + EvidenceAnchor anchor = EvidenceAnchor.builder() + .documentId(1L) + .documentName("doc.md") + .sectionPath("14.3.1") + .structureNodeId(1431L) + .parentBlockId(9001L) + .chunkId(8001L) + .snippet("上一轮最终证据") + .build(); + + AnswerHistoryContext context = assembler.assemble( + "第一项和第三项分别是什么?", + "", + understanding, + List.of(anchor) + ); + + assertThat(context.isFollowUpQuestion()).isTrue(); + assertThat(context.getEvidenceAnchors()).hasSize(1); + assertThat(context.getStructuredContext()) + .contains("14.3.1") + .contains("9001") + .contains("8001"); + assertThat(context.isEmpty()).isFalse(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestratorTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestratorTest.java index 314a00f195165b94a2eec86228ff59331efab646..24f7401f268f7da68585d08e41d96bbfa89e5ed9 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestratorTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestratorTest.java @@ -4,18 +4,24 @@ import org.javaup.ai.chatagent.model.memory.ConversationMemoryContext; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; import org.javaup.ai.chatagent.rag.model.DocumentNavigationDecision; +import org.javaup.ai.chatagent.rag.model.EvidenceAnchor; import org.javaup.ai.chatagent.rag.model.ExecutionMode; +import org.javaup.ai.chatagent.rag.model.QueryType; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; import org.javaup.ai.chatagent.rag.model.RagRewriteResult; import org.javaup.ai.chatagent.rag.model.RetrievalIntent; import org.javaup.ai.chatagent.service.ConversationMemoryService; import org.javaup.ai.chatagent.service.TaskInfo; import org.javaup.ai.chatagent.support.StreamEventMetadata; import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot; import org.javaup.ai.manage.model.route.DocumentRouteCandidate; +import org.javaup.ai.manage.model.route.KnowledgeRouteContext; import org.javaup.ai.manage.model.route.KnowledgeRouteDecision; import org.javaup.ai.manage.service.DocumentKnowledgeService; import org.javaup.ai.manage.service.KnowledgeRouteService; import org.javaup.enums.ChatQueryMode; +import org.javaup.enums.KnowledgeBaseSelectionMode; import org.junit.jupiter.api.Test; import org.springframework.ai.document.Document; @@ -95,15 +101,47 @@ class ChatPreparationOrchestratorTest { assertThat(plan.getClarificationReply()).contains("这个问题目前存在文档范围歧义"); } + @Test + void followUpPlanCarriesPreviousFinalEvidenceAnchor() { + ChatRagProperties properties = new ChatRagProperties(); + EvidenceAnchor anchor = EvidenceAnchor.builder() + .documentId(1001L) + .documentName("测试文档.md") + .sectionPath("14.3.1") + .structureNodeId(1431L) + .parentBlockId(9001L) + .chunkId(8001L) + .snippet("上一轮最终证据") + .build(); + + ChatPreparationOrchestrator orchestrator = new ChatPreparationOrchestrator( + properties, + new StaticConversationMemoryService(), + new AnswerHistoryContextAssembler(properties), + new StaticRewriteService(properties), + new FollowUpDocumentQuestionRouter(), + new StaticKnowledgeRouteService(new KnowledgeRouteDecision()), + new StaticDocumentKnowledgeService(), + new StaticConversationEvidenceAnchorService(List.of(anchor)) + ); + + ConversationExecutionPlan plan = orchestrator.prepare(documentTaskInfo("第一项和第三项分别是什么?")); + + assertThat(plan.getMode()).isEqualTo(ExecutionMode.RETRIEVAL); + assertThat(plan.getAnswerHistoryContext()).isNotNull(); + assertThat(plan.getAnswerHistoryContext().getEvidenceAnchors()).hasSize(1); + assertThat(plan.getAnswerHistoryContext().getStructuredContext()) + .contains("14.3.1") + .contains("9001") + .contains("8001"); + assertThat(plan.getNavigationDecision().getExecutionMode()).isEqualTo(ExecutionMode.RETRIEVAL); + } + private static DocumentRouteCandidate candidate(String documentId, String taskId, String name, double score) { return new DocumentRouteCandidate( documentId, name, taskId, - "scope-a", - "测试知识范围", - "", - "", BigDecimal.valueOf(score), "test" ); @@ -119,6 +157,54 @@ class ChatPreparationOrchestratorTest { null, "", null, + KnowledgeBaseSelectionSnapshot.builder() + .selectionMode(KnowledgeBaseSelectionMode.SELECTED) + .selectedKnowledgeBaseIds(List.of(1L)) + .selectedKnowledgeBaseNames(List.of("测试知识库")) + .allowedDocuments(List.of( + new KnowledgeDocumentDescriptor(1001L, "O6跨文档图谱-审计系统别名说明B.md", 2001L, 1L, "测试知识库"), + new KnowledgeDocumentDescriptor(1002L, "O6跨文档图谱-审计证据规范A.md", 2002L, 1L, "测试知识库") + )) + .allowedDocumentIds(List.of(1001L, 1002L)) + .allowedTaskIds(List.of(2001L, 2002L)) + .build(), + LocalDate.of(2026, 6, 26), + "2026年6月26日", + null, + null, + null, + null, + null, + new StreamEventMetadata("conversation-a", 1L), + "", + "", + List.of(), + List.of(), + Set.of(), + System.currentTimeMillis() + ); + } + + private static TaskInfo documentTaskInfo(String question) { + return new TaskInfo( + "conversation-a", + 1L, + question, + ChatQueryMode.DOCUMENT, + "trace-a", + 1001L, + "测试文档.md", + 2001L, + KnowledgeBaseSelectionSnapshot.builder() + .selectionMode(KnowledgeBaseSelectionMode.SELECTED) + .selectedKnowledgeBaseIds(List.of(1L)) + .selectedKnowledgeBaseNames(List.of("测试知识库")) + .allowedDocuments(List.of( + new KnowledgeDocumentDescriptor(1001L, "测试文档.md", 2001L, 1L, "测试知识库") + )) + .allowedDocumentIds(List.of(1001L)) + .allowedTaskIds(List.of(2001L)) + .build(), LocalDate.of(2026, 6, 26), "2026年6月26日", null, @@ -192,13 +278,22 @@ class ChatPreparationOrchestratorTest { private static class StaticDocumentQuestionRouter extends DocumentQuestionRouter { StaticDocumentQuestionRouter() { - super(null, null, null, null, null); + super(null, null, null); } @Override public DocumentNavigationDecision route(Long documentId, String originalQuestion, RagRewriteResult rewriteResult) { + return route(documentId, originalQuestion, rewriteResult, "", ""); + } + + @Override + public DocumentNavigationDecision route(Long documentId, + String originalQuestion, + RagRewriteResult rewriteResult, + String historySummary, + String answerRecentTranscript) { return DocumentNavigationDecision.builder() .executionMode(ExecutionMode.RETRIEVAL) .retrievalIntent(RetrievalIntent.GRAPH_RAG) @@ -210,6 +305,49 @@ class ChatPreparationOrchestratorTest { } } + private static class FollowUpDocumentQuestionRouter extends DocumentQuestionRouter { + + FollowUpDocumentQuestionRouter() { + super(null, null, null); + } + + @Override + public DocumentNavigationDecision route(Long documentId, + String originalQuestion, + RagRewriteResult rewriteResult, + String historySummary, + String answerRecentTranscript) { + return DocumentNavigationDecision.builder() + .executionMode(ExecutionMode.RETRIEVAL) + .retrievalIntent(RetrievalIntent.GENERAL) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.FOLLOW_UP) + .confidence(0.88D) + .source("test") + .build()) + .retrievalPlan(new org.javaup.ai.chatagent.rag.model.RetrievalQuestionPlan( + rewriteResult.getRewrittenQuestion(), + rewriteResult.getSubQuestions() + )) + .build(); + } + } + + private static class StaticConversationEvidenceAnchorService extends ConversationEvidenceAnchorService { + + private final List anchors; + + StaticConversationEvidenceAnchorService(List anchors) { + super(null); + this.anchors = anchors; + } + + @Override + public List loadRecentEvidenceAnchors(String conversationId, int limit) { + return anchors.stream().limit(Math.max(0, limit)).toList(); + } + } + private static class StaticKnowledgeRouteService implements KnowledgeRouteService { private final KnowledgeRouteDecision decision; @@ -219,7 +357,7 @@ class ChatPreparationOrchestratorTest { } @Override - public KnowledgeRouteDecision route(String question, String rewriteQuestion) { + public KnowledgeRouteDecision route(KnowledgeRouteContext context) { return decision; } @@ -227,15 +365,13 @@ class ChatPreparationOrchestratorTest { public void recordShadowRoute(String conversationId, long exchangeId, Long selectedDocumentId, - String question, - String rewriteQuestion) { + KnowledgeRouteContext context) { } @Override public void recordAutoRoute(String conversationId, long exchangeId, - String question, - String rewriteQuestion, + KnowledgeRouteContext context, KnowledgeRouteDecision decision) { } } @@ -247,6 +383,11 @@ class ChatPreparationOrchestratorTest { return List.of(); } + @Override + public List listRetrievableDocumentsByKnowledgeBaseIds(java.util.Collection knowledgeBaseIds) { + return List.of(); + } + @Override public List vectorSearch(org.javaup.ai.manage.model.DocumentRetrieveRequest request) { return List.of(); diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouterTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c98ad1cdc4e81123d1988be5b864100650cc3dfb --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouterTest.java @@ -0,0 +1,204 @@ +package org.javaup.ai.chatagent.rag.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.javaup.ai.chatagent.rag.model.DocumentNavigationDecision; +import org.javaup.ai.chatagent.rag.model.DocumentNavigationAction; +import org.javaup.ai.chatagent.rag.model.ExecutionMode; +import org.javaup.ai.chatagent.rag.model.QueryType; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.javaup.ai.chatagent.rag.model.RagRewriteResult; +import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationOperation; +import org.javaup.ai.manage.model.graph.GraphItem; +import org.javaup.ai.manage.model.graph.GraphSection; +import org.javaup.ai.manage.service.DocumentNavigationIndexService; +import org.javaup.ai.manage.service.DocumentStructureGraphService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.ObjectProvider; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class DocumentQuestionRouterTest { + + @Test + void structureNavigationChildrenIntentKeepsRetrievalAndSetsChildAction() { + QueryUnderstandingService queryUnderstandingService = new QueryUnderstandingService(null, null, new ObjectMapper()) { + @Override + public QueryUnderstandingResult understand(String originalQuestion, + String rewrittenQuestion, + List subQuestions, + String historySummary, + String answerRecentTranscript) { + return QueryUnderstandingResult.builder() + .queryType(QueryType.STRUCTURE_NAVIGATION) + .channels(List.of(RetrievalIntent.GENERAL, RetrievalIntent.STRUCTURE)) + .structureNavigationIntent(StructureNavigationIntent.builder() + .operations(List.of(StructureNavigationOperation.SECTION_WITH_CHILDREN)) + .sectionAnchors(List.of("机器人策略设计")) + .confidence(0.91D) + .source("test") + .build()) + .confidence(0.91D) + .source("test") + .build(); + } + }; + DocumentQuestionRouter router = new DocumentQuestionRouter( + new StaticDocumentStructureGraphService(), + emptyProvider(), + provider(queryUnderstandingService) + ); + RagRewriteResult rewrite = new RagRewriteResult( + "机器人策略设计都包含哪些章节?", + List.of("机器人策略设计都包含哪些章节?"), + "" + ); + + DocumentNavigationDecision decision = router.route(1L, "机器人策略设计都包含哪些章节?", rewrite, "", ""); + + assertThat(decision.getExecutionMode()).isEqualTo(ExecutionMode.RETRIEVAL); + assertThat(decision.getRetrievalIntent()).isEqualTo(RetrievalIntent.STRUCTURE); + assertThat(decision.getNavigationAction()).isEqualTo(DocumentNavigationAction.CHILD_SECTION_DESCEND); + assertThat(decision.getQueryUnderstanding().getStructureNavigationIntent().getOperations()) + .containsExactly(StructureNavigationOperation.SECTION_WITH_CHILDREN); + } + + @Test + void followUpItemReferenceWithoutExplicitSectionUsesRetrievalWithSoftAnchor() { + QueryUnderstandingService queryUnderstandingService = new QueryUnderstandingService(null, null, new ObjectMapper()) { + @Override + public QueryUnderstandingResult understand(String originalQuestion, + String rewrittenQuestion, + List subQuestions, + String historySummary, + String answerRecentTranscript) { + return QueryUnderstandingResult.builder() + .queryType(QueryType.FOLLOW_UP) + .channels(List.of(RetrievalIntent.GENERAL, RetrievalIntent.STRUCTURE)) + .confidence(0.86D) + .source("test") + .build(); + } + }; + DocumentQuestionRouter router = new DocumentQuestionRouter( + new StaticDocumentStructureGraphService(), + emptyProvider(), + provider(queryUnderstandingService) + ); + RagRewriteResult rewrite = new RagRewriteResult( + "前一轮问题的检查顺序中,第一项和第三项分别是什么?", + List.of("前一轮问题的检查顺序中,第一项和第三项分别是什么?"), + "" + ); + + DocumentNavigationDecision decision = router.route( + 1L, + "那这个问题里的第一项和第三项分别是什么?", + rewrite, + "", + "" + ); + + assertThat(decision.getExecutionMode()).isEqualTo(ExecutionMode.RETRIEVAL); + assertThat(decision.getStructureAnchor().getScopeMode()).isIn("SOFT", "NONE"); + } + + private static ObjectProvider provider(T value) { + return new ObjectProvider<>() { + @Override + public T getObject(Object... args) { + return value; + } + + @Override + public T getIfAvailable() { + return value; + } + + @Override + public T getIfUnique() { + return value; + } + + @Override + public T getObject() { + return value; + } + }; + } + + private static ObjectProvider emptyProvider() { + return provider(null); + } + + private static class StaticDocumentStructureGraphService implements DocumentStructureGraphService { + + @Override + public GraphSection findSectionById(Long documentId, Long sectionNodeId) { + return null; + } + + @Override + public GraphSection findSectionByCode(Long documentId, String nodeCode) { + return null; + } + + @Override + public GraphSection findSectionByTitle(Long documentId, String title) { + return null; + } + + @Override + public GraphSection findSectionByCanonicalPath(Long documentId, String canonicalPath) { + return null; + } + + @Override + public GraphSection findBestSection(Long documentId, String topic, String facet) { + return null; + } + + @Override + public List listSections(Long documentId) { + return List.of(); + } + + @Override + public List listChildren(Long documentId, Long sectionNodeId) { + return List.of(); + } + + @Override + public GraphSection parentSection(Long documentId, Long sectionNodeId) { + return null; + } + + @Override + public GraphSection previousSibling(Long documentId, Long sectionNodeId) { + return null; + } + + @Override + public GraphSection nextSibling(Long documentId, Long sectionNodeId) { + return null; + } + + @Override + public GraphItem findItemByIndex(Long documentId, Long sectionNodeId, Integer itemIndex) { + return null; + } + + @Override + public List listItems(Long documentId, Long sectionNodeId) { + return List.of(); + } + + @Override + public List searchItemsInSection(Long documentId, Long sectionNodeId, String keyword) { + return List.of(); + } + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityServiceTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..624db87a9e6943a94bba8bf4b89f9f86cad8a0e1 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityServiceTest.java @@ -0,0 +1,116 @@ +package org.javaup.ai.chatagent.rag.service; + +import org.javaup.ai.chatagent.rag.model.EvidenceRole; +import org.javaup.ai.chatagent.rag.model.EvidenceApplicabilityResult; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class EvidenceApplicabilityServiceTest { + + private final EvidenceApplicabilityService service = new EvidenceApplicabilityService(); + + @Test + void rejectsEvidenceThatOnlyMatchesExcludedEntity() { + QueryUnderstandingResult understanding = QueryUnderstandingResult.builder() + .targetEntities(List.of("知识引用错误率突然升高")) + .excludedEntities(List.of("人工转接率异常升高")) + .negativeBoundary(true) + .confidence(0.9D) + .source("test") + .build(); + + Document evidence = doc("e1", "# 14.3.1\n1. 看是不是服务时间策略误配置。", 0.9D, Map.of( + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.3.1", + DocumentKnowledgeMetadataKeys.TITLE, "人工转接率异常升高" + )); + + EvidenceApplicabilityResult result = service.evaluate(understanding, evidence); + + assertThat(result.isApplicable()).isFalse(); + assertThat(result.getReason()).contains("excluded"); + } + + @Test + void keepsEvidenceWhenTargetEntityIsGrounded() { + QueryUnderstandingResult understanding = QueryUnderstandingResult.builder() + .targetEntities(List.of("知识引用错误率突然升高")) + .excludedEntities(List.of("人工转接率异常升高")) + .negativeBoundary(true) + .confidence(0.9D) + .source("test") + .build(); + + Document evidence = doc("e1", "文档明确说明知识引用错误率突然升高时,应先检查引用来源。", 0.9D, Map.of( + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.4", + DocumentKnowledgeMetadataKeys.TITLE, "知识引用错误率突然升高" + )); + + EvidenceApplicabilityResult result = service.evaluate(understanding, evidence); + + assertThat(result.isApplicable()).isTrue(); + assertThat(result.getStatus()).isEqualTo("APPLICABLE"); + } + + @Test + void doesNotInferEvidenceRoleFromSectionTitle() { + QueryUnderstandingResult understanding = QueryUnderstandingResult.builder() + .expectedEvidenceRoles(List.of(EvidenceRole.SYMPTOM)) + .confidence(0.9D) + .source("test") + .build(); + + Document evidence = doc("cause", "父子块策略配置错误、索引任务未完成、finalTopK 被下调。", 0.9D, Map.of( + DocumentKnowledgeMetadataKeys.SECTION_PATH, "十四、常见问题处理 > 14.1 检索命中率突然下降 > 14.1.2 可能原因", + DocumentKnowledgeMetadataKeys.TITLE, "14.1.2 可能原因" + )); + + EvidenceApplicabilityResult result = service.evaluate(understanding, evidence); + + assertThat(result.isApplicable()).isTrue(); + assertThat(result.getStatus()).isEqualTo(EvidenceApplicabilityResult.APPLICABLE_UNKNOWN); + assertThat(result.getReason()).contains("evidence role is GENERAL"); + assertThat(evidence.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.EVIDENCE_ROLE, "GENERAL"); + } + + @Test + void keepsEvidenceWhenStructuredRoleMatches() { + QueryUnderstandingResult understanding = QueryUnderstandingResult.builder() + .expectedEvidenceRoles(List.of(EvidenceRole.SYMPTOM)) + .confidence(0.9D) + .source("test") + .build(); + + Document evidence = doc("symptom", "日志中 finalTopK 下降明显,召回片段数量低于平时。", 0.9D, Map.of( + DocumentKnowledgeMetadataKeys.SECTION_PATH, "十四、常见问题处理 > 14.1 检索命中率突然下降 > 14.1.1 现象", + DocumentKnowledgeMetadataKeys.TITLE, "14.1.1 现象", + DocumentKnowledgeMetadataKeys.EVIDENCE_ROLE, "SYMPTOM" + )); + + EvidenceApplicabilityResult result = service.evaluate(understanding, evidence); + + assertThat(result.isApplicable()).isTrue(); + assertThat(result.getReason()).contains("evidence role matched"); + assertThat(evidence.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.EVIDENCE_ROLE, "SYMPTOM"); + } + + private static Document doc(String id, String text, double score, Map metadata) { + LinkedHashMap mergedMetadata = new LinkedHashMap<>(metadata); + mergedMetadata.put(DocumentKnowledgeMetadataKeys.SCORE, score); + return Document.builder() + .id(id) + .text(text) + .metadata(mergedMetadata) + .score(score) + .build(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicyTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicyTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ead3e8d4d842ab2b082165b669556789fae7db2b --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicyTest.java @@ -0,0 +1,353 @@ +package org.javaup.ai.chatagent.rag.service; + +import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.QueryType; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.javaup.enums.RetrievalChannelEnum; +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinalEvidenceSelectionPolicyTest { + + @Test + void preservesStructureNavigationContextReasonForDirectoryAnswer() { + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .retrievalIntent(RetrievalIntent.STRUCTURE) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.STRUCTURE_NAVIGATION) + .confidence(0.91D) + .source("test") + .build()) + .build(); + + Document parent = doc("nav-parent", "结构导航节点:十三、上线观察与值班规则", 1.20D, metadata( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.TASK_ID, 11L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 130L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "十三、上线观察与值班规则", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "十三、上线观察与值班规则", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE", + DocumentKnowledgeMetadataKeys.CHANNEL, "structure-navigation", + DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_NAVIGATION_PARENT" + )); + Document next = doc("nav-next", "结构导航节点:13.2 值班安排", 1.19D, metadata( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.TASK_ID, 11L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 132L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "十三、上线观察与值班规则 > 13.2 值班安排", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "十三、上线观察与值班规则/13.2 值班安排", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE", + DocumentKnowledgeMetadataKeys.CHANNEL, "structure-navigation", + DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_NAVIGATION_SIBLING" + )); + + ChatRagProperties properties = new ChatRagProperties(); + properties.setFinalTopK(2); + List selected = new FinalEvidenceSelectionPolicy(properties).select(List.of(parent, next), plan); + + assertThat(selected).extracting(Document::getId).containsExactly("nav-parent", "nav-next"); + assertThat(parent.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "SELECTED_STRUCTURE_NAVIGATION_PARENT"); + assertThat(next.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "SELECTED_STRUCTURE_NAVIGATION_SIBLING"); + } + + @Test + void keepsBodyEvidenceWhenTitleCandidateRanksAboveBodyCandidate() { + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .retrievalIntent(RetrievalIntent.GENERAL) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.DOCUMENT_QA) + .sectionAnchors(List.of("14.1.2")) + .confidence(0.84D) + .source("test") + .build()) + .build(); + + Document title = doc("title", "# 14.1.2", 0.99D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 101L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.1.2", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.1/14.1.2", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE" + )); + Document body = doc("body", "1. 新版本切块异常。\n2. 父子块配置错误。\n3. 向量索引构建不完整。\n4. 检索过滤条件误收紧。", 0.50D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 101L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.1.2", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.1/14.1.2", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "BODY" + )); + Document unrelated = doc("unrelated", "其他章节内容", 0.98D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 999L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "12.3" + )); + + ChatRagProperties properties = new ChatRagProperties(); + properties.setFinalTopK(2); + List selected = new FinalEvidenceSelectionPolicy(properties).select(List.of(title, unrelated, body), plan); + + assertThat(selected).extracting(Document::getId).containsExactly("title", "body"); + assertThat(body.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "SAME_SECTION_BODY"); + } + + @Test + void structureAnchorBodyCandidateReplacesTitleOnlyEvidence() { + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .retrievalIntent(RetrievalIntent.GENERAL) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.DOCUMENT_QA) + .sectionAnchors(List.of("14.3.1")) + .confidence(0.88D) + .source("test") + .build()) + .build(); + + Document titleOnly = doc("title-only", "#### 14.3.1 检查顺序", 0.99D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 301L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.3.1", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.3/14.3.1", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE" + )); + Document unrelated = doc("unrelated", "其他章节内容", 0.98D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 999L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "12.3" + )); + Document structureBody = doc("structure-body", "1. 检查机器人策略。\n2. 检查知识召回质量。\n3. 检查人工排队规则。", 0.40D, metadata( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 301L, + DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9301L, + DocumentKnowledgeMetadataKeys.CHUNK_ID, 930101L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.3.1", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.3/14.3.1", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "BODY", + DocumentKnowledgeMetadataKeys.CHANNEL, "structure-anchor", + DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_ANCHOR_BODY_CANDIDATE", + DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE, "NODE_ID", + DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_RAW_BODY, true, + DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW, true + )); + + ChatRagProperties properties = new ChatRagProperties(); + properties.setFinalTopK(2); + List selected = new FinalEvidenceSelectionPolicy(properties).select( + List.of(titleOnly, unrelated, structureBody), + plan + ); + + assertThat(selected).extracting(Document::getId).containsExactly("structure-body", "unrelated"); + assertThat(structureBody.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_ANCHOR_BODY") + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "REPLACED_TITLE_ONLY_WITH_BODY"); + } + + @Test + void descendantStructureBodyCandidateIsProtectedWhenParentAnchorMatches() { + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .retrievalIntent(RetrievalIntent.GENERAL) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.DOCUMENT_QA) + .sectionAnchors(List.of("14.1")) + .confidence(0.86D) + .source("test") + .build()) + .build(); + + Document parentTitle = doc("parent-title", "### 14.1 场景一", 0.99D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 201L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.1", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.1", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE" + )); + Document descendantBody = doc("descendant-body", "1. 新版本切块异常。\n2. 父子块配置错误。\n3. 向量索引构建不完整。", 0.35D, metadata( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 202L, + DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9202L, + DocumentKnowledgeMetadataKeys.CHUNK_ID, 920201L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.1.2", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.1/14.1.2", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "BODY", + DocumentKnowledgeMetadataKeys.CHANNEL, "structure-anchor", + DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_ANCHOR_BODY_CANDIDATE", + DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE, "CANONICAL_DESCENDANT", + DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_RAW_BODY, true, + DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW, true + )); + + ChatRagProperties properties = new ChatRagProperties(); + properties.setFinalTopK(1); + List selected = new FinalEvidenceSelectionPolicy(properties).select( + List.of(parentTitle, descendantBody), + plan + ); + + assertThat(selected).extracting(Document::getId).containsExactly("descendant-body"); + assertThat(descendantBody.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_DESCENDANT_BODY") + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "REPLACED_TITLE_ONLY_WITH_BODY"); + } + + @Test + void structureAnchorCandidateWithoutRawBodyIsNotProtected() { + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .retrievalIntent(RetrievalIntent.GENERAL) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.DOCUMENT_QA) + .sectionAnchors(List.of("14.3.1")) + .confidence(0.88D) + .source("test") + .build()) + .build(); + + Document titleOnly = doc("title-only", "#### 14.3.1 检查顺序", 0.99D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 301L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.3.1", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.3/14.3.1", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE" + )); + Document unrelated = doc("unrelated", "其他章节内容", 0.98D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 999L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "12.3" + )); + Document wrapped = doc("wrapped", "[GraphRAG entity] 14.3.1 检查顺序", 0.40D, Map.of( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L, + DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 301L, + DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9301L, + DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.3.1", + DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.3/14.3.1", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE", + DocumentKnowledgeMetadataKeys.CHANNEL, "structure-anchor", + DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_ANCHOR_BODY_CANDIDATE", + DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE, "NODE_ID", + DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW, true + )); + + ChatRagProperties properties = new ChatRagProperties(); + properties.setFinalTopK(2); + List selected = new FinalEvidenceSelectionPolicy(properties).select( + List.of(titleOnly, unrelated, wrapped), + plan + ); + + assertThat(selected).extracting(Document::getId).containsExactly("title-only", "unrelated"); + assertThat(wrapped.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_ANCHOR_BODY_CANDIDATE") + .doesNotContainEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "REPLACED_TITLE_ONLY_WITH_BODY"); + } + + @Test + void reservesRaptorSourceChunkInsteadOfSummaryOnlyEvidence() { + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .retrievalIntent(RetrievalIntent.RAPTOR) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.GLOBAL_SUMMARY) + .channels(List.of(RetrievalIntent.RAPTOR)) + .confidence(0.86D) + .source("test") + .build()) + .build(); + + Document summaryOnly = doc("raptor-summary", "RAPTOR 摘要:灰度上线需要持续观察核心指标。", 0.99D, Map.of( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "RAPTOR", + DocumentKnowledgeMetadataKeys.CHANNEL, RetrievalChannelEnum.RAPTOR.getName(), + DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID, 3001L, + DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS, "SUMMARY_ONLY", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "RAPTOR_SUMMARY" + )); + Document sourceChunk = doc("raptor-source", "原文:灰度期需要观察回答准确率、人工转接率和无证据回复率。", 0.50D, Map.of( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "RAPTOR", + DocumentKnowledgeMetadataKeys.CHANNEL, RetrievalChannelEnum.RAPTOR.getName(), + DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID, 3001L, + DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS, "SOURCE_CHUNK", + DocumentKnowledgeMetadataKeys.CHUNK_ID, 2001L, + DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9001L, + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "RAPTOR_SOURCE_CHUNK" + )); + + ChatRagProperties properties = new ChatRagProperties(); + properties.setFinalTopK(1); + List selected = new FinalEvidenceSelectionPolicy(properties).select(List.of(summaryOnly, sourceChunk), plan); + + assertThat(selected).extracting(Document::getId).containsExactly("raptor-source"); + assertThat(sourceChunk.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "RAPTOR_SOURCE_CHUNK") + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "SELECTED_RAPTOR_SOURCE_CHUNK"); + } + + @Test + void reservesGraphRelationQuoteInsteadOfCommunitySummaryOnlyEvidence() { + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .retrievalIntent(RetrievalIntent.GRAPH_RAG) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.GRAPH_RELATION) + .channels(List.of(RetrievalIntent.GRAPH_RAG)) + .confidence(0.86D) + .source("test") + .build()) + .build(); + + Document communitySummaryOnly = doc("graph-community-summary", "社区摘要:该社区覆盖服务和团队。", 0.99D, Map.of( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "GRAPH_RAG", + DocumentKnowledgeMetadataKeys.CHANNEL, RetrievalChannelEnum.GRAPH_RAG.getName(), + DocumentKnowledgeMetadataKeys.KG_COMMUNITY_ID, 3001L, + DocumentKnowledgeMetadataKeys.KG_COMMUNITY_TITLE, "服务职责社区", + DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY, "该社区覆盖服务和团队,但没有原文 quote。", + DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY, true, + DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL, "COMMUNITY_SUMMARY_ONLY" + )); + Document relationQuote = doc("graph-relation-quote", "原文:PaymentService 由 OwnerTeam 负责维护。", 0.45D, Map.of( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "GRAPH_RAG", + DocumentKnowledgeMetadataKeys.CHANNEL, RetrievalChannelEnum.GRAPH_RAG.getName(), + DocumentKnowledgeMetadataKeys.KG_RELATION_ID, 7001L, + DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "CALLS", + DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID, 8001L, + DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE, "llm.controlled.query_plan.v1", + DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL, "RELATION_STRONG_QUOTE", + DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET, "PaymentService 由 OwnerTeam 负责维护。" + )); + + ChatRagProperties properties = new ChatRagProperties(); + properties.setFinalTopK(1); + List selected = new FinalEvidenceSelectionPolicy(properties).select(List.of(communitySummaryOnly, relationQuote), plan); + + assertThat(selected).extracting(Document::getId).containsExactly("graph-relation-quote"); + assertThat(relationQuote.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "GRAPH_RAG_QUOTE") + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "SELECTED_GRAPH_RAG_QUOTE"); + } + + private static Document doc(String id, String text, double score, Map metadata) { + LinkedHashMap mergedMetadata = new LinkedHashMap<>(metadata); + mergedMetadata.put(DocumentKnowledgeMetadataKeys.SCORE, score); + return Document.builder() + .id(id) + .text(text) + .metadata(mergedMetadata) + .score(score) + .build(); + } + + private static Map metadata(Object... values) { + LinkedHashMap result = new LinkedHashMap<>(); + for (int index = 0; index + 1 < values.length; index += 2) { + result.put(String.valueOf(values[index]), values[index + 1]); + } + return result; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolverTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolverTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0b90e911e5b85b0a6517735b555a9e0736750c36 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolverTest.java @@ -0,0 +1,101 @@ +package org.javaup.ai.chatagent.rag.service; + +import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class KnowledgeBaseRuntimeConfigResolverTest { + + @Test + void singleKnowledgeBaseOverridesNonEmptyRetrievalConfig() { + ChatRagProperties defaults = defaults(); + KnowledgeBaseRuntimeConfigResolver resolver = new KnowledgeBaseRuntimeConfigResolver(defaults); + SuperAgentKnowledgeBase kb = knowledgeBase(1L, "研发库", """ + { + "vectorTopK": 12, + "keywordTopK": 6, + "candidateTopK": 20, + "rerankCandidateTopK": 16, + "reserveCandidateTopK": 9, + "minVectorSimilarity": 0.62, + "keywordChannelEnabled": false, + "hybrid": { + "vectorWeight": 1.7, + "keywordWeight": 0.4 + } + } + """); + + RagRuntimeOptions options = resolver.resolve(List.of(kb)); + + assertThat(options.getVectorTopK()).isEqualTo(12); + assertThat(options.getKeywordTopK()).isEqualTo(6); + assertThat(options.getCandidateTopK()).isEqualTo(20); + assertThat(options.getRerankCandidateTopK()).isEqualTo(16); + assertThat(options.getReserveCandidateTopK()).isEqualTo(9); + assertThat(options.getMinVectorSimilarity()).isEqualTo(0.62D); + assertThat(options.isKeywordChannelEnabled()).isFalse(); + assertThat(options.getHybrid().getVectorWeight()).isEqualTo(1.7D); + assertThat(options.getHybrid().getKeywordWeight()).isEqualTo(0.4D); + assertThat(options.getKbConfigConflictFields()).isEmpty(); + } + + @Test + void multipleKnowledgeBasesOnlyOverrideConsistentFieldsAndReportConflicts() { + ChatRagProperties defaults = defaults(); + KnowledgeBaseRuntimeConfigResolver resolver = new KnowledgeBaseRuntimeConfigResolver(defaults); + SuperAgentKnowledgeBase kbA = knowledgeBase(1L, "研发库", """ + { + "vectorTopK": 12, + "keywordTopK": 6, + "graphRagTopK": 9, + "hybrid": { + "vectorWeight": 1.5 + } + } + """); + SuperAgentKnowledgeBase kbB = knowledgeBase(2L, "客服库", """ + { + "vectorTopK": 12, + "keywordTopK": 8, + "graphRagTopK": 9, + "hybrid": { + "vectorWeight": 1.5 + } + } + """); + + RagRuntimeOptions options = resolver.resolve(List.of(kbA, kbB)); + + assertThat(options.getVectorTopK()).isEqualTo(12); + assertThat(options.getGraphRagTopK()).isEqualTo(9); + assertThat(options.getKeywordTopK()).isEqualTo(defaults.getKeywordTopK()); + assertThat(options.getHybrid().getVectorWeight()).isEqualTo(1.5D); + assertThat(options.getKbConfigConflictFields()).containsExactly("keywordTopK"); + } + + private static ChatRagProperties defaults() { + ChatRagProperties properties = new ChatRagProperties(); + properties.setVectorTopK(8); + properties.setKeywordTopK(8); + properties.setGraphRagTopK(5); + properties.setMinVectorSimilarity(0.45D); + properties.setKeywordChannelEnabled(true); + properties.getHybrid().setVectorWeight(1.0D); + properties.getHybrid().setKeywordWeight(1.0D); + return properties; + } + + private static SuperAgentKnowledgeBase knowledgeBase(Long id, String name, String retrievalConfigJson) { + SuperAgentKnowledgeBase knowledgeBase = new SuperAgentKnowledgeBase(); + knowledgeBase.setId(id); + knowledgeBase.setBaseName(name); + knowledgeBase.setRetrievalConfigJson(retrievalConfigJson); + return knowledgeBase; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingServiceTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6d00eb19d4bdc9ff980c0e4533513c2d9349725e --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingServiceTest.java @@ -0,0 +1,76 @@ +package org.javaup.ai.chatagent.rag.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.javaup.ai.chatagent.rag.model.QueryType; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationOperation; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class QueryUnderstandingServiceTest { + + @Test + void deterministicFallbackBuildsSiblingNavigationIntent() { + QueryUnderstandingService service = new QueryUnderstandingService(null, null, new ObjectMapper()); + + QueryUnderstandingResult result = service.understand( + "刚才说的“观察时长”属于哪个一级章节和哪个小节?同一一级章节里的下一小节是什么?", + "", + List.of(), + "", + "" + ); + + assertThat(result.getQueryType()).isEqualTo(QueryType.STRUCTURE_NAVIGATION); + assertThat(result.getChannels()).contains(RetrievalIntent.STRUCTURE); + assertThat(result.getStructureNavigationIntent()).isNotNull(); + assertThat(result.getStructureNavigationIntent().getOperations()) + .containsExactly(StructureNavigationOperation.SECTION_WITH_SIBLINGS); + assertThat(result.getStructureNavigationIntent().getSectionAnchors()).contains("观察时长"); + } + + @Test + void deterministicFallbackBuildsChildrenNavigationIntent() { + QueryUnderstandingService service = new QueryUnderstandingService(null, null, new ObjectMapper()); + + QueryUnderstandingResult result = service.understand( + "机器人策略设计都包含哪些章节?", + "", + List.of(), + "", + "" + ); + + assertThat(result.getQueryType()).isEqualTo(QueryType.STRUCTURE_NAVIGATION); + assertThat(result.getStructureNavigationIntent()).isNotNull(); + assertThat(result.getStructureNavigationIntent().getOperations()) + .containsExactly(StructureNavigationOperation.SECTION_WITH_CHILDREN); + } + + @Test + void deterministicFallbackDoesNotInferEvidenceRolesFromQuestionText() { + QueryUnderstandingService service = new QueryUnderstandingService(null, null, new ObjectMapper()); + + QueryUnderstandingResult symptom = service.understand( + "还有造成这个问题的现象都有什么?", + "", + List.of(), + "", + "" + ); + QueryUnderstandingResult cause = service.understand( + "检索命中率突然下降的可能原因都有哪些?", + "", + List.of(), + "", + "" + ); + + assertThat(symptom.getExpectedEvidenceRoles()).isEmpty(); + assertThat(cause.getExpectedEvidenceRoles()).isEmpty(); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairServiceTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairServiceTest.java index 165a7fad07e86771a5e38ef18ca34d168f48b4e7..941bd46f09d0cc7a28478944f801a7bacb8cd03c 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairServiceTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairServiceTest.java @@ -41,6 +41,57 @@ class RagCitationRepairServiceTest { assertThat(result.get(1).getBboxJson()).isEqualTo("[0,0,10,10]"); } + @Test + void repairDoesNotPromoteNotApplicableEvidence() { + CapturingRagToolsClient ragToolsClient = new CapturingRagToolsClient(); + ragToolsClient.responseEvidenceId = "2"; + ragToolsClient.responseChunkId = 202L; + ragToolsClient.responseQuoteText = "当前目标对象有明确证据。"; + RagCitationRepairService service = new RagCitationRepairService(ragToolsClient); + + SearchReference notApplicable = documentReference("1", 101L, "相似对象的处理步骤。", 3, "[0,0,10,10]"); + notApplicable.setFinalSelectionReason("FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY"); + notApplicable.setEvidenceApplicabilityStatus("NOT_APPLICABLE"); + notApplicable.setEvidenceApplicabilityReason("only excluded entity matched"); + SearchReference applicable = documentReference("2", 202L, "当前目标对象有明确证据。", 4, "[0,0,20,20]"); + + List result = service.repair( + "当前目标对象有明确证据。", + List.of(notApplicable, applicable), + null, + "RETRIEVAL" + ); + + assertThat(ragToolsClient.lastRequest.getEvidences()) + .extracting(RagToolsCitationRepairRequest.Evidence::getId) + .containsExactly("2"); + assertThat(result).extracting(SearchReference::getReferenceId) + .containsExactly("2"); + assertThat(result.get(0).isCitationRepaired()).isTrue(); + assertThat(result.get(0).getChunkId()).isEqualTo(202L); + } + + @Test + void repairSkipsRagToolsWhenOnlyNotApplicableDocumentEvidenceExists() { + CapturingRagToolsClient ragToolsClient = new CapturingRagToolsClient(); + RagCitationRepairService service = new RagCitationRepairService(ragToolsClient); + + SearchReference notApplicable = documentReference("1", 101L, "相似对象的处理步骤。", 3, "[0,0,10,10]"); + notApplicable.setEvidenceApplicabilityStatus("NOT_APPLICABLE"); + SearchReference webReference = new SearchReference("网页", "https://example.com", "外部搜索结果"); + + List result = service.repair( + "当前目标对象没有明确证据。", + List.of(notApplicable, webReference), + null, + "RETRIEVAL" + ); + + assertThat(ragToolsClient.lastRequest).isNull(); + assertThat(result).extracting(SearchReference::getSourceType) + .containsExactly("WEB"); + } + private static SearchReference documentReference(String referenceId, Long chunkId, String snippet, @@ -62,6 +113,9 @@ class RagCitationRepairServiceTest { private static class CapturingRagToolsClient extends RagToolsClient { private RagToolsCitationRepairRequest lastRequest; + private String responseEvidenceId = "1"; + private Long responseChunkId = 101L; + private String responseQuoteText = "出差结束后 10 个工作日内提交报销。"; private CapturingRagToolsClient() { super(new RagToolsProperties(), new com.fasterxml.jackson.databind.ObjectMapper()); @@ -72,13 +126,13 @@ class RagCitationRepairServiceTest { this.lastRequest = request; RagToolsCitationRepairResponse response = new RagToolsCitationRepairResponse(); RagToolsCitationRepairResponse.Result result = new RagToolsCitationRepairResponse.Result(); - result.setEvidenceId("1"); + result.setEvidenceId(responseEvidenceId); result.setAnswerSegment("报销时限是出差结束后 10 个工作日内提交。"); result.setSegmentIndex(1); - result.setQuoteText("出差结束后 10 个工作日内提交报销。"); + result.setQuoteText(responseQuoteText); result.setScore(0.87D); result.setRank(1); - result.setChunkId(101L); + result.setChunkId(responseChunkId); result.setPageNo(3); result.setPageRange("3"); result.setBboxJson("[0,0,10,10]"); diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagPromptAssemblyServiceTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagPromptAssemblyServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7714b87849c76ffbd379721e4fb316d37acfb549 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagPromptAssemblyServiceTest.java @@ -0,0 +1,55 @@ +package org.javaup.ai.chatagent.rag.service; + +import org.javaup.ai.chatagent.model.SearchReference; +import org.javaup.ai.chatagent.rag.config.ChatRagProperties; +import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.EvidenceRole; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; +import org.javaup.ai.chatagent.rag.model.RagRetrievalContext; +import org.javaup.ai.chatagent.rag.model.SubQuestionEvidence; +import org.javaup.ai.prompt.PromptTemplateService; +import org.junit.jupiter.api.Test; +import org.springframework.core.io.DefaultResourceLoader; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class RagPromptAssemblyServiceTest { + + @Test + void promptIncludesExpectedEvidenceRoleBoundary() { + RagPromptAssemblyService service = new RagPromptAssemblyService( + new ChatRagProperties(), + new PromptTemplateService(new DefaultResourceLoader()), + new AnswerPlanService() + ); + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .currentDateText("2026-07-06") + .originalQuestion("还有造成这个问题的现象都有什么?") + .retrievalQuestion("检索命中率突然下降的现象") + .queryUnderstanding(QueryUnderstandingResult.builder() + .expectedEvidenceRoles(List.of(EvidenceRole.SYMPTOM)) + .build()) + .build(); + SearchReference reference = new SearchReference(); + reference.setReferenceId("1"); + reference.setSourceType("DOCUMENT"); + reference.setDocumentName("星联智服全渠道客服平台上线与运营管理手册.md"); + reference.setSectionPath("14.1.2 可能原因"); + reference.setSnippet("父子块策略配置错误、索引任务未完成、finalTopK 被下调。"); + reference.setEvidenceRole("CAUSE"); + RagRetrievalContext context = new RagRetrievalContext( + "检索命中率突然下降的现象", + List.of(new SubQuestionEvidence(1, "检索命中率突然下降的现象", List.of(), List.of(reference), List.of(), 1, 1, 1)), + List.of(), + List.of("vector") + ); + + String prompt = service.buildUserPrompt(plan, context); + + assertThat(prompt).contains("本轮问题期望证据角色:SYMPTOM"); + assertThat(prompt).contains("背景证据不能替代对应角色证据"); + assertThat(prompt).contains("证据角色:CAUSE"); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagRetrievalEngineTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagRetrievalEngineTest.java index f8ec8dde6701f3a746dd1375e0b3108cbc8b6fde..a0ffef2b19760c1e19b1a404ccab64d96ed0bdc3 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagRetrievalEngineTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/RagRetrievalEngineTest.java @@ -2,9 +2,13 @@ package org.javaup.ai.chatagent.rag.service; import org.javaup.ai.chatagent.rag.config.ChatRagProperties; import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan; +import org.javaup.ai.chatagent.rag.model.DocumentNavigationDecision; import org.javaup.ai.chatagent.rag.model.ExecutionMode; +import org.javaup.ai.chatagent.rag.model.QueryType; +import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult; import org.javaup.ai.chatagent.rag.model.RagRetrievalContext; import org.javaup.ai.chatagent.rag.model.RetrievalIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationResult; import org.javaup.ai.chatagent.rag.retrieve.channel.RetrievalChannel; import org.javaup.ai.chatagent.rag.retrieve.channel.RetrievalChannelResult; import org.javaup.ai.chatagent.service.ConversationTraceRecorder; @@ -13,8 +17,12 @@ import org.javaup.ai.chatagent.model.ChannelExecutionView; import org.javaup.ai.chatagent.model.RetrievalResultView; import org.javaup.ai.manage.model.DocumentRetrieveRequest; import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor; +import org.javaup.ai.manage.model.StructureAnchoredEvidenceRequest; +import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode; import org.javaup.ai.manage.service.DocumentKnowledgeService; import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.javaup.enums.ChatQueryMode; +import org.javaup.enums.KnowledgeBaseSelectionMode; import org.javaup.enums.RetrievalChannelEnum; import org.junit.jupiter.api.Test; import org.springframework.ai.document.Document; @@ -29,6 +37,76 @@ import static org.assertj.core.api.Assertions.assertThat; class RagRetrievalEngineTest { + @Test + void structureNavigationResultEntersFinalEvidenceWhenChannelsAreEmpty() { + ChatRagProperties properties = new ChatRagProperties(); + properties.setRerankEnabled(false); + properties.setMinVectorSimilarity(0D); + properties.setKeywordRelativeScoreFloor(0D); + properties.setCandidateTopK(10); + properties.setFinalTopK(4); + + StructureNavigationResult structureResult = StructureNavigationResult.builder() + .documentId(1L) + .anchorNodeId(131L) + .current(structureNode(131L, 1L, 11L, 131, "13.1 观察时长", 130L, null, 132L, + "十三、上线观察与值班规则 > 13.1 观察时长", "十三、上线观察与值班规则/13.1 观察时长")) + .parent(structureNode(130L, 1L, 11L, 130, "十三、上线观察与值班规则", null, null, null, + "十三、上线观察与值班规则", "十三、上线观察与值班规则")) + .nextSibling(structureNode(132L, 1L, 11L, 132, "13.2 值班安排", 130L, 131L, null, + "十三、上线观察与值班规则 > 13.2 值班安排", "十三、上线观察与值班规则/13.2 值班安排")) + .deterministic(true) + .build(); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + try { + RagRetrievalEngine engine = new RagRetrievalEngine( + List.of(new StaticRetrievalChannel(RetrievalChannelEnum.VECTOR.getName(), List.of())), + properties, + null, + new PassThroughDocumentKnowledgeService(), + executorService + ); + + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .mode(ExecutionMode.RETRIEVAL) + .retrievalIntent(RetrievalIntent.STRUCTURE) + .retrievalQuestion("观察时长属于哪个章节,下一节是什么?") + .retrievalSubQuestions(List.of("观察时长属于哪个章节,下一节是什么?")) + .selectedDocumentId(1L) + .selectedTaskId(11L) + .retrievalDocumentIds(List.of(1L)) + .retrievalTaskIds(List.of(11L)) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.STRUCTURE_NAVIGATION) + .confidence(0.91D) + .source("test") + .build()) + .navigationDecision(DocumentNavigationDecision.builder() + .structureNavigationResult(structureResult) + .retrievalIntent(RetrievalIntent.STRUCTURE) + .build()) + .build(); + + RagRetrievalContext context = engine.retrieve(plan, null); + + List documents = context.getSubQuestionEvidenceList().get(0).getDocuments(); + assertThat(documents) + .extracting(Document::getId) + .contains("structure-navigation:CURRENT:131", "structure-navigation:PARENT:130", "structure-navigation:SIBLING:132"); + Document next = documents.stream() + .filter(document -> "structure-navigation:SIBLING:132".equals(document.getId())) + .findFirst() + .orElseThrow(); + assertThat(next.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.CHANNEL, "structure-navigation") + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "SELECTED_STRUCTURE_NAVIGATION_SIBLING"); + } + finally { + executorService.shutdownNow(); + } + } + @Test void weightedHybridUsesMetadataBoostAndRecordsChannelScores() { ChatRagProperties properties = new ChatRagProperties(); @@ -130,7 +208,7 @@ class RagRetrievalEngineTest { assertThat(documents.get(0).getMetadata()) .containsEntry(DocumentKnowledgeMetadataKeys.RETRIEVAL_INTENT, RetrievalIntent.TABLE.name()); assertThat(((Number) documents.get(0).getMetadata().get(DocumentKnowledgeMetadataKeys.CHANNEL_WEIGHT)).doubleValue()) - .isCloseTo(1.74D, org.assertj.core.data.Offset.offset(0.0001D)); + .isCloseTo(1.296D, org.assertj.core.data.Offset.offset(0.0001D)); } finally { executorService.shutdownNow(); @@ -259,6 +337,65 @@ class RagRetrievalEngineTest { } } + @Test + void finalReferencesFillKnowledgeBaseMetadataFromDocumentDescriptor() { + ChatRagProperties properties = new ChatRagProperties(); + properties.setRerankEnabled(false); + properties.setMinVectorSimilarity(0D); + properties.setKeywordRelativeScoreFloor(0D); + properties.setCandidateTopK(10); + properties.setFinalTopK(1); + + Document keywordDoc = document("chunk-9001", "值班 SRE 负责执行回滚演练。", 0.91D); + keywordDoc.getMetadata().put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "DOCUMENT"); + keywordDoc.getMetadata().put(DocumentKnowledgeMetadataKeys.CHANNEL, RetrievalChannelEnum.KEYWORD.getName()); + keywordDoc.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1001L); + keywordDoc.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, "生产发布回滚规范A.md"); + keywordDoc.getMetadata().put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, ""); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + try { + RagRetrievalEngine engine = new RagRetrievalEngine( + List.of(new StaticRetrievalChannel(RetrievalChannelEnum.KEYWORD.getName(), List.of(keywordDoc))), + properties, + null, + new DescriptorDocumentKnowledgeService(List.of(new KnowledgeDocumentDescriptor( + 1001L, + "生产发布回滚规范A.md", + 2001L, + 3001L, + "生产运维知识库" + ))), + executorService + ); + + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .mode(ExecutionMode.RETRIEVAL) + .retrievalQuestion("值班 SRE 负责什么?") + .retrievalSubQuestions(List.of("值班 SRE 负责什么?")) + .knowledgeBaseSelectionMode(KnowledgeBaseSelectionMode.SELECTED) + .selectedKnowledgeBaseIds(List.of(3001L)) + .build(); + + RagRetrievalContext context = engine.retrieve(plan, null); + + Document finalDocument = context.getSubQuestionEvidenceList().get(0).getDocuments().get(0); + assertThat(finalDocument.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, 3001L) + .containsEntry(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, "生产运维知识库"); + assertThat(context.getSubQuestionEvidenceList().get(0).getReferences()) + .hasSize(1) + .first() + .satisfies(reference -> { + assertThat(reference.getKnowledgeBaseId()).isEqualTo(3001L); + assertThat(reference.getKnowledgeBaseName()).isEqualTo("生产运维知识库"); + }); + } + finally { + executorService.shutdownNow(); + } + } + @Test void weightedHybridPreservesGraphRagMetadataWhenSameCandidateAlsoComesFromVector() { ChatRagProperties properties = new ChatRagProperties(); @@ -453,20 +590,20 @@ class RagRetrievalEngineTest { graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_ENTITY_ID, 1001L); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_ENTITY_NAME, "权限申请"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_ID, 2001L); - graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "APPROVES"); + graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "CALLS"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATED_ENTITY_ID, 1002L); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATED_ENTITY_NAME, "信息安全部"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID, 3001L); - graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_GRAPH_PATH, "二跳:AuditTrail --RECORDS--> 权限申请 --APPROVES--> 信息安全部"); - graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH, "AuditTrail --RECORDS--> 权限申请 --APPROVES--> 信息安全部"); + graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_GRAPH_PATH, "二跳:AuditTrail --RECORDS--> 权限申请 --CALLS--> 信息安全部"); + graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH, "AuditTrail --RECORDS--> 权限申请 --CALLS--> 信息安全部"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE, "java.graph_query_profile.v2,llm.controlled.query_plan.v1"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ANSWER_TYPES, "ORG"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ENTITIES, "审计系统"); - graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY, "PROCESS:权限申请->APPROVES->ORG:信息安全部"); + graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY, "PROCESS:权限申请->CALLS->ORG:信息安全部"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE, 0.84D); Document graphDoc = Document.builder() .id("graph-approval") - .text("GraphRAG: 权限申请 APPROVES 信息安全部。") + .text("GraphRAG: 权限申请 CALLS 信息安全部。") .metadata(graphMetadata) .score(0.60D) .build(); @@ -504,9 +641,219 @@ class RagRetrievalEngineTest { .findFirst() .orElseThrow(); assertThat(reservedGraphDocument.getMetadata()) - .containsEntry(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "APPROVES") + .containsEntry(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "CALLS") .containsEntry(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ANSWER_TYPES, "ORG") - .containsEntry(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH, "AuditTrail --RECORDS--> 权限申请 --APPROVES--> 信息安全部"); + .containsEntry(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH, "AuditTrail --RECORDS--> 权限申请 --CALLS--> 信息安全部"); + } + finally { + executorService.shutdownNow(); + } + } + + @Test + void finalEvidencePolicyKeepsSameSectionBodyAfterRerank() { + ChatRagProperties properties = new ChatRagProperties(); + properties.setRerankEnabled(false); + properties.setMinVectorSimilarity(0D); + properties.setKeywordRelativeScoreFloor(0D); + properties.setCandidateTopK(10); + properties.setFinalTopK(2); + properties.getHybrid().setOriginalScoreWeight(1D); + properties.getHybrid().setMetadataBoostWeight(0D); + + Document title = document("title", "# 14.1.2", 0.99D); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 101L); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.1.2"); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.1/14.1.2"); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE"); + + Document unrelated = document("unrelated", "其他章节内容。", 0.98D); + unrelated.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L); + unrelated.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 999L); + unrelated.getMetadata().put(DocumentKnowledgeMetadataKeys.SECTION_PATH, "12.3"); + + Document body = document("body", "1. 新版本切块异常。\n2. 父子块配置错误。\n3. 向量索引构建不完整。", 0.50D); + body.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L); + body.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 101L); + body.getMetadata().put(DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.1.2"); + body.getMetadata().put(DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.1/14.1.2"); + body.getMetadata().put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "BODY"); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + try { + RagRetrievalEngine engine = new RagRetrievalEngine( + List.of(new StaticRetrievalChannel(RetrievalChannelEnum.KEYWORD.getName(), List.of(title, unrelated, body))), + properties, + null, + new PassThroughDocumentKnowledgeService(), + executorService + ); + + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .mode(ExecutionMode.RETRIEVAL) + .retrievalIntent(RetrievalIntent.GENERAL) + .retrievalQuestion("14.1.2") + .retrievalSubQuestions(List.of("14.1.2")) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.DOCUMENT_QA) + .sectionAnchors(List.of("14.1.2")) + .confidence(0.84D) + .source("test") + .build()) + .build(); + + RagRetrievalContext context = engine.retrieve(plan, null); + + List documents = context.getSubQuestionEvidenceList().get(0).getDocuments(); + assertThat(documents).extracting(Document::getId).containsExactly("title", "body"); + assertThat(documents.get(1).getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "SAME_SECTION_BODY"); + } + finally { + executorService.shutdownNow(); + } + } + + @Test + void structureAnchorEvidenceBypassesReserveWindowAndReplacesTitleOnlyEvidence() { + ChatRagProperties properties = new ChatRagProperties(); + properties.setRerankEnabled(false); + properties.setMinVectorSimilarity(0D); + properties.setKeywordRelativeScoreFloor(0D); + properties.setCandidateTopK(10); + properties.setReserveCandidateTopK(2); + properties.setFinalTopK(2); + properties.getHybrid().setOriginalScoreWeight(1D); + properties.getHybrid().setMetadataBoostWeight(0D); + + Document title = document("title", "#### 14.3.1 检查顺序", 0.99D); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.TASK_ID, 11L); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 301L); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.3.1"); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.3/14.3.1"); + title.getMetadata().put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE"); + + Document unrelated = document("unrelated", "其他章节内容。", 0.98D); + unrelated.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L); + unrelated.getMetadata().put(DocumentKnowledgeMetadataKeys.TASK_ID, 11L); + unrelated.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 999L); + unrelated.getMetadata().put(DocumentKnowledgeMetadataKeys.SECTION_PATH, "12.3"); + + Document structureBody = document("structure-body", "1. 检查机器人策略。\n2. 检查知识召回质量。\n3. 检查人工排队规则。", 0.30D); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.TASK_ID, 11L); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9301L); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.CHUNK_ID, 930101L); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID, 301L); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.3.1"); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.CANONICAL_PATH, "14.3/14.3.1"); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "BODY"); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.CHANNEL, "structure-anchor"); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_ANCHOR_BODY_CANDIDATE"); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE, "NODE_ID"); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_RAW_BODY, true); + structureBody.getMetadata().put(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW, true); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + try { + RagRetrievalEngine engine = new RagRetrievalEngine( + List.of(new StaticRetrievalChannel(RetrievalChannelEnum.KEYWORD.getName(), List.of(title, unrelated))), + properties, + null, + new StructureExpansionDocumentKnowledgeService(List.of(structureBody)), + executorService + ); + + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .mode(ExecutionMode.RETRIEVAL) + .retrievalIntent(RetrievalIntent.GENERAL) + .retrievalQuestion("14.3.1") + .retrievalSubQuestions(List.of("14.3.1")) + .selectedDocumentId(1L) + .selectedTaskId(11L) + .retrievalDocumentIds(List.of(1L)) + .retrievalTaskIds(List.of(11L)) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.DOCUMENT_QA) + .sectionAnchors(List.of("14.3.1")) + .confidence(0.84D) + .source("test") + .build()) + .build(); + + RagRetrievalContext context = engine.retrieve(plan, null); + + List documents = context.getSubQuestionEvidenceList().get(0).getDocuments(); + assertThat(documents).extracting(Document::getId).contains("structure-body"); + Document selectedBody = documents.stream() + .filter(document -> "structure-body".equals(document.getId())) + .findFirst() + .orElseThrow(); + assertThat(selectedBody.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_ANCHOR_BODY") + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "REPLACED_TITLE_ONLY_WITH_BODY"); + assertThat(context.getRetrievalNotes()) + .anySatisfy(note -> assertThat(note).contains("结构锚点正文扩展命中 1 条")); + } + finally { + executorService.shutdownNow(); + } + } + + @Test + void finalEvidenceMarksExcludedOnlyEvidenceAsNotApplicable() { + ChatRagProperties properties = new ChatRagProperties(); + properties.setRerankEnabled(false); + properties.setMinVectorSimilarity(0D); + properties.setKeywordRelativeScoreFloor(0D); + properties.setCandidateTopK(10); + properties.setFinalTopK(1); + + Document excludedEvidence = document("excluded", "1. 先检查策略配置。\n2. 再检查服务队列。", 0.99D); + excludedEvidence.getMetadata().put(DocumentKnowledgeMetadataKeys.TITLE, "人工转接率异常升高"); + excludedEvidence.getMetadata().put(DocumentKnowledgeMetadataKeys.SECTION_PATH, "14.3.1"); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + try { + RagRetrievalEngine engine = new RagRetrievalEngine( + List.of(new StaticRetrievalChannel(RetrievalChannelEnum.KEYWORD.getName(), List.of(excludedEvidence))), + properties, + null, + new PassThroughDocumentKnowledgeService(), + executorService + ); + + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .mode(ExecutionMode.RETRIEVAL) + .retrievalQuestion("文档是否明确给出目标对象的检查顺序?") + .retrievalSubQuestions(List.of("文档是否明确给出目标对象的检查顺序?")) + .queryUnderstanding(QueryUnderstandingResult.builder() + .queryType(QueryType.DOCUMENT_QA) + .targetEntities(List.of("知识引用错误率突然升高")) + .excludedEntities(List.of("人工转接率异常升高")) + .negativeBoundary(true) + .answerExpectation("EXPLICIT_EVIDENCE_REQUIRED") + .confidence(0.9D) + .source("test") + .build()) + .build(); + + RagRetrievalContext context = engine.retrieve(plan, null); + + Document finalDocument = context.getSubQuestionEvidenceList().get(0).getDocuments().get(0); + assertThat(finalDocument.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY") + .containsEntry(DocumentKnowledgeMetadataKeys.EVIDENCE_APPLICABILITY_STATUS, "NOT_APPLICABLE"); + assertThat(context.getRetrievalNotes()) + .anySatisfy(note -> assertThat(note).contains("未明确支持当前目标对象")); + assertThat(context.getSubQuestionEvidenceList().get(0).getReferences()) + .singleElement() + .satisfies(reference -> { + assertThat(reference.getFinalSelectionReason()).isEqualTo("FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY"); + assertThat(reference.getEvidenceApplicabilityStatus()).isEqualTo("NOT_APPLICABLE"); + }); } finally { executorService.shutdownNow(); @@ -538,20 +885,20 @@ class RagRetrievalEngineTest { graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_ENTITY_ID, 1001L); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_ENTITY_NAME, "权限申请"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_ID, 2001L); - graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "APPROVES"); + graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "CALLS"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATED_ENTITY_ID, 1002L); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATED_ENTITY_NAME, "信息安全部"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID, 3001L); - graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_GRAPH_PATH, "二跳:AuditTrail --RECORDS--> 权限申请 --APPROVES--> 信息安全部"); - graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH, "AuditTrail --RECORDS--> 权限申请 --APPROVES--> 信息安全部"); + graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_GRAPH_PATH, "二跳:AuditTrail --RECORDS--> 权限申请 --CALLS--> 信息安全部"); + graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH, "AuditTrail --RECORDS--> 权限申请 --CALLS--> 信息安全部"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE, "java.graph_query_profile.v2,llm.controlled.query_plan.v1"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ANSWER_TYPES, "ORG"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ENTITIES, "审计系统"); - graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY, "PROCESS:权限申请->APPROVES->ORG:信息安全部"); + graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY, "PROCESS:权限申请->CALLS->ORG:信息安全部"); graphMetadata.put(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE, 0.84D); Document graphDoc = Document.builder() .id("graph-approval") - .text("GraphRAG: 权限申请 APPROVES 信息安全部。") + .text("GraphRAG: 权限申请 CALLS 信息安全部。") .metadata(graphMetadata) .score(0.60D) .build(); @@ -611,7 +958,7 @@ class RagRetrievalEngineTest { .orElseThrow(); assertThat(reservedGraphDocument.getMetadata()) .containsEntry(DocumentKnowledgeMetadataKeys.RERANK_RANK, 3) - .containsEntry(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "APPROVES") + .containsEntry(DocumentKnowledgeMetadataKeys.KG_RELATION_TYPE, "CALLS") .containsEntry(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ANSWER_TYPES, "ORG"); assertThat(context.getUsedChannels()).contains(RetrievalChannelEnum.RERANK.getName()); } @@ -961,6 +1308,142 @@ class RagRetrievalEngineTest { } } + @Test + void rerankCandidateWindowLimitsModelInputAndRecordsFilterReasons() { + ChatRagProperties properties = new ChatRagProperties(); + properties.setRerankEnabled(true); + properties.setMinVectorSimilarity(0D); + properties.setKeywordRelativeScoreFloor(0D); + properties.setCandidateTopK(5); + properties.setRerankCandidateTopK(2); + properties.setReserveCandidateTopK(2); + properties.setFinalTopK(1); + + Document first = document("doc-1", "第一条候选。", 0.99D); + first.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 1L); + Document second = document("doc-2", "第二条候选。", 0.98D); + second.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 2L); + Document third = document("doc-3", "第三条候选。", 0.97D); + third.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 3L); + + InMemoryRetrievalObserveStore observeStore = new InMemoryRetrievalObserveStore(); + ConversationTraceRecorder traceRecorder = new ConversationTraceRecorder( + null, + observeStore, + "conv-rerank-window", + 9001L, + "trace-rerank-window" + ); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + try { + RagRetrievalEngine engine = new RagRetrievalEngine( + List.of(new StaticRetrievalChannel(RetrievalChannelEnum.VECTOR.getName(), List.of(first, second, third))), + properties, + new RagRerankService(null, properties) { + @Override + public List rerank(String query, List candidates) { + assertThat(candidates).extracting(Document::getId).containsExactly("doc-1", "doc-2"); + for (int index = 0; index < candidates.size(); index++) { + Document candidate = candidates.get(index); + candidate.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_RANK, index + 1); + candidate.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_SCORE, 1.0D - index * 0.1D); + candidate.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_STATUS, "SUCCESS"); + } + return candidates; + } + }, + new PassThroughDocumentKnowledgeService(), + executorService + ); + + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .mode(ExecutionMode.RETRIEVAL) + .retrievalQuestion("候选窗口测试") + .retrievalSubQuestions(List.of("候选窗口测试")) + .build(); + + RagRetrievalContext context = engine.retrieve(plan, traceRecorder); + + assertThat(context.getSubQuestionEvidenceList().get(0).getRerankedCandidateCount()).isEqualTo(2); + assertThat(context.getSubQuestionEvidenceList().get(0).getDocuments()) + .extracting(Document::getId) + .containsExactly("doc-1"); + assertThat(reasonByDocumentId(observeStore.results, 1L)).isEqualTo("SELECTED_TOP_RANK"); + assertThat(reasonByDocumentId(observeStore.results, 2L)).isEqualTo("FILTERED_BY_FINAL_TOP_K"); + assertThat(reasonByDocumentId(observeStore.results, 3L)).isEqualTo("FILTERED_BY_RERANK_CANDIDATE_TOP_K"); + } + finally { + executorService.shutdownNow(); + } + } + + @Test + void autoDocumentRouteCandidateSourceEvidenceBypassesCandidateAndRerankWindows() { + ChatRagProperties properties = new ChatRagProperties(); + properties.setRerankEnabled(true); + properties.setMinVectorSimilarity(0D); + properties.setKeywordRelativeScoreFloor(0D); + properties.setCandidateTopK(2); + properties.setRerankCandidateTopK(2); + properties.setReserveCandidateTopK(2); + properties.setFinalTopK(2); + properties.getHybrid().setOriginalScoreWeight(1D); + properties.getHybrid().setMetadataBoostWeight(0D); + + Document docAFirst = sourceChunk("doc-a-1", 10L, 1001L, "发布流程背景证据。", 0.99D); + Document docASecond = sourceChunk("doc-a-2", 10L, 1002L, "发布验证背景证据。", 0.98D); + Document docBTarget = sourceChunk("doc-b-target", 20L, 2001L, "NovaRAG 检索服务降级时,应先启用缓存,再关闭重排序,缩小检索范围,最后人工接管。", 0.50D); + + ExecutorService executorService = Executors.newFixedThreadPool(2); + try { + RagRetrievalEngine engine = new RagRetrievalEngine( + List.of(new StaticRetrievalChannel(RetrievalChannelEnum.VECTOR.getName(), List.of(docAFirst, docASecond, docBTarget))), + properties, + new RagRerankService(null, properties) { + @Override + public List rerank(String query, List candidates) { + assertThat(candidates).extracting(Document::getId) + .containsExactly("doc-a-1", "doc-a-2", "doc-b-target"); + docAFirst.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_RANK, 1); + docASecond.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_RANK, 2); + docBTarget.getMetadata().put(DocumentKnowledgeMetadataKeys.RERANK_RANK, 3); + return List.of(docAFirst, docASecond, docBTarget); + } + }, + new PassThroughDocumentKnowledgeService(), + executorService + ); + + ConversationExecutionPlan plan = ConversationExecutionPlan.builder() + .mode(ExecutionMode.RETRIEVAL) + .chatMode(ChatQueryMode.AUTO_DOCUMENT) + .knowledgeBaseSelectionMode(KnowledgeBaseSelectionMode.SELECTED) + .selectedKnowledgeBaseIds(List.of(3001L)) + .retrievalDocumentIds(List.of(10L, 20L)) + .retrievalTaskIds(List.of(110L, 220L)) + .retrievalQuestion("NovaRAG 检索服务降级时,按什么顺序处理?") + .retrievalSubQuestions(List.of("NovaRAG 检索服务降级时,按什么顺序处理?")) + .build(); + + RagRetrievalContext context = engine.retrieve(plan, null); + + assertThat(context.getSubQuestionEvidenceList().get(0).getRerankedCandidateCount()).isEqualTo(3); + List documents = context.getSubQuestionEvidenceList().get(0).getDocuments(); + assertThat(documents).extracting(Document::getId).contains("doc-b-target"); + Document selectedTarget = documents.stream() + .filter(document -> "doc-b-target".equals(document.getId())) + .findFirst() + .orElseThrow(); + assertThat(selectedTarget.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "ROUTE_CANDIDATE_SOURCE") + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, "SELECTED_ROUTE_CANDIDATE_RESERVE"); + } + finally { + executorService.shutdownNow(); + } + } + private static Document document(String id, String text, double score) { LinkedHashMap metadata = new LinkedHashMap<>(); metadata.put(DocumentKnowledgeMetadataKeys.SCORE, score); @@ -971,6 +1454,48 @@ class RagRetrievalEngineTest { .build(); } + private static Document sourceChunk(String id, Long documentId, Long chunkId, String text, double score) { + Document document = document(id, text, score); + document.getMetadata().put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "DOCUMENT"); + document.getMetadata().put(DocumentKnowledgeMetadataKeys.CHANNEL, RetrievalChannelEnum.VECTOR.getName()); + document.getMetadata().put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, documentId); + document.getMetadata().put(DocumentKnowledgeMetadataKeys.CHUNK_ID, chunkId); + document.getMetadata().put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TEXT"); + return document; + } + + private static SuperAgentDocumentStructureNode structureNode(Long id, + Long documentId, + Long parseTaskId, + Integer nodeNo, + String title, + Long parentNodeId, + Long prevSiblingNodeId, + Long nextSiblingNodeId, + String sectionPath, + String canonicalPath) { + SuperAgentDocumentStructureNode node = new SuperAgentDocumentStructureNode(); + node.setId(id); + node.setDocumentId(documentId); + node.setParseTaskId(parseTaskId); + node.setNodeNo(nodeNo); + node.setTitle(title); + node.setParentNodeId(parentNodeId); + node.setPrevSiblingNodeId(prevSiblingNodeId); + node.setNextSiblingNodeId(nextSiblingNodeId); + node.setSectionPath(sectionPath); + node.setCanonicalPath(canonicalPath); + return node; + } + + private static String reasonByDocumentId(List results, Long documentId) { + return results.stream() + .filter(result -> documentId.equals(result.getDocumentId())) + .findFirst() + .map(RetrievalResultView::getSelectionReason) + .orElseThrow(); + } + private static Document graphRagDocument(String id, String text, double score, @@ -1060,6 +1585,11 @@ class RagRetrievalEngineTest { return List.of(); } + @Override + public List listRetrievableDocumentsByKnowledgeBaseIds(java.util.Collection knowledgeBaseIds) { + return List.of(); + } + @Override public List vectorSearch(DocumentRetrieveRequest request) { return List.of(); @@ -1075,4 +1605,46 @@ class RagRetrievalEngineTest { return childDocuments; } } + + private static class StructureExpansionDocumentKnowledgeService extends PassThroughDocumentKnowledgeService { + + private final List expanded; + + private StructureExpansionDocumentKnowledgeService(List expanded) { + this.expanded = expanded; + } + + @Override + public List expandStructureAnchoredEvidence(StructureAnchoredEvidenceRequest request) { + assertThat(request.getDocumentIds()).containsExactly(1L); + assertThat(request.getTaskIds()).containsExactly(11L); + assertThat(request.getStructureNodeIds()).contains(301L); + return expanded; + } + } + + private static class DescriptorDocumentKnowledgeService extends PassThroughDocumentKnowledgeService { + + private final List descriptors; + + private DescriptorDocumentKnowledgeService(List descriptors) { + this.descriptors = descriptors; + } + + @Override + public List listRetrievableDocuments() { + return descriptors; + } + + @Override + public List listRetrievableDocumentsByKnowledgeBaseIds(java.util.Collection knowledgeBaseIds) { + if (knowledgeBaseIds == null || knowledgeBaseIds.isEmpty()) { + return List.of(); + } + return descriptors.stream() + .filter(descriptor -> descriptor.getKnowledgeBaseId() != null + && knowledgeBaseIds.contains(descriptor.getKnowledgeBaseId())) + .toList(); + } + } } diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/StructureNavigationResolverTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/StructureNavigationResolverTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a3634df0d62b05c3bd835a4ba1956f0c6f89b9d4 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/service/StructureNavigationResolverTest.java @@ -0,0 +1,184 @@ +package org.javaup.ai.chatagent.rag.service; + +import org.javaup.ai.chatagent.rag.model.ConversationStructureAnchor; +import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent; +import org.javaup.ai.chatagent.rag.model.StructureNavigationOperation; +import org.javaup.ai.chatagent.rag.model.StructureNavigationResult; +import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode; +import org.javaup.ai.manage.service.DocumentStructureNodeService; +import org.javaup.ai.manage.support.DocumentStructureNodeCandidate; +import org.junit.jupiter.api.Test; + +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +class StructureNavigationResolverTest { + + @Test + void resolvesParentAndNextSiblingFromStructureNodeAnchor() { + InMemoryStructureNodeService nodeService = new InMemoryStructureNodeService(List.of( + node(130L, 10L, 20L, 130, "十三、上线观察与值班规则", null, null, null, + "十三、上线观察与值班规则", "十三、上线观察与值班规则"), + node(131L, 10L, 20L, 131, "13.1 观察时长", 130L, null, 132L, + "十三、上线观察与值班规则 > 13.1 观察时长", "十三、上线观察与值班规则/13.1 观察时长"), + node(132L, 10L, 20L, 132, "13.2 值班安排", 130L, 131L, null, + "十三、上线观察与值班规则 > 13.2 值班安排", "十三、上线观察与值班规则/13.2 值班安排") + )); + StructureNavigationResolver resolver = new StructureNavigationResolver(nodeService); + + StructureNavigationResult result = resolver.resolve( + 10L, + 20L, + StructureNavigationIntent.builder() + .anchorStructureNodeId(131L) + .operations(List.of(StructureNavigationOperation.SECTION_WITH_SIBLINGS)) + .confidence(0.91D) + .source("test") + .build(), + null + ); + + assertThat(result.isDeterministic()).isTrue(); + assertThat(result.getCurrent().getTitle()).isEqualTo("13.1 观察时长"); + assertThat(result.getParent().getTitle()).isEqualTo("十三、上线观察与值班规则"); + assertThat(result.getNextSibling().getTitle()).isEqualTo("13.2 值班安排"); + assertThat(result.getPreviousSibling()).isNull(); + } + + @Test + void resolvesDirectChildrenFromConversationAnchor() { + InMemoryStructureNodeService nodeService = new InMemoryStructureNodeService(List.of( + node(100L, 10L, 20L, 100, "十、机器人策略设计", null, null, null, + "十、机器人策略设计", "十、机器人策略设计"), + node(101L, 10L, 20L, 101, "10.1 策略层次", 100L, null, 102L, + "十、机器人策略设计 > 10.1 策略层次", "十、机器人策略设计/10.1 策略层次"), + node(102L, 10L, 20L, 102, "10.2 必配策略项", 100L, 101L, 103L, + "十、机器人策略设计 > 10.2 必配策略项", "十、机器人策略设计/10.2 必配策略项"), + node(103L, 10L, 20L, 103, "10.3 不建议交给机器人直接回答的主题", 100L, 102L, null, + "十、机器人策略设计 > 10.3 不建议交给机器人直接回答的主题", "十、机器人策略设计/10.3 不建议交给机器人直接回答的主题") + )); + StructureNavigationResolver resolver = new StructureNavigationResolver(nodeService); + + StructureNavigationResult result = resolver.resolve( + 10L, + 20L, + StructureNavigationIntent.builder() + .operations(List.of(StructureNavigationOperation.SECTION_WITH_CHILDREN)) + .confidence(0.88D) + .source("test") + .build(), + ConversationStructureAnchor.builder() + .structureNodeId(100L) + .canonicalPath("十、机器人策略设计") + .scopeMode("HARD") + .build() + ); + + assertThat(result.isDeterministic()).isTrue(); + assertThat(result.getCurrent().getTitle()).isEqualTo("十、机器人策略设计"); + assertThat(result.getDirectChildren()) + .extracting(SuperAgentDocumentStructureNode::getTitle) + .containsExactly("10.1 策略层次", "10.2 必配策略项", "10.3 不建议交给机器人直接回答的主题"); + } + + private static SuperAgentDocumentStructureNode node(Long id, + Long documentId, + Long parseTaskId, + Integer nodeNo, + String title, + Long parentNodeId, + Long prevSiblingNodeId, + Long nextSiblingNodeId, + String sectionPath, + String canonicalPath) { + SuperAgentDocumentStructureNode node = new SuperAgentDocumentStructureNode(); + node.setId(id); + node.setDocumentId(documentId); + node.setParseTaskId(parseTaskId); + node.setNodeNo(nodeNo); + node.setTitle(title); + node.setParentNodeId(parentNodeId); + node.setPrevSiblingNodeId(prevSiblingNodeId); + node.setNextSiblingNodeId(nextSiblingNodeId); + node.setSectionPath(sectionPath); + node.setCanonicalPath(canonicalPath); + return node; + } + + private static final class InMemoryStructureNodeService implements DocumentStructureNodeService { + + private final Map nodes = new LinkedHashMap<>(); + + private InMemoryStructureNodeService(List nodes) { + for (SuperAgentDocumentStructureNode node : nodes) { + this.nodes.put(node.getId(), node); + } + } + + @Override + public List replaceDocumentNodes(Long documentId, + Long parseTaskId, + List candidates) { + return List.of(); + } + + @Override + public List listDocumentNodes(Long documentId, Long parseTaskId) { + return nodes.values().stream() + .filter(node -> Objects.equals(node.getDocumentId(), documentId)) + .filter(node -> parseTaskId == null || Objects.equals(node.getParseTaskId(), parseTaskId)) + .sorted(Comparator.comparing(SuperAgentDocumentStructureNode::getNodeNo)) + .toList(); + } + + @Override + public Map nodeMap(Long documentId, Long parseTaskId) { + Map result = new LinkedHashMap<>(); + for (SuperAgentDocumentStructureNode node : listDocumentNodes(documentId, parseTaskId)) { + result.put(node.getId(), node); + } + return result; + } + + @Override + public List listChildren(Long documentId, Long parseTaskId, Long parentNodeId) { + return listDocumentNodes(documentId, parseTaskId).stream() + .filter(node -> Objects.equals(node.getParentNodeId(), parentNodeId)) + .sorted(Comparator.comparing(SuperAgentDocumentStructureNode::getNodeNo)) + .toList(); + } + + @Override + public SuperAgentDocumentStructureNode findById(Long documentId, Long parseTaskId, Long nodeId) { + SuperAgentDocumentStructureNode node = nodes.get(nodeId); + if (node == null || !Objects.equals(node.getDocumentId(), documentId)) { + return null; + } + if (parseTaskId != null && !Objects.equals(node.getParseTaskId(), parseTaskId)) { + return null; + } + return node; + } + + @Override + public SuperAgentDocumentStructureNode findPreviousSibling(Long documentId, Long parseTaskId, Long nodeId) { + SuperAgentDocumentStructureNode node = findById(documentId, parseTaskId, nodeId); + return node == null ? null : findById(documentId, parseTaskId, node.getPrevSiblingNodeId()); + } + + @Override + public SuperAgentDocumentStructureNode findNextSibling(Long documentId, Long parseTaskId, Long nodeId) { + SuperAgentDocumentStructureNode node = findById(documentId, parseTaskId, nodeId); + return node == null ? null : findById(documentId, parseTaskId, node.getNextSiblingNodeId()); + } + + @Override + public void deleteByDocumentId(Long documentId) { + } + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/support/EvidenceIdentityResolverTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/support/EvidenceIdentityResolverTest.java new file mode 100644 index 0000000000000000000000000000000000000000..15e281f769f9a7fd92e6403e33479a7924ae0ae3 --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/chatagent/rag/support/EvidenceIdentityResolverTest.java @@ -0,0 +1,165 @@ +package org.javaup.ai.chatagent.rag.support; + +import org.javaup.ai.chatagent.model.SearchReference; +import org.javaup.ai.chatagent.rag.model.CitationEvidenceType; +import org.javaup.ai.chatagent.rag.model.EvidenceIdentity; +import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; +import org.junit.jupiter.api.Test; +import org.springframework.ai.document.Document; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class EvidenceIdentityResolverTest { + + @Test + void titleAndBodyUnderSameParentHaveDifferentCitationIdentity() { + Document title = document("title", "# 14.1.2 可能原因", metadata( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 10L, + DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9001L, + DocumentKnowledgeMetadataKeys.CHUNK_ID, 1001L, + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE" + )); + Document body = document("body", "1. 新版本切块异常。\n2. 向量索引构建不完整。", metadata( + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 10L, + DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9001L, + DocumentKnowledgeMetadataKeys.CHUNK_ID, 1002L, + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TEXT" + )); + + assertThat(EvidenceIdentityResolver.citationIdentity(title)).isNull(); + EvidenceIdentity bodyIdentity = EvidenceIdentityResolver.citationIdentity(body); + assertThat(bodyIdentity.type()).isEqualTo(CitationEvidenceType.CHUNK); + assertThat(bodyIdentity.value()).isEqualTo("CHUNK:10:1002"); + assertThat(EvidenceIdentityResolver.contextIdentity(title).value()).isEqualTo("PARENT:10:9001"); + assertThat(EvidenceIdentityResolver.contextIdentity(body).value()).isEqualTo("PARENT:10:9001"); + assertThat(EvidenceIdentityResolver.sameCitationEvidence(title, body)).isFalse(); + assertThat(EvidenceIdentityResolver.sameContext(title, body)).isTrue(); + } + + @Test + void searchReferenceTitleChunkIsContextOnlyEvenWhenChunkIdExists() { + SearchReference reference = new SearchReference(); + reference.setSourceType("DOCUMENT"); + reference.setDocumentId(10L); + reference.setParentBlockId(9001L); + reference.setChunkId(1001L); + reference.setChunkType("TITLE"); + + assertThat(EvidenceIdentityResolver.citationIdentity(reference)).isNull(); + assertThat(EvidenceIdentityResolver.isContextOnly(reference)).isTrue(); + assertThat(reference.uniqueKey()).isEqualTo("PARENT:10:9001"); + } + + @Test + void graphRagWrapperIsContextOnlyButQuoteSourceIsCitationCapable() { + Document wrapper = document("kg-wrapper", "[GraphRAG entity] 服务 A 关联团队 B", metadata( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "GRAPH_RAG", + DocumentKnowledgeMetadataKeys.CHANNEL, "graph-rag", + DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID, 3001L + )); + Document quote = document("kg-quote", "原文:服务 A 由团队 B 维护。", metadata( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "GRAPH_RAG", + DocumentKnowledgeMetadataKeys.CHANNEL, "graph-rag", + DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID, 3001L, + DocumentKnowledgeMetadataKeys.CHUNK_ID, 5001L, + DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET, "服务 A 由团队 B 维护。" + )); + + assertThat(EvidenceIdentityResolver.citationIdentity(wrapper)).isNull(); + assertThat(EvidenceIdentityResolver.isContextOnly(wrapper)).isTrue(); + EvidenceIdentity quoteIdentity = EvidenceIdentityResolver.citationIdentity(quote); + assertThat(quoteIdentity.type()).isEqualTo(CitationEvidenceType.KG_QUOTE_SOURCE); + assertThat(quoteIdentity.value()).isEqualTo("KG_QUOTE:3001:CHUNK:5001"); + } + + @Test + void raptorSummaryIsContextOnlyButSourceChunkIsCitationCapable() { + Document summary = document("raptor-summary", "RAPTOR 摘要:上线后需要观察指标。", metadata( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "RAPTOR", + DocumentKnowledgeMetadataKeys.CHANNEL, "raptor", + DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID, 7001L, + DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS, "SUMMARY_ONLY", + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "RAPTOR_SUMMARY" + )); + Document sourceChunk = document("raptor-source", "原文:上线后观察回答准确率和人工转接率。", metadata( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "RAPTOR", + DocumentKnowledgeMetadataKeys.CHANNEL, "raptor", + DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID, 7001L, + DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS, "SOURCE_CHUNK", + DocumentKnowledgeMetadataKeys.CHUNK_ID, 8001L, + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "RAPTOR_SOURCE_CHUNK" + )); + + assertThat(EvidenceIdentityResolver.citationIdentity(summary)).isNull(); + assertThat(EvidenceIdentityResolver.isContextOnly(summary)).isTrue(); + EvidenceIdentity sourceIdentity = EvidenceIdentityResolver.citationIdentity(sourceChunk); + assertThat(sourceIdentity.type()).isEqualTo(CitationEvidenceType.RAPTOR_SOURCE_CHUNK); + assertThat(sourceIdentity.value()).isEqualTo("RAPTOR_SOURCE:7001:8001"); + } + + @Test + void tableEvidenceUsesCellOrRowIdentity() { + Document table = document("table", "表格结果:研发部报销金额合计 1200", metadata( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "DOCUMENT_TABLE", + DocumentKnowledgeMetadataKeys.TABLE_ID, 60L, + DocumentKnowledgeMetadataKeys.TABLE_EVIDENCE_CELL_IDS, List.of(401L, 402L), + DocumentKnowledgeMetadataKeys.TABLE_EVIDENCE_ROW_IDS, List.of(301L) + )); + + EvidenceIdentity identity = EvidenceIdentityResolver.citationIdentity(table); + + assertThat(identity.type()).isEqualTo(CitationEvidenceType.TABLE_CELL_OR_ROW); + assertThat(identity.value()).isEqualTo("TABLE:60:CELLS:[401, 402]"); + } + + @Test + void mapperCarriesChunkTypeIntoSearchReferenceIdentity() { + Document title = document("title", "# 标题", metadata( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "DOCUMENT", + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 10L, + DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9001L, + DocumentKnowledgeMetadataKeys.CHUNK_ID, 1001L, + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TITLE" + )); + Document body = document("body", "正文内容可以被引用。", metadata( + DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "DOCUMENT", + DocumentKnowledgeMetadataKeys.DOCUMENT_ID, 10L, + DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, 9001L, + DocumentKnowledgeMetadataKeys.CHUNK_ID, 1002L, + DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "TEXT" + )); + + SearchReference titleReference = SearchReferenceMapper.fromDocument(title, 0, "问题", 1); + SearchReference bodyReference = SearchReferenceMapper.fromDocument(body, 0, "问题", 2); + + assertThat(titleReference.getChunkType()).isEqualTo("TITLE"); + assertThat(titleReference.isContextOnly()).isTrue(); + assertThat(titleReference.getCitationEvidenceType()).isEqualTo("CONTEXT_ONLY"); + assertThat(titleReference.getCitationIdentity()).isEmpty(); + assertThat(titleReference.uniqueKey()).isEqualTo("PARENT:10:9001"); + assertThat(bodyReference.getChunkType()).isEqualTo("TEXT"); + assertThat(bodyReference.isContextOnly()).isFalse(); + assertThat(bodyReference.getCitationEvidenceType()).isEqualTo("CHUNK"); + assertThat(bodyReference.getCitationIdentity()).isEqualTo("CHUNK:10:1002"); + } + + private static Document document(String id, String text, Map metadata) { + return Document.builder() + .id(id) + .text(text) + .metadata(metadata) + .build(); + } + + private static Map metadata(Object... values) { + LinkedHashMap metadata = new LinkedHashMap<>(); + for (int index = 0; index + 1 < values.length; index += 2) { + metadata.put(String.valueOf(values[index]), values[index + 1]); + } + return metadata; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/DocumentKnowledgeServiceImplTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/DocumentKnowledgeServiceImplTest.java index 1c76a9754cc4dcc1d1062a54217e768fa1cec320..98e8dab3f81610415a1ec9c9356ffcb6314f6b7f 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/DocumentKnowledgeServiceImplTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/DocumentKnowledgeServiceImplTest.java @@ -1,9 +1,13 @@ package org.javaup.ai.manage.service.impl; import com.fasterxml.jackson.databind.ObjectMapper; +import org.javaup.ai.manage.data.SuperAgentDocumentChunk; import org.javaup.ai.manage.data.SuperAgentDocumentParentBlock; +import org.javaup.ai.manage.data.SuperAgentDocument; +import org.javaup.ai.manage.mapper.SuperAgentDocumentChunkMapper; import org.javaup.ai.manage.mapper.SuperAgentDocumentMapper; import org.javaup.ai.manage.mapper.SuperAgentDocumentParentBlockMapper; +import org.javaup.ai.manage.model.StructureAnchoredEvidenceRequest; import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys; import org.javaup.ai.manage.support.GraphRagTypedChunkMetadataSupport; import org.junit.jupiter.api.Test; @@ -73,6 +77,7 @@ class DocumentKnowledgeServiceImplTest { DocumentKnowledgeServiceImpl service = new DocumentKnowledgeServiceImpl( mapper(SuperAgentDocumentMapper.class, List.of()), parentBlockMapper, + mapper(SuperAgentDocumentChunkMapper.class, List.of()), null, null, null, @@ -155,6 +160,7 @@ class DocumentKnowledgeServiceImplTest { DocumentKnowledgeServiceImpl service = new DocumentKnowledgeServiceImpl( mapper(SuperAgentDocumentMapper.class, List.of()), mapper(SuperAgentDocumentParentBlockMapper.class, List.of(parentBlock)), + mapper(SuperAgentDocumentChunkMapper.class, List.of()), null, null, null, @@ -184,6 +190,129 @@ class DocumentKnowledgeServiceImplTest { assertThat(metadata.get(DocumentKnowledgeMetadataKeys.KG_DEGREE)).isEqualTo(5); } + @Test + void expandStructureAnchoredEvidenceUsesStructureMetadataAndKnowledgeBaseBoundary() { + SuperAgentDocument document = new SuperAgentDocument(); + document.setId(10L); + document.setDocumentName("测试文档"); + document.setLastIndexTaskId(20L); + document.setKnowledgeBaseId(100L); + document.setKnowledgeBaseName("测试知识库"); + + SuperAgentDocumentParentBlock exact = new SuperAgentDocumentParentBlock(); + exact.setId(1003L); + exact.setDocumentId(10L); + exact.setTaskId(20L); + exact.setParentNo(6); + exact.setSectionPath("14.1.2"); + exact.setCanonicalPath("14.1/14.1.2"); + exact.setStructureNodeId(202L); + exact.setParentText("# 14.1.2 可能原因"); + + SuperAgentDocumentParentBlock descendant = new SuperAgentDocumentParentBlock(); + descendant.setId(1004L); + descendant.setDocumentId(10L); + descendant.setTaskId(20L); + descendant.setParentNo(7); + descendant.setSectionPath("14.1.3"); + descendant.setCanonicalPath("14.1/14.1.3"); + descendant.setStructureNodeId(203L); + descendant.setParentText("1. 先回滚切块策略。\n2. 再重建索引。"); + + SuperAgentDocumentChunk exactText = chunk(2003L, 10L, 20L, 1003L, 9, 202L, "14.1.2", "14.1/14.1.2", "TEXT", + "1. 新版本切块异常。\n2. 父子块配置错误。\n3. 向量索引构建不完整。"); + SuperAgentDocumentChunk descendantText = chunk(2004L, 10L, 20L, 1004L, 10, 203L, "14.1.3", "14.1/14.1.3", "TEXT", + "1. 先回滚切块策略。\n2. 再重建索引。"); + + DocumentKnowledgeServiceImpl service = new DocumentKnowledgeServiceImpl( + mapper(SuperAgentDocumentMapper.class, List.of(document)), + mapper(SuperAgentDocumentParentBlockMapper.class, List.of(exact, descendant)), + mapper(SuperAgentDocumentChunkMapper.class, List.of(exactText, descendantText)), + null, + null, + null, + new GraphRagTypedChunkMetadataSupport(new ObjectMapper()) + ); + + List expanded = service.expandStructureAnchoredEvidence(StructureAnchoredEvidenceRequest.builder() + .documentIds(List.of(10L)) + .taskIds(List.of(20L)) + .knowledgeBaseIds(List.of(100L)) + .structureNodeIds(List.of(202L)) + .canonicalPaths(List.of("14.1")) + .maxPerAnchor(2) + .maxTotal(4) + .maxChars(512) + .build()); + + assertThat(expanded).extracting(Document::getId) + .contains("structure-chunk-2003", "structure-chunk-2004"); + Document exactEvidence = expanded.stream() + .filter(documentItem -> "structure-chunk-2003".equals(documentItem.getId())) + .findFirst() + .orElseThrow(); + assertThat(exactEvidence.getText()).contains("父子块配置错误"); + assertThat(exactEvidence.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.CHANNEL, "structure-anchor") + .containsEntry(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, 100L) + .containsEntry(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, "STRUCTURE_ANCHOR_BODY_CANDIDATE") + .containsEntry(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_RAW_BODY, true) + .containsEntry(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW, true); + } + + @Test + void expandStructureAnchoredEvidenceUsesContinuationListWhenAnchorParentIsTitleOnly() { + SuperAgentDocument document = new SuperAgentDocument(); + document.setId(10L); + document.setDocumentName("测试文档"); + document.setLastIndexTaskId(20L); + document.setKnowledgeBaseId(100L); + document.setKnowledgeBaseName("测试知识库"); + + SuperAgentDocumentParentBlock titleOnly = new SuperAgentDocumentParentBlock(); + titleOnly.setId(1003L); + titleOnly.setDocumentId(10L); + titleOnly.setTaskId(20L); + titleOnly.setParentNo(6); + titleOnly.setSectionPath("14.1.2"); + titleOnly.setCanonicalPath("14.1/14.1.2"); + titleOnly.setStructureNodeId(202L); + titleOnly.setParentText("# 14.1.2 可能原因"); + + SuperAgentDocumentChunk titleChunk = chunk(2003L, 10L, 20L, 1003L, 9, 202L, "14.1.2", "14.1/14.1.2", "TITLE", + "# 14.1.2 可能原因"); + SuperAgentDocumentChunk continuationList = chunk(2004L, 10L, 20L, 1004L, 10, 203L, "14.1.2", "14.1/14.1.2", "TITLE", + "1. 新版本切块异常。 2. 父子块配置错误。 3. 向量索引构建不完整。 4. 检索过滤条件误收紧。"); + + DocumentKnowledgeServiceImpl service = new DocumentKnowledgeServiceImpl( + mapper(SuperAgentDocumentMapper.class, List.of(document)), + mapper(SuperAgentDocumentParentBlockMapper.class, List.of(titleOnly)), + mapper(SuperAgentDocumentChunkMapper.class, List.of(titleChunk, continuationList)), + null, + null, + null, + new GraphRagTypedChunkMetadataSupport(new ObjectMapper()) + ); + + List expanded = service.expandStructureAnchoredEvidence(StructureAnchoredEvidenceRequest.builder() + .documentIds(List.of(10L)) + .taskIds(List.of(20L)) + .knowledgeBaseIds(List.of(100L)) + .structureNodeIds(List.of(202L)) + .maxPerAnchor(2) + .maxTotal(4) + .maxChars(512) + .build()); + + assertThat(expanded).extracting(Document::getId).containsExactly("structure-chunk-2004"); + Document evidence = expanded.get(0); + assertThat(evidence.getText()).contains("父子块配置错误").doesNotContain("# 14.1.2"); + assertThat(evidence.getMetadata()) + .containsEntry(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "LIST") + .containsEntry(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_RAW_BODY, true) + .containsEntry(DocumentKnowledgeMetadataKeys.STRUCTURE_BODY_RESOLVED_FROM, "CONTINUATION_LIST"); + } + private static Map metadata(Object... values) { java.util.LinkedHashMap result = new java.util.LinkedHashMap<>(); for (int index = 0; index + 1 < values.length; index += 2) { @@ -192,6 +321,32 @@ class DocumentKnowledgeServiceImplTest { return result; } + private static SuperAgentDocumentChunk chunk(Long id, + Long documentId, + Long taskId, + Long parentBlockId, + Integer chunkNo, + Long structureNodeId, + String sectionPath, + String canonicalPath, + String chunkType, + String chunkText) { + SuperAgentDocumentChunk chunk = new SuperAgentDocumentChunk(); + chunk.setId(id); + chunk.setDocumentId(documentId); + chunk.setTaskId(taskId); + chunk.setParentBlockId(parentBlockId); + chunk.setChunkNo(chunkNo); + chunk.setSourceType(1); + chunk.setSectionPath(sectionPath); + chunk.setStructureNodeId(structureNodeId); + chunk.setCanonicalPath(canonicalPath); + chunk.setChunkType(chunkType); + chunk.setChunkText(chunkText); + chunk.setContentWithWeight(chunkText); + return chunk; + } + @SuppressWarnings("unchecked") private static T mapper(Class mapperType, List selectListResult) { return (T) Proxy.newProxyInstance( diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/DocumentStrategyServiceImplTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/DocumentStrategyServiceImplTest.java index 3f9b224257855f118da1eb788af47c09c65f2fca..d4e555f32394b0fd2dbc5fc5c463e4b197bff909 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/DocumentStrategyServiceImplTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/DocumentStrategyServiceImplTest.java @@ -7,6 +7,7 @@ import org.javaup.ai.manage.data.SuperAgentDocumentBlock; import org.javaup.ai.manage.data.SuperAgentDocumentStrategyPlan; import org.javaup.ai.manage.data.SuperAgentDocumentStrategyStep; import org.javaup.ai.manage.service.DocumentStructureNodeService; +import org.javaup.ai.manage.support.KnowledgeBaseIndexingConfigResolver; import org.javaup.ai.manage.support.ParentBlockCandidate; import org.javaup.ai.prompt.PromptTemplateService; import org.javaup.enums.DocumentStrategyPipelineTypeEnum; @@ -32,7 +33,8 @@ class DocumentStrategyServiceImplTest { new ObjectMapper(), new EmptyChatModelProvider(), new EmptyDocumentStructureNodeService(), - new PromptTemplateService(null) + new PromptTemplateService(null), + new KnowledgeBaseIndexingConfigResolver(new DocumentManageProperties()) ); SuperAgentDocument document = new SuperAgentDocument(); diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagCrossDocumentIndexServiceImplTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagCrossDocumentIndexServiceImplTest.java index a8833c22983c5d241e640066a338e046c6fa9067..3dc1e61e0ee6096c2e7b0caf407384c3c77c207e 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagCrossDocumentIndexServiceImplTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagCrossDocumentIndexServiceImplTest.java @@ -3,6 +3,7 @@ package org.javaup.ai.manage.service.impl; import com.baidu.fsg.uid.UidGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import org.javaup.ai.manage.data.SuperAgentDocument; +import org.javaup.ai.manage.data.SuperAgentKnowledgeTopicNode; import org.javaup.ai.manage.data.SuperAgentKgCanonicalEntityGroup; import org.javaup.ai.manage.data.SuperAgentKgCanonicalEntityMember; import org.javaup.ai.manage.data.SuperAgentKgCrossDocumentCommunity; @@ -12,7 +13,9 @@ import org.javaup.ai.manage.data.SuperAgentKgEvidence; import org.javaup.ai.manage.data.SuperAgentKgRelation; import org.javaup.ai.manage.data.SuperAgentKgRelationGroup; import org.javaup.ai.manage.data.SuperAgentKgRelationGroupMember; +import org.javaup.ai.manage.data.SuperAgentTopicDocumentRelation; import org.javaup.ai.manage.mapper.SuperAgentDocumentMapper; +import org.javaup.ai.manage.mapper.SuperAgentKnowledgeTopicNodeMapper; import org.javaup.ai.manage.mapper.SuperAgentKgCanonicalEntityGroupMapper; import org.javaup.ai.manage.mapper.SuperAgentKgCanonicalEntityMemberMapper; import org.javaup.ai.manage.mapper.SuperAgentKgCrossDocumentCommunityMapper; @@ -22,6 +25,7 @@ import org.javaup.ai.manage.mapper.SuperAgentKgEvidenceMapper; import org.javaup.ai.manage.mapper.SuperAgentKgRelationGroupMapper; import org.javaup.ai.manage.mapper.SuperAgentKgRelationGroupMemberMapper; import org.javaup.ai.manage.mapper.SuperAgentKgRelationMapper; +import org.javaup.ai.manage.mapper.SuperAgentTopicDocumentRelationMapper; import org.javaup.ai.manage.model.graph.GraphRagCrossDocumentIndexBuildResult; import org.javaup.ai.manage.support.GraphRagCrossDocumentIndex; import org.javaup.ai.manage.support.GraphRagCrossDocumentIndexSupport; @@ -42,9 +46,9 @@ class GraphRagCrossDocumentIndexServiceImplTest { @Test void rebuildAllPersistsGlobalAndKnowledgeScopeDerivedIndexThenLoadsKnowledgeScope() { InMemoryMapper documentStore = new InMemoryMapper<>(List.of( - document(10L, "security"), - document(11L, "security"), - document(12L, "release") + document(10L), + document(11L), + document(12L) )); InMemoryMapper entityStore = new InMemoryMapper<>(List.of( entity(1001L, 10L, 20L, "AuditTrail", null, "SYSTEM", "{\"aliases\":[\"审计系统\"],\"rankBoost\":0.7}"), @@ -69,6 +73,15 @@ class GraphRagCrossDocumentIndexServiceImplTest { InMemoryMapper relationGroupMemberStore = new InMemoryMapper<>(List.of()); InMemoryMapper communityStore = new InMemoryMapper<>(List.of()); InMemoryMapper communityMemberStore = new InMemoryMapper<>(List.of()); + InMemoryMapper topicStore = new InMemoryMapper<>(List.of( + topic(1L, "security-topic", 101L), + topic(2L, "release-topic", 102L) + )); + InMemoryMapper topicDocumentRelationStore = new InMemoryMapper<>(List.of( + topicRelation(1L, 1L, 10L), + topicRelation(2L, 1L, 11L), + topicRelation(3L, 2L, 12L) + )); ObjectMapper objectMapper = new ObjectMapper(); GraphRagCrossDocumentIndexServiceImpl service = new GraphRagCrossDocumentIndexServiceImpl( documentStore.proxy(SuperAgentDocumentMapper.class), @@ -81,6 +94,8 @@ class GraphRagCrossDocumentIndexServiceImplTest { relationGroupMemberStore.proxy(SuperAgentKgRelationGroupMemberMapper.class), communityStore.proxy(SuperAgentKgCrossDocumentCommunityMapper.class), communityMemberStore.proxy(SuperAgentKgCrossDocumentCommunityMemberMapper.class), + topicStore.proxy(SuperAgentKnowledgeTopicNodeMapper.class), + topicDocumentRelationStore.proxy(SuperAgentTopicDocumentRelationMapper.class), new GraphRagCrossDocumentIndexSupport(objectMapper), uidGenerator(), objectMapper @@ -89,17 +104,17 @@ class GraphRagCrossDocumentIndexServiceImplTest { List results = service.rebuildAll(); assertThat(results).extracting(GraphRagCrossDocumentIndexBuildResult::getScopeKey) - .containsExactly("global", "knowledge:security", "knowledge:release"); + .containsExactly("global", "kb:1", "kb:1:scope:101", "kb:1:scope:102"); assertThat(canonicalGroupStore.items()).extracting(SuperAgentKgCanonicalEntityGroup::getScopeKey) - .contains("global", "knowledge:security", "knowledge:release"); + .contains("global", "kb:1", "kb:1:scope:101", "kb:1:scope:102"); assertThat(canonicalMemberStore.items()).extracting(SuperAgentKgCanonicalEntityMember::getScopeKey) - .contains("global", "knowledge:security", "knowledge:release"); + .contains("global", "kb:1", "kb:1:scope:101", "kb:1:scope:102"); assertThat(relationGroupStore.items()).extracting(SuperAgentKgRelationGroup::getScopeKey) - .contains("global", "knowledge:security"); + .contains("global", "kb:1", "kb:1:scope:101"); assertThat(communityStore.items()).extracting(SuperAgentKgCrossDocumentCommunity::getScopeKey) - .contains("global", "knowledge:security"); + .contains("global", "kb:1", "kb:1:scope:101"); assertThat(communityMemberStore.items()).extracting(SuperAgentKgCrossDocumentCommunityMember::getScopeKey) - .contains("global", "knowledge:security"); + .contains("global", "kb:1", "kb:1:scope:101"); assertThat(communityStore.items()).allSatisfy(community -> { Map metadata = objectMapper.readValue(community.getMetadataJson(), Map.class); assertThat(metadata).containsEntry("sourceType", "java.cross_document_community.v1"); @@ -150,7 +165,7 @@ class GraphRagCrossDocumentIndexServiceImplTest { assertThat(communityMemberStore.deleteCount()).isEqualTo(1); GraphRagCrossDocumentIndexServiceImpl loadService = new GraphRagCrossDocumentIndexServiceImpl( - new InMemoryMapper<>(List.of(document(10L, "security"), document(11L, "security"))).proxy(SuperAgentDocumentMapper.class), + new InMemoryMapper<>(List.of(document(10L), document(11L))).proxy(SuperAgentDocumentMapper.class), new InMemoryMapper<>(entityStore.items()).proxy(SuperAgentKgEntityMapper.class), new InMemoryMapper<>(List.of( relation(2001L, 10L, 20L, 1001L, 1002L, "RECORDS"), @@ -158,23 +173,28 @@ class GraphRagCrossDocumentIndexServiceImplTest { )).proxy(SuperAgentKgRelationMapper.class), new InMemoryMapper<>(evidenceStore.items()).proxy(SuperAgentKgEvidenceMapper.class), new InMemoryMapper<>(canonicalGroupStore.items().stream() - .filter(group -> "knowledge:security".equals(group.getScopeKey())) + .filter(group -> "kb:1:scope:101".equals(group.getScopeKey())) .toList()).proxy(SuperAgentKgCanonicalEntityGroupMapper.class), new InMemoryMapper<>(canonicalMemberStore.items().stream() - .filter(member -> "knowledge:security".equals(member.getScopeKey())) + .filter(member -> "kb:1:scope:101".equals(member.getScopeKey())) .toList()).proxy(SuperAgentKgCanonicalEntityMemberMapper.class), new InMemoryMapper<>(relationGroupStore.items().stream() - .filter(group -> "knowledge:security".equals(group.getScopeKey())) + .filter(group -> "kb:1:scope:101".equals(group.getScopeKey())) .toList()).proxy(SuperAgentKgRelationGroupMapper.class), new InMemoryMapper<>(relationGroupMemberStore.items().stream() - .filter(member -> "knowledge:security".equals(member.getScopeKey())) + .filter(member -> "kb:1:scope:101".equals(member.getScopeKey())) .toList()).proxy(SuperAgentKgRelationGroupMemberMapper.class), new InMemoryMapper<>(communityStore.items().stream() - .filter(community -> "knowledge:security".equals(community.getScopeKey())) + .filter(community -> "kb:1:scope:101".equals(community.getScopeKey())) .toList()).proxy(SuperAgentKgCrossDocumentCommunityMapper.class), new InMemoryMapper<>(communityMemberStore.items().stream() - .filter(member -> "knowledge:security".equals(member.getScopeKey())) + .filter(member -> "kb:1:scope:101".equals(member.getScopeKey())) .toList()).proxy(SuperAgentKgCrossDocumentCommunityMemberMapper.class), + new InMemoryMapper<>(List.of(topic(1L, "security-topic", 101L))).proxy(SuperAgentKnowledgeTopicNodeMapper.class), + new InMemoryMapper<>(List.of( + topicRelation(1L, 1L, 10L), + topicRelation(2L, 1L, 11L) + )).proxy(SuperAgentTopicDocumentRelationMapper.class), new GraphRagCrossDocumentIndexSupport(objectMapper), uidGenerator(), objectMapper @@ -220,8 +240,8 @@ class GraphRagCrossDocumentIndexServiceImplTest { @Test void rebuildAllDoesNotMergeUnrelatedEntitiesByGeneratedCanonicalKeyOnly() { InMemoryMapper documentStore = new InMemoryMapper<>(List.of( - document(10L, "security"), - document(11L, "security") + document(10L), + document(11L) )); InMemoryMapper entityStore = new InMemoryMapper<>(List.of( entity(1001L, 10L, 20L, "AuditTrail", null, "CONCEPT", "{\"canonicalKey\":\"ENT_SHARED\",\"rankBoost\":0.5}"), @@ -243,6 +263,8 @@ class GraphRagCrossDocumentIndexServiceImplTest { new InMemoryMapper(List.of()).proxy(SuperAgentKgRelationGroupMemberMapper.class), new InMemoryMapper(List.of()).proxy(SuperAgentKgCrossDocumentCommunityMapper.class), new InMemoryMapper(List.of()).proxy(SuperAgentKgCrossDocumentCommunityMemberMapper.class), + new InMemoryMapper(List.of()).proxy(SuperAgentKnowledgeTopicNodeMapper.class), + new InMemoryMapper(List.of()).proxy(SuperAgentTopicDocumentRelationMapper.class), new GraphRagCrossDocumentIndexSupport(objectMapper), uidGenerator(), objectMapper @@ -263,7 +285,7 @@ class GraphRagCrossDocumentIndexServiceImplTest { @Test void rebuildAllPersistsEntityQualityNoiseReasonsForSentenceLikeNames() throws Exception { InMemoryMapper documentStore = new InMemoryMapper<>(List.of( - document(10L, "security") + document(10L) )); InMemoryMapper entityStore = new InMemoryMapper<>(List.of( entity( @@ -298,6 +320,8 @@ class GraphRagCrossDocumentIndexServiceImplTest { new InMemoryMapper(List.of()).proxy(SuperAgentKgRelationGroupMemberMapper.class), new InMemoryMapper(List.of()).proxy(SuperAgentKgCrossDocumentCommunityMapper.class), new InMemoryMapper(List.of()).proxy(SuperAgentKgCrossDocumentCommunityMemberMapper.class), + new InMemoryMapper(List.of()).proxy(SuperAgentKnowledgeTopicNodeMapper.class), + new InMemoryMapper(List.of()).proxy(SuperAgentTopicDocumentRelationMapper.class), new GraphRagCrossDocumentIndexSupport(objectMapper), uidGenerator(), objectMapper @@ -321,14 +345,36 @@ class GraphRagCrossDocumentIndexServiceImplTest { assertThat(sentenceLikeGroup.getRankScore()).isLessThan(auditGroup.getRankScore()); } - private static SuperAgentDocument document(Long id, String knowledgeScopeCode) { + private static SuperAgentDocument document(Long id) { SuperAgentDocument document = new SuperAgentDocument(); document.setId(id); - document.setKnowledgeScopeCode(knowledgeScopeCode); + document.setKnowledgeBaseId(1L); + document.setKnowledgeBaseName("测试知识库"); document.setStatus(BusinessStatus.YES.getCode()); return document; } + private static SuperAgentKnowledgeTopicNode topic(Long id, String topicName, Long scopeId) { + SuperAgentKnowledgeTopicNode topic = new SuperAgentKnowledgeTopicNode(); + topic.setId(id); + topic.setKnowledgeBaseId(1L); + topic.setTopicName(topicName); + topic.setScopeId(scopeId); + topic.setStatus(BusinessStatus.YES.getCode()); + return topic; + } + + private static SuperAgentTopicDocumentRelation topicRelation(Long id, Long topicId, Long documentId) { + SuperAgentTopicDocumentRelation relation = new SuperAgentTopicDocumentRelation(); + relation.setId(id); + relation.setKnowledgeBaseId(1L); + relation.setTopicId(topicId); + relation.setDocumentId(documentId); + relation.setRelationScore(BigDecimal.ONE); + relation.setStatus(BusinessStatus.YES.getCode()); + return relation; + } + private static SuperAgentKgEntity entity(Long id, Long documentId, Long taskId, diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagSearchServiceImplTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagSearchServiceImplTest.java index bb621ed308887945c5f6832143d4ff9b9c956055..0dce4911eaf7de6130eb171cf4f2300ad588c84c 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagSearchServiceImplTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagSearchServiceImplTest.java @@ -1547,6 +1547,66 @@ class GraphRagSearchServiceImplTest { assertThat(results).isEmpty(); } + @Test + void controlledRelationProfilePromotesResponsibilityQuoteOverWeakRelationAndEntityAlias() { + SuperAgentKgEntity serviceEntity = entity(9001L, 90L, 91L, "PaymentService", "PaySvc", "SYSTEM", "PaymentService handles payments."); + SuperAgentKgEntity ownerTeam = entity(9002L, 90L, 91L, "OwnerTeam", null, "ORG", "OwnerTeam maintains services."); + SuperAgentKgEntity auditLog = entity(9003L, 90L, 91L, "AuditLog", null, "PROCESS", "AuditLog stores events."); + SuperAgentKgRelation strongRelation = relation(9101L, 90L, 91L, 9001L, 9002L, "RESPONSIBLE_FOR", + "PaymentService is maintained by OwnerTeam.", 0.35D); + SuperAgentKgRelation weakRelation = relation(9102L, 90L, 91L, 9001L, 9003L, "ASSOCIATED_WITH", + "PaymentService is associated with AuditLog.", 1.0D); + SuperAgentKgEvidence strongEvidence = evidence(9201L, 90L, 91L, 9301L, 9401L, 9101L, null, + "PaymentService is maintained by OwnerTeam.", 2); + SuperAgentKgEvidence weakEvidence = evidence(9202L, 90L, 91L, 9302L, 9402L, 9102L, null, + "PaymentService sends audit events to AuditLog.", 3); + SuperAgentKgEvidence entityEvidence = evidence(9203L, 90L, 91L, 9303L, 9403L, null, 9001L, + "PaySvc is another name for PaymentService.", 4); + AtomicInteger advisorCallCount = new AtomicInteger(); + GraphRagQueryPlanAdvisor advisor = (question, catalog) -> { + advisorCallCount.incrementAndGet(); + return Optional.of(GraphRagQueryPlanAdvice.builder() + .graphQuery(true) + .entitiesFromQuery(List.of("PaymentService")) + .entityNames(List.of("PaymentService")) + .answerTypeKeywords(List.of("ORG")) + .relationTypes(List.of("RESPONSIBLE_FOR")) + .relationQuestion(true) + .maxHops(1) + .confidence(0.91D) + .reason("问题询问责任主体,Java 只采纳 KG 中存在的 RESPONSIBLE_FOR") + .build()); + }; + + GraphRagSearchServiceImpl service = new GraphRagSearchServiceImpl( + mapper(SuperAgentKgEntityMapper.class, List.of(serviceEntity, ownerTeam, auditLog), null), + mapper(SuperAgentKgRelationMapper.class, List.of(strongRelation, weakRelation), null), + mapper(SuperAgentKgEvidenceMapper.class, List.of(strongEvidence, weakEvidence, entityEvidence), null), + mapper(SuperAgentKgCommunityMapper.class, List.of(), null), + new ObjectMapper(), + advisor + ); + + List results = service.search( + "PaymentService 的责任主体是谁?", + List.of(90L), + List.of(91L), + 3, + 1 + ); + + assertThat(advisorCallCount).hasValue(1); + assertThat(results).isNotEmpty(); + assertThat(results.get(0).getRelationId()).isEqualTo(9101L); + assertThat(results.get(0).getRelationType()).isEqualTo("RESPONSIBLE_FOR"); + assertThat(results) + .extracting(GraphRagSearchResult::getRelationId) + .contains(9102L); + assertThat(results) + .filteredOn(item -> Long.valueOf(9203L).equals(item.getEvidenceId())) + .isNotEmpty(); + } + private static SuperAgentKgEntity entity(Long id, Long documentId, Long taskId, diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagTypedChunkServiceImplTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagTypedChunkServiceImplTest.java index d4e8fa1f8a2cde823a298a27cacf813e6eb33d15..0444d670f9d00571f7c8ca48a7e2f811321fddce 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagTypedChunkServiceImplTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/service/impl/GraphRagTypedChunkServiceImplTest.java @@ -83,6 +83,33 @@ class GraphRagTypedChunkServiceImplTest { .containsEntry(DocumentKnowledgeMetadataKeys.KG_RANK_BOOST, 0.82); } + @Test + void enrichMetadataDoesNotOverwriteKnowledgeBaseMetadataWithLegacyBlankValues() { + ObjectMapper objectMapper = new ObjectMapper(); + GraphRagTypedChunkMetadataSupport metadataSupport = new GraphRagTypedChunkMetadataSupport(objectMapper); + Map legacySourceMetadata = new java.util.LinkedHashMap<>(); + legacySourceMetadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, ""); + legacySourceMetadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, ""); + legacySourceMetadata.put(DocumentKnowledgeMetadataKeys.KG_ENTITY_NAME, "ReleaseControl"); + + Map metadata = new java.util.LinkedHashMap<>(); + metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "DOCUMENT"); + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, 1001L); + metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, "生产运维知识库"); + + metadataSupport.enrichMetadata( + metadata, + GraphRagTypedChunkMetadataSupport.CHUNK_TYPE_ENTITY, + metadataSupport.writeSourceMetadata(legacySourceMetadata) + ); + + assertThat(metadata) + .containsEntry(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, "GRAPH_RAG") + .containsEntry(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, 1001L) + .containsEntry(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, "生产运维知识库") + .containsEntry(DocumentKnowledgeMetadataKeys.KG_ENTITY_NAME, "ReleaseControl"); + } + private static List entities(ObjectMapper objectMapper) { return List.of( entity(1001L, "SuperAgent", "SYSTEM", "超级智能体主链路。", diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/DocumentStructureSignalExtractorTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/DocumentStructureSignalExtractorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..cb89fb35f7273dbafa84a4f67b28d0695375699a --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/DocumentStructureSignalExtractorTest.java @@ -0,0 +1,56 @@ +package org.javaup.ai.manage.support; + +import org.javaup.ai.manage.config.DocumentManageProperties; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class DocumentStructureSignalExtractorTest { + + @Test + void markdownHeadingFollowedByOrderedListKeepsListItemsAsListItems() { + DocumentStructureSignalExtractor extractor = new DocumentStructureSignalExtractor( + new DocumentManageProperties(), + new DocumentLineClassifier() + ); + + DocumentStructureSignalBatch batch = extractor.extract("测试文档", """ + # 14.1.2 可能原因 + 1. 新版本切块异常。 + 2. 父子块配置错误。 + 3. 向量索引构建不完整。 + """); + + List signals = batch.signals(); + assertThat(signals) + .filteredOn(signal -> signal.getLineNo() > 0) + .filteredOn(signal -> signal.getKind() != DocumentStructureSignalKind.BLANK) + .extracting(DocumentStructureSignal::getKind) + .containsExactly( + DocumentStructureSignalKind.HEADING, + DocumentStructureSignalKind.LIST_ITEM, + DocumentStructureSignalKind.LIST_ITEM, + DocumentStructureSignalKind.LIST_ITEM + ); + } + + @Test + void collapsedOrderedListIsNotPromotedToHeadingCandidate() { + DocumentStructureSignalExtractor extractor = new DocumentStructureSignalExtractor( + new DocumentManageProperties(), + new DocumentLineClassifier() + ); + + DocumentStructureSignalBatch batch = extractor.extract("测试文档", + "1. 新版本切块异常。 2. 父子块配置错误。 3. 向量索引构建不完整。"); + + DocumentStructureSignal signal = batch.signals().stream() + .filter(item -> item.getLineNo() > 0) + .findFirst() + .orElseThrow(); + assertThat(signal.getKind()).isEqualTo(DocumentStructureSignalKind.LIST_ITEM); + assertThat(signal.getReasons()).contains("collapsed-ordered-list"); + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/KnowledgeBaseIndexingConfigResolverTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/KnowledgeBaseIndexingConfigResolverTest.java new file mode 100644 index 0000000000000000000000000000000000000000..74f8387d341dcfb90c74f3b4fbfdaa312886bd4a --- /dev/null +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/KnowledgeBaseIndexingConfigResolverTest.java @@ -0,0 +1,124 @@ +package org.javaup.ai.manage.support; + +import org.javaup.ai.manage.config.DocumentManageProperties; +import org.javaup.ai.manage.data.SuperAgentKnowledgeBase; +import org.javaup.ai.manage.model.KnowledgeBaseIndexingOptions; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class KnowledgeBaseIndexingConfigResolverTest { + + @Test + void resolvesNestedIndexingGraphAndRaptorBuildConfig() { + KnowledgeBaseIndexingConfigResolver resolver = new KnowledgeBaseIndexingConfigResolver(defaultProperties()); + SuperAgentKnowledgeBase knowledgeBase = new SuperAgentKnowledgeBase(); + knowledgeBase.setId(1L); + knowledgeBase.setBaseName("测试库"); + knowledgeBase.setRetrievalConfigJson(""" + { + "vectorTopK": 8, + "indexing": { + "childRecursiveMaxChars": 640, + "childRecursiveOverlapChars": 80, + "childSemanticMaxChars": 600, + "childSemanticMinChars": 180, + "childSemanticSimilarityThreshold": 0.22, + "parentBlockMaxChars": 1800, + "parentBlockOverlapChars": 160, + "parentSemanticMaxChars": 1500, + "parentSemanticMinChars": 360 + } + } + """); + knowledgeBase.setGraphRagConfigJson(""" + { + "graphRagTopK": 6, + "build": { + "graphRagBuildEnabled": false + } + } + """); + knowledgeBase.setRaptorConfigJson(""" + { + "raptorTopK": 5, + "build": { + "raptorBuildEnabled": true, + "raptorMaxClusterSize": 8, + "raptorMaxLevels": 4, + "raptorLlmSummaryEnabled": false, + "raptorSummaryQualityFloor": 0.55 + } + } + """); + + KnowledgeBaseIndexingOptions options = resolver.resolve(knowledgeBase); + + assertThat(options.getChunk().getChildRecursiveMaxChars()).isEqualTo(640); + assertThat(options.getChunk().getChildRecursiveOverlapChars()).isEqualTo(80); + assertThat(options.getChunk().getChildSemanticMaxChars()).isEqualTo(600); + assertThat(options.getChunk().getChildSemanticMinChars()).isEqualTo(180); + assertThat(options.getChunk().getChildSemanticSimilarityThreshold()).isEqualTo(0.22D); + assertThat(options.getChunk().getParentBlockMaxChars()).isEqualTo(1800); + assertThat(options.getChunk().getParentBlockOverlapChars()).isEqualTo(160); + assertThat(options.getChunk().getParentSemanticMaxChars()).isEqualTo(1500); + assertThat(options.getChunk().getParentSemanticMinChars()).isEqualTo(360); + assertThat(options.getGraphRag().getGraphRagBuildEnabled()).isFalse(); + assertThat(options.getRaptor().getRaptorBuildEnabled()).isTrue(); + assertThat(options.getRaptor().getRaptorMaxClusterSize()).isEqualTo(8); + assertThat(options.getRaptor().getRaptorMaxLevels()).isEqualTo(4); + assertThat(options.getRaptor().getRaptorLlmSummaryEnabled()).isFalse(); + assertThat(options.getRaptor().getRaptorSummaryQualityFloor()).isEqualTo(0.55D); + } + + @Test + void clampsUnsafeValuesToUsableRanges() { + KnowledgeBaseIndexingConfigResolver resolver = new KnowledgeBaseIndexingConfigResolver(defaultProperties()); + SuperAgentKnowledgeBase knowledgeBase = new SuperAgentKnowledgeBase(); + knowledgeBase.setRetrievalConfigJson(""" + { + "indexing": { + "childRecursiveMaxChars": 10, + "childRecursiveOverlapChars": 9999, + "childSemanticSimilarityThreshold": 9, + "parentBlockMaxChars": 200, + "parentBlockOverlapChars": 9999 + } + } + """); + knowledgeBase.setRaptorConfigJson(""" + { + "build": { + "raptorMaxClusterSize": 1, + "raptorMaxLevels": 99, + "raptorSummaryQualityFloor": -1 + } + } + """); + + KnowledgeBaseIndexingOptions options = resolver.resolve(knowledgeBase); + + assertThat(options.getChunk().getChildRecursiveMaxChars()).isEqualTo(100); + assertThat(options.getChunk().getChildRecursiveOverlapChars()).isEqualTo(99); + assertThat(options.getChunk().getChildSemanticSimilarityThreshold()).isEqualTo(1D); + assertThat(options.getChunk().getParentBlockMaxChars()).isEqualTo(300); + assertThat(options.getChunk().getParentBlockOverlapChars()).isEqualTo(299); + assertThat(options.getRaptor().getRaptorMaxClusterSize()).isEqualTo(2); + assertThat(options.getRaptor().getRaptorMaxLevels()).isEqualTo(8); + assertThat(options.getRaptor().getRaptorSummaryQualityFloor()).isZero(); + } + + private static DocumentManageProperties defaultProperties() { + DocumentManageProperties properties = new DocumentManageProperties(); + properties.getChunk().setRecursiveMaxChars(800); + properties.getChunk().setRecursiveOverlapChars(120); + properties.getChunk().setSemanticMaxChars(700); + properties.getChunk().setSemanticMinChars(240); + properties.getChunk().setSemanticSimilarityThreshold(0.18D); + properties.getChunk().setParentBlockMaxChars(2200); + properties.getChunk().setParentBlockOverlapChars(180); + properties.getChunk().setParentSemanticMaxChars(1600); + properties.getChunk().setParentSemanticMinChars(480); + return properties; + } +} diff --git a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/RaptorScopeSupportTest.java b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/RaptorScopeSupportTest.java index e9e3f168bcdf7e504c81c1c34cd227042f746440..7621a5c63e22415212bf9abe4bf1e8cc287f29b4 100644 --- a/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/RaptorScopeSupportTest.java +++ b/super-agent-business/super-agent-business-chat/src/test/java/org/javaup/ai/manage/support/RaptorScopeSupportTest.java @@ -10,28 +10,34 @@ import static org.assertj.core.api.Assertions.assertThat; class RaptorScopeSupportTest { @Test - void buildsKnowledgeAndGlobalScopeKeysFromJavaSelectedDocuments() { - SuperAgentDocument release = document(1L, "release"); - SuperAgentDocument qa = document(2L, "qa"); - SuperAgentDocument blank = document(3L, " "); + void buildsKnowledgeBaseAwareScopeKeysFromSelectedDocuments() { + SuperAgentDocument release = document(1L); + release.setKnowledgeBaseId(10L); + SuperAgentDocument qa = document(2L); + qa.setKnowledgeBaseId(10L); + SuperAgentDocument blank = document(3L); + blank.setKnowledgeBaseId(20L); List scopeKeys = RaptorScopeSupport.searchScopeKeys(List.of(release, qa, blank)); - assertThat(scopeKeys).containsExactly("knowledge:release", "knowledge:qa", "global"); + assertThat(scopeKeys).containsExactly( + "kb:10", + "kb:20" + ); } @Test void keepsDocumentScopeSeparateFromDatasetScope() { assertThat(RaptorScopeSupport.documentScopeKey(99L)).isEqualTo("document:99"); - assertThat(RaptorScopeSupport.knowledgeScopeKey(" Release Ops ")).isEqualTo("knowledge:release_ops"); + assertThat(RaptorScopeSupport.knowledgeBaseScopeKey(10L)).isEqualTo("kb:10"); + assertThat(RaptorScopeSupport.knowledgeScopeKey(10L, 30L)).isEqualTo("kb:10:scope:30"); assertThat(RaptorScopeSupport.isDatasetScope(RaptorScopeSupport.SCOPE_TYPE_DATASET)).isTrue(); assertThat(RaptorScopeSupport.isDatasetScope(RaptorScopeSupport.SCOPE_TYPE_DOCUMENT)).isFalse(); } - private static SuperAgentDocument document(Long id, String scopeCode) { + private static SuperAgentDocument document(Long id) { SuperAgentDocument document = new SuperAgentDocument(); document.setId(id); - document.setKnowledgeScopeCode(scopeCode); return document; } } diff --git a/super-agent-common/super-agent-common-frame/src/main/java/org/javaup/enums/KnowledgeBaseSelectionMode.java b/super-agent-common/super-agent-common-frame/src/main/java/org/javaup/enums/KnowledgeBaseSelectionMode.java new file mode 100644 index 0000000000000000000000000000000000000000..d89db2e37abcd9c89e39416d22efa6d30606bf56 --- /dev/null +++ b/super-agent-common/super-agent-common-frame/src/main/java/org/javaup/enums/KnowledgeBaseSelectionMode.java @@ -0,0 +1,31 @@ +package org.javaup.enums; + +import lombok.Getter; + +@Getter +public enum KnowledgeBaseSelectionMode { + + NONE("不使用知识库检索"), + + ALL("使用全部启用知识库"), + + SELECTED("使用显式选择知识库"); + + private final String label; + + KnowledgeBaseSelectionMode(String label) { + this.label = label; + } + + public static KnowledgeBaseSelectionMode fromName(String value) { + if (value == null || value.isBlank()) { + return NONE; + } + for (KnowledgeBaseSelectionMode mode : values()) { + if (mode.name().equalsIgnoreCase(value.trim())) { + return mode; + } + } + throw new IllegalArgumentException("未知的知识库选择模式: " + value); + } +} diff --git a/vue/src/api/api.js b/vue/src/api/api.js index cfa398e74824a4dbe565ac5dc15322301921fbde..7ba57d6290fd9fa6ba0af73aa4b10fe4a0f12382 100644 --- a/vue/src/api/api.js +++ b/vue/src/api/api.js @@ -315,6 +315,13 @@ export const chatApi = { }) }, + listKnowledgeBaseOptions() { + return requestApiEnvelope('/api/chat/knowledge-base/options', { + method: 'POST', + body: {} + }) + }, + listSessions(query = {}) { return chatApi.listSessionsPage({ keyword: query.keyword || '', @@ -482,23 +489,55 @@ export const adminAuthApi = { } export const manageApi = { - uploadDocument({ file, documentName, operatorId, knowledgeScopeCode, knowledgeScopeName, businessCategory, documentTags }) { + uploadDocument({ file, documentName, operatorId, knowledgeBaseId }) { const formData = new FormData() formData.append('file', file) const meta = stringifyManageValue({ documentName: documentName || '', operatorId: operatorId ?? '', - knowledgeScopeCode: knowledgeScopeCode || '', - knowledgeScopeName: knowledgeScopeName || '', - businessCategory: businessCategory || '', - documentTags: documentTags || '' + knowledgeBaseId: knowledgeBaseId || '' }) formData.append('meta', new Blob([JSON.stringify(meta)], { type: 'application/json' })) return requestMultipartApiEnvelope('/manage/document/upload', formData) }, + saveKnowledgeBase(payload) { + return requestApiEnvelope('/manage/knowledge/base/save', { + method: 'POST', + body: stringifyManageValue(payload) + }) + }, + + deleteKnowledgeBase(payload) { + return requestApiEnvelope('/manage/knowledge/base/delete', { + method: 'POST', + body: stringifyManageValue(payload) + }) + }, + + listKnowledgeBases() { + return requestApiEnvelope('/manage/knowledge/base/list', { + method: 'POST', + body: {} + }) + }, + + queryKnowledgeBaseDetail(payload) { + return requestApiEnvelope('/manage/knowledge/base/detail', { + method: 'POST', + body: stringifyManageValue(payload) + }) + }, + + updateKnowledgeBaseConfig(payload) { + return requestApiEnvelope('/manage/knowledge/base/config/update', { + method: 'POST', + body: stringifyManageValue(payload) + }) + }, + queryDocumentPage(payload) { return requestApiEnvelope('/manage/document/page/query', { method: 'POST', @@ -625,10 +664,10 @@ export const manageApi = { }) }, - listKnowledgeScopes() { + listKnowledgeScopes(payload = {}) { return requestApiEnvelope('/manage/knowledge/scope/list', { method: 'POST', - body: {} + body: stringifyManageValue(payload) }) }, diff --git a/vue/src/components/Chat.vue b/vue/src/components/Chat.vue index 7139ffa9a457bff338b723e969d5feed9eda7889..3c4acb6b72fc03cd8e0f6518cf484c2c300cbe18 100644 --- a/vue/src/components/Chat.vue +++ b/vue/src/components/Chat.vue @@ -68,14 +68,14 @@
- {{ item.scopeName || item.scopeCode }} · {{ item.scoreText }} + {{ item.scopeName || `范围 ${item.scopeId || index + 1}` }} · {{ item.scoreText }}
- {{ item.topicName || item.topicCode }} · {{ item.scoreText }} + {{ item.topicName || `主题 ${item.topicId || index + 1}` }} · {{ item.scoreText }}
diff --git a/vue/src/router/index.js b/vue/src/router/index.js index 5fdfbcfd69cfb4ace1b358673e88ab43890121ba..2ac668c9671222549136904ddab0410e416846ee 100644 --- a/vue/src/router/index.js +++ b/vue/src/router/index.js @@ -62,6 +62,14 @@ const router = createRouter({ title: '文档详情' } }, + { + path: 'knowledge-bases', + name: 'AdminKnowledgeBases', + component: () => import('../views/admin/AdminKnowledgeBaseView.vue'), + meta: { + title: '知识库管理' + } + }, { path: 'knowledge-route', name: 'AdminKnowledgeRoute', diff --git a/vue/src/views/BusinessChatView.vue b/vue/src/views/BusinessChatView.vue index 742a97f371d315995b0ec99f8f6c144d10772c80..1d5cb864c028c3e21709253cbfb41d0404735cc8 100644 --- a/vue/src/views/BusinessChatView.vue +++ b/vue/src/views/BusinessChatView.vue @@ -92,6 +92,55 @@ 正在生成回答... +
+ 知识库 +
+ + +
+ +
+
+ + 暂无可用知识库,可在管理端创建 + +
+
回答模式
@@ -102,28 +151,15 @@
-
- - - +
+ 提问文档 + + 当前文档:{{ selectedDocumentName }} + 当前文档模式需要先选择知识库 + 请先选择一个文档再发送问题
+
+
+
+ + +
+ +
+
+ + +
+ + + +
+
+
问答运行时 RAG 参数
+

影响新对话的召回、融合、精排候选和最终证据预算。

+
+ +
+
检索窗口与阈值
+
+
+ + +
+
+
+ +
+
通道开关
+
+ +
+
+ +
+
GraphRAG 与 RAPTOR 查询
+
+
+ + +
+
+
+ +
+
混合融合权重
+
+
+ + +
+
+
+
+ +
+
+
解析与索引构建参数
+

影响新上传或重新构建索引后的块、GraphRAG 与 RAPTOR 产物。

+
+ +
+
ChildChunk 参数
+
+
+ + +
+
+
+ +
+
ParentBlock 参数
+
+
+ + +
+
+
+ +
+
构建通道
+
+ +
+
+ +
+
RAPTOR 构建
+
+
+ + +
+
+
+
+ + + +
+ + +
+ + + + +
+
+ + + + + + + + diff --git a/vue/src/views/admin/AdminKnowledgeRouteTraceView.vue b/vue/src/views/admin/AdminKnowledgeRouteTraceView.vue index b1f9df89115b9d876c3b72e83fbfbabaedf6e072..b9a272d19532c726a694bd1cc49f40db237be13a 100644 --- a/vue/src/views/admin/AdminKnowledgeRouteTraceView.vue +++ b/vue/src/views/admin/AdminKnowledgeRouteTraceView.vue @@ -276,12 +276,12 @@ const candidateGroups = computed(() => { return [ { title: '范围候选', count: selectedRecord.value.scopes.length, - items: selectedRecord.value.scopes.map((c) => ({ name: c.scopeName || c.scopeCode, scoreText: c.scoreText })), + items: selectedRecord.value.scopes.map((c, index) => ({ name: c.scopeName || `范围 ${c.scopeId || index + 1}`, scoreText: c.scoreText })), empty: '当前没有显式范围候选。' }, { title: '主题候选', count: selectedRecord.value.topics.length, - items: selectedRecord.value.topics.map((c) => ({ name: c.topicName || c.topicCode, scoreText: c.scoreText })), + items: selectedRecord.value.topics.map((c, index) => ({ name: c.topicName || `主题 ${c.topicId || index + 1}`, scoreText: c.scoreText })), empty: '当前没有显式主题候选。' } ] @@ -382,8 +382,8 @@ function recommendationTitle(item) { } function recommendationText(item) { - if (item.lowConfidenceWidened || item.statusKey === 'LOW_CONFIDENCE') return '优先补 documentTags、knowledgeScopeName、topic 别名,以及 topic-document relation 的人工确认。' - if (item.statusKey === 'FAILED') return '当前路由没有形成稳定候选,先检查上传元数据、文档画像和主题树是否为空。' + if (item.lowConfidenceWidened || item.statusKey === 'LOW_CONFIDENCE') return '优先补 topic 别名、示例问题,以及 topic-document relation 的人工确认。' + if (item.statusKey === 'FAILED') return '当前路由没有形成稳定候选,先检查文档画像、主题树和主题文档关联是否为空。' if (item.mode === 'shadow' && item.missedTop3) return '人工选文档和自动路由差异较大,建议对比问题表达与文档画像的关键词覆盖情况。' return '当前样本已经接近可教学展示状态,下一步重点看不同问题类型下是否还能持续稳定。' } diff --git a/vue/src/views/admin/AdminKnowledgeRouteView.vue b/vue/src/views/admin/AdminKnowledgeRouteView.vue index 7d92467ab71085343cf7355dceaa48d0ec958c3e..8db21c55bcf39697f8e2f07bd2adfc4f025ed7cd 100644 --- a/vue/src/views/admin/AdminKnowledgeRouteView.vue +++ b/vue/src/views/admin/AdminKnowledgeRouteView.vue @@ -6,6 +6,10 @@

按 范围 → 主题 → 画像 → 关联 的顺序逐步配置,构建自动知识问答的候选预选体系。

+
@@ -32,13 +36,12 @@
-
{{ item.scopeName }} - {{ item.scopeCode }}
{{ item.coverageRateText }}
@@ -73,17 +76,17 @@

知识范围

先把大范围定清楚,自动知识问答才能稳定地在正确文档池里预选。

-
+
-
{{ item.scopeName }}
{{ item.description || '暂无描述' }}
- 主题 {{ topics.filter(t => t.scopeCode === item.scopeCode).length }} - 文档 {{ documents.filter(d => d.knowledgeScopeCode === item.scopeCode).length }} + 主题 {{ topics.filter(t => sameId(t.scopeId, item.scopeId)).length }} + 文档 {{ linkedDocumentCountByScope(item.scopeId) }}
没有匹配的知识范围。
@@ -98,16 +101,16 @@
- - + - +
-
{{ item.topicName }}
@@ -126,7 +129,7 @@

文档画像

查看文档的类型、摘要、核心主题和图能力开关,判断自动路由是否有足够信息。

-
+
@@ -185,9 +188,9 @@
- - + @@ -196,12 +199,12 @@ {{ relations.length }} 条可见关联
-
{{ item.documentName }} - {{ item.topicCode }} · 分数 {{ item.relationScore }} · {{ item.knowledgeScopeName || item.knowledgeScopeCode || '未分范围' }} + {{ item.topicName || topicNameText(item.topicId) }} · 分数 {{ item.relationScore }} · {{ item.scopeName || topicScopeText(item.topicId) }} {{ item.reason || documentMetaLine(item) }}
@@ -237,9 +240,18 @@