Commit d1e207899a1b7e4497104cd3321f4d2fcf5c3a4c

Authored by YunaiV
1 parent e5cc9d2a

【功能新增】AI:知识库,新增切片接口

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http 0 → 100644
  1 +### 切片内容
  2 +GET {{baseUrl}}/ai/knowledge/segment/split?url=https://static.iocoder.cn/README_yudao.md&segmentMaxTokens=800
  3 +Content-Type: application/json
  4 +Authorization: Bearer {{token}}
  5 +tenant-id: {{adminTenantId}}
0 \ No newline at end of file 6 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java
@@ -10,12 +10,16 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle @@ -10,12 +10,16 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle
10 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; 10 import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
11 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; 11 import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
12 import io.swagger.v3.oas.annotations.Operation; 12 import io.swagger.v3.oas.annotations.Operation;
  13 +import io.swagger.v3.oas.annotations.Parameter;
  14 +import io.swagger.v3.oas.annotations.Parameters;
13 import io.swagger.v3.oas.annotations.tags.Tag; 15 import io.swagger.v3.oas.annotations.tags.Tag;
14 import jakarta.annotation.Resource; 16 import jakarta.annotation.Resource;
15 import jakarta.validation.Valid; 17 import jakarta.validation.Valid;
16 import org.springframework.validation.annotation.Validated; 18 import org.springframework.validation.annotation.Validated;
17 import org.springframework.web.bind.annotation.*; 19 import org.springframework.web.bind.annotation.*;
18 20
  21 +import java.util.List;
  22 +
19 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 23 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
20 24
21 // TODO @芋艿:增加权限标识 25 // TODO @芋艿:增加权限标识
@@ -30,7 +34,8 @@ public class AiKnowledgeSegmentController { @@ -30,7 +34,8 @@ public class AiKnowledgeSegmentController {
30 34
31 @GetMapping("/page") 35 @GetMapping("/page")
32 @Operation(summary = "获取段落分页") 36 @Operation(summary = "获取段落分页")
33 - public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPage(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) { 37 + public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPage(
  38 + @Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
34 PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO); 39 PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO);
35 return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class)); 40 return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class));
36 } 41 }
@@ -44,9 +49,23 @@ public class AiKnowledgeSegmentController { @@ -44,9 +49,23 @@ public class AiKnowledgeSegmentController {
44 49
45 @PutMapping("/update-status") 50 @PutMapping("/update-status")
46 @Operation(summary = "启禁用段落内容") 51 @Operation(summary = "启禁用段落内容")
47 - public CommonResult<Boolean> updateKnowledgeSegmentStatus(@Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { 52 + public CommonResult<Boolean> updateKnowledgeSegmentStatus(
  53 + @Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
48 segmentService.updateKnowledgeSegmentStatus(reqVO); 54 segmentService.updateKnowledgeSegmentStatus(reqVO);
49 return success(true); 55 return success(true);
50 } 56 }
51 57
  58 + @GetMapping("/split")
  59 + @Operation(summary = "切片内容")
  60 + @Parameters({
  61 + @Parameter(name = "url", description = "文档 URL", required = true),
  62 + @Parameter(name = "segmentMaxTokens", description = "分段的最大 Token 数", required = true)
  63 + })
  64 + public CommonResult<List<AiKnowledgeSegmentRespVO>> splitContent(
  65 + @RequestParam("url") String url,
  66 + @RequestParam(value = "segmentMaxTokens") Integer segmentMaxTokens) {
  67 + List<AiKnowledgeSegmentDO> segments = segmentService.splitContent(url, segmentMaxTokens);
  68 + return success(BeanUtils.toBean(segments, AiKnowledgeSegmentRespVO.class));
  69 + }
  70 +
52 } 71 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
3 import io.swagger.v3.oas.annotations.media.Schema; 3 import io.swagger.v3.oas.annotations.media.Schema;
4 import lombok.Data; 4 import lombok.Data;
5 5
6 -@Schema(description = "管理后台 - AI 知识库-文档 Response VO") 6 +@Schema(description = "管理后台 - AI 知识库文档分片 Response VO")
7 @Data 7 @Data
8 public class AiKnowledgeSegmentRespVO { 8 public class AiKnowledgeSegmentRespVO {
9 9
@@ -22,13 +22,19 @@ public class AiKnowledgeSegmentRespVO { @@ -22,13 +22,19 @@ public class AiKnowledgeSegmentRespVO {
22 @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") 22 @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册")
23 private String content; 23 private String content;
24 24
  25 + @Schema(description = "切片内容长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  26 + private Integer contentLength;
  27 +
25 @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") 28 @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
26 private Integer tokens; 29 private Integer tokens;
27 30
28 - @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008")  
29 - private Integer wordCount; 31 + @Schema(description = "召回次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
  32 + private Integer retrievalCount;
30 33
31 @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 34 @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
32 private Integer status; 35 private Integer status;
33 36
  37 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  38 + private Long createTime;
  39 +
34 } 40 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java
@@ -63,4 +63,12 @@ public interface AiKnowledgeDocumentService { @@ -63,4 +63,12 @@ public interface AiKnowledgeDocumentService {
63 */ 63 */
64 AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id); 64 AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id);
65 65
  66 + /**
  67 + * 读取 URL 内容
  68 + *
  69 + * @param url URL
  70 + * @return 内容
  71 + */
  72 + String readUrl(String url);
  73 +
66 } 74 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java
@@ -127,9 +127,10 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @@ -127,9 +127,10 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
127 return knowledgeDocument; 127 return knowledgeDocument;
128 } 128 }
129 129
130 - private static String readUrl(String url) { 130 + @Override
  131 + public String readUrl(String url) {
131 // 下载文件 132 // 下载文件
132 - ByteArrayResource resource = null; 133 + ByteArrayResource resource;
133 try { 134 try {
134 byte[] bytes = HttpUtil.downloadBytes(url); 135 byte[] bytes = HttpUtil.downloadBytes(url);
135 if (bytes.length == 0) { 136 if (bytes.length == 0) {
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java
@@ -29,7 +29,7 @@ public interface AiKnowledgeSegmentService { @@ -29,7 +29,7 @@ public interface AiKnowledgeSegmentService {
29 * 基于 content 内容,切片创建多个段落 29 * 基于 content 内容,切片创建多个段落
30 * 30 *
31 * @param documentId 知识库文档编号 31 * @param documentId 知识库文档编号
32 - * @param content 文档内容 32 + * @param content 文档内容
33 */ 33 */
34 void createKnowledgeSegmentBySplitContent(Long documentId, String content); 34 void createKnowledgeSegmentBySplitContent(Long documentId, String content);
35 35
@@ -37,7 +37,7 @@ public interface AiKnowledgeSegmentService { @@ -37,7 +37,7 @@ public interface AiKnowledgeSegmentService {
37 * 【异步】基于 content 内容,切片创建多个段落 37 * 【异步】基于 content 内容,切片创建多个段落
38 * 38 *
39 * @param documentId 知识库文档编号 39 * @param documentId 知识库文档编号
40 - * @param content 文档内容 40 + * @param content 文档内容
41 */ 41 */
42 @Async 42 @Async
43 default void createKnowledgeSegmentBySplitContentAsync(Long documentId, String content) { 43 default void createKnowledgeSegmentBySplitContentAsync(Long documentId, String content) {
@@ -66,4 +66,13 @@ public interface AiKnowledgeSegmentService { @@ -66,4 +66,13 @@ public interface AiKnowledgeSegmentService {
66 */ 66 */
67 List<AiKnowledgeSegmentDO> similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO); 67 List<AiKnowledgeSegmentDO> similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO);
68 68
  69 + /**
  70 + * 根据 URL 内容,切片创建多个段落
  71 + *
  72 + * @param url URL 地址
  73 + * @param segmentMaxTokens 段落最大 Token 数
  74 + * @return 切片后的段落列表
  75 + */
  76 + List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens);
  77 +
69 } 78 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
@@ -75,9 +75,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @@ -75,9 +75,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
75 VectorStore vectorStore = getVectorStoreById(knowledgeDO); 75 VectorStore vectorStore = getVectorStoreById(knowledgeDO);
76 76
77 // 2. 文档切片 77 // 2. 文档切片
78 - Document document = new Document(content);  
79 - TextSplitter textSplitter = buildTokenTextSplitter(documentDO.getSegmentMaxTokens());  
80 - List<Document> documentSegments = textSplitter.apply(Collections.singletonList(document)); 78 + List<Document> documentSegments = splitContentByToken(content, documentDO.getSegmentMaxTokens());
81 79
82 // 3.1 存储切片 80 // 3.1 存储切片
83 List<AiKnowledgeSegmentDO> segmentDOs = convertList(documentSegments, segment -> { 81 List<AiKnowledgeSegmentDO> segmentDOs = convertList(documentSegments, segment -> {
@@ -86,7 +84,8 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @@ -86,7 +84,8 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
86 } 84 }
87 return new AiKnowledgeSegmentDO().setKnowledgeId(documentDO.getKnowledgeId()).setDocumentId(documentId) 85 return new AiKnowledgeSegmentDO().setKnowledgeId(documentDO.getKnowledgeId()).setDocumentId(documentId)
88 .setContent(segment.getText()).setContentLength(segment.getText().length()) 86 .setContent(segment.getText()).setContentLength(segment.getText().length())
89 - .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY).setTokens(tokenCountEstimator.estimate(segment.getText())) 87 + .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY)
  88 + .setTokens(tokenCountEstimator.estimate(segment.getText()))
90 .setStatus(CommonStatusEnum.ENABLE.getStatus()); 89 .setStatus(CommonStatusEnum.ENABLE.getStatus());
91 }); 90 });
92 segmentMapper.insertBatch(segmentDOs); 91 segmentMapper.insertBatch(segmentDOs);
@@ -180,6 +179,26 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @@ -180,6 +179,26 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
180 return segmentMapper.selectListByVectorIds(convertList(documents, Document::getId)); 179 return segmentMapper.selectListByVectorIds(convertList(documents, Document::getId));
181 } 180 }
182 181
  182 + @Override
  183 + public List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) {
  184 + // 1. 读取 URL 内容
  185 + String content = knowledgeDocumentService.readUrl(url);
  186 +
  187 + // 2. 文档切片
  188 + List<Document> documentSegments = splitContentByToken(content, segmentMaxTokens);
  189 +
  190 + // 3. 转换为段落对象
  191 + return convertList(documentSegments, segment -> {
  192 + if (StrUtil.isEmpty(segment.getText())) {
  193 + return null;
  194 + }
  195 + return new AiKnowledgeSegmentDO()
  196 + .setContent(segment.getText())
  197 + .setContentLength(segment.getText().length())
  198 + .setTokens(tokenCountEstimator.estimate(segment.getText()));
  199 + });
  200 + }
  201 +
183 /** 202 /**
184 * 校验段落是否存在 203 * 校验段落是否存在
185 * 204 *
@@ -202,6 +221,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @@ -202,6 +221,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
202 return getVectorStoreById(knowledgeService.validateKnowledgeExists(knowledgeId)); 221 return getVectorStoreById(knowledgeService.validateKnowledgeExists(knowledgeId));
203 } 222 }
204 223
  224 + private static List<Document> splitContentByToken(String content, Integer segmentMaxTokens) {
  225 + TextSplitter textSplitter = buildTokenTextSplitter(segmentMaxTokens);
  226 + return textSplitter.apply(Collections.singletonList(new Document(content)));
  227 + }
  228 +
205 private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) { 229 private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) {
206 return TokenTextSplitter.builder() 230 return TokenTextSplitter.builder()
207 .withChunkSize(segmentMaxTokens) 231 .withChunkSize(segmentMaxTokens)