feat(knowledge): 完成知识库基本功能开发

This commit is contained in:
v-zhangjc9
2025-05-28 15:06:30 +08:00
parent f7ed3bd270
commit 3ee6303cf5
30 changed files with 1787 additions and 273 deletions

View File

@@ -24,7 +24,7 @@
<spring-boot.version>3.4.3</spring-boot.version>
<spring-cloud.version>2024.0.1</spring-cloud.version>
<spring-ai.version>1.0.0-RC1</spring-ai.version>
<spring-ai.version>1.0.0</spring-ai.version>
<eclipse-collections.version>11.1.0</eclipse-collections.version>
<curator.version>5.1.0</curator.version>
<hutool.version>5.8.27</hutool.version>

View File

@@ -8,6 +8,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author lanyuanxiaoyao
@@ -18,6 +19,7 @@ import org.springframework.retry.annotation.EnableRetry;
@EnableConfigurationProperties
@EnableEncryptableProperties
@EnableRetry
@EnableScheduling
public class KnowledgeApplication implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(KnowledgeApplication.class, args);

View File

@@ -0,0 +1,39 @@
package com.lanyuanxiaoyao.service.ai.knowledge.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author lanyuanxiaoyao
* @version 20250527
*/
@Configuration
@ConfigurationProperties(prefix = "knowledge")
public class KnowledgeConfiguration {
private String downloadPrefix;
private String uploadPath;
public String getDownloadPrefix() {
return downloadPrefix;
}
public void setDownloadPrefix(String downloadPrefix) {
this.downloadPrefix = downloadPrefix;
}
public String getUploadPath() {
return uploadPath;
}
public void setUploadPath(String uploadPath) {
this.uploadPath = uploadPath;
}
@Override
public String toString() {
return "KnowledgeConfiguration{" +
"downloadPrefix='" + downloadPrefix + '\'' +
", uploadPath='" + uploadPath + '\'' +
'}';
}
}

View File

@@ -0,0 +1,382 @@
package com.lanyuanxiaoyao.service.ai.knowledge.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.crypto.SecureUtil;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.lanyuanxiaoyao.service.ai.core.entity.amis.AmisResponse;
import com.lanyuanxiaoyao.service.ai.knowledge.configuration.KnowledgeConfiguration;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.vo.DataFileVO;
import com.lanyuanxiaoyao.service.ai.knowledge.service.DataFileService;
import jakarta.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件上传接口
*
* @author lanyuanxiaoyao
* @date 2024-11-21
*/
@RestController
@RequestMapping("/upload")
public class DataFileController {
private static final Logger log = LoggerFactory.getLogger(DataFileController.class);
private final KnowledgeConfiguration knowledgeConfiguration;
private final DataFileService dataFileService;
private final String uploadFolderPath;
private final String cacheFolderPath;
private final String sliceFolderPath;
public DataFileController(KnowledgeConfiguration knowledgeConfiguration, DataFileService dataFileService) {
this.knowledgeConfiguration = knowledgeConfiguration;
this.dataFileService = dataFileService;
this.uploadFolderPath = knowledgeConfiguration.getUploadPath();
this.cacheFolderPath = StrUtil.format("{}/cache", uploadFolderPath);
this.sliceFolderPath = StrUtil.format("{}/slice", uploadFolderPath);
}
@PostMapping("")
public AmisResponse<FinishResponse> upload(@RequestParam("file") MultipartFile file) throws IOException {
String filename = file.getOriginalFilename();
Long id = dataFileService.initialDataFile(filename);
String url = StrUtil.format("{}/upload/download/{}", knowledgeConfiguration.getDownloadPrefix(), id);
byte[] bytes = file.getBytes();
String originMd5 = SecureUtil.md5(new ByteArrayInputStream(bytes));
File targetFile = new File(StrUtil.format("{}/{}", uploadFolderPath, originMd5));
if (targetFile.exists()) {
dataFileService.updateDataFile(id, FileUtil.getAbsolutePath(targetFile), FileUtil.size(targetFile), originMd5, file.getContentType());
return AmisResponse.responseSuccess(new FinishResponse(id, filename, url, url));
}
File cacheFile = new File(StrUtil.format("{}/{}", cacheFolderPath, id));
cacheFile = FileUtil.writeBytes(bytes, cacheFile);
String targetMd5 = SecureUtil.md5(cacheFile);
if (!StrUtil.equals(originMd5, targetMd5)) {
throw new RuntimeException("文件上传失败,校验不匹配");
}
FileUtil.move(cacheFile, targetFile, true);
dataFileService.updateDataFile(id, FileUtil.getAbsolutePath(targetFile), FileUtil.size(targetFile), targetMd5, file.getContentType());
return AmisResponse.responseSuccess(new FinishResponse(id, filename, url, url));
}
@GetMapping("/download/{id}")
public void download(@PathVariable Long id, HttpServletResponse response) throws IOException {
DataFileVO dataFile = dataFileService.downloadFile(id);
File targetFile = new File(dataFile.getPath());
response.setHeader("Content-Type", dataFile.getType());
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Content-Disposition", StrUtil.format("attachment; filename={}", URLUtil.encodeAll(dataFile.getFilename())));
IoUtil.copy(new FileInputStream(targetFile), response.getOutputStream());
}
@PostMapping("/start")
public AmisResponse<StartResponse> start(@RequestBody StartRequest request) {
log.info("Request: {}", request);
Long id = dataFileService.initialDataFile(request.filename);
return AmisResponse.responseSuccess(new StartResponse(id.toString()));
}
@PostMapping("/slice")
public AmisResponse<SliceResponse> slice(
@RequestParam("uploadId")
Long uploadId,
@RequestParam("partNumber")
Integer sequence,
@RequestParam("partSize")
Long size,
@RequestParam("file")
MultipartFile file
) throws IOException {
byte[] bytes = file.getBytes();
String md5 = SecureUtil.md5(new ByteArrayInputStream(bytes));
String targetFilename = StrUtil.format("{}-{}", sequence, md5);
String targetFilePath = sliceFilePath(uploadId, targetFilename);
FileUtil.mkParentDirs(targetFilePath);
FileUtil.writeBytes(bytes, targetFilePath);
return AmisResponse.responseSuccess(new SliceResponse(targetFilename));
}
private String sliceFilePath(Long uploadId, String sliceFilename) {
return StrUtil.format("{}/{}/{}", sliceFolderPath, uploadId, sliceFilename);
}
@PostMapping("finish")
public AmisResponse<FinishResponse> finish(@RequestBody FinishRequest request) {
if (request.partList.anySatisfy(part -> !FileUtil.exist(sliceFilePath(request.uploadId, part.eTag)))) {
throw new RuntimeException("文件校验失败,请重新上传");
}
try {
File cacheFile = new File(StrUtil.format("{}/{}", cacheFolderPath, request.uploadId));
FileUtil.mkParentDirs(cacheFile);
if (cacheFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(cacheFile)) {
try (FileChannel fosChannel = fos.getChannel()) {
for (FinishRequest.Part part : request.partList) {
File sliceFile = new File(sliceFilePath(request.uploadId, part.eTag));
try (FileInputStream fis = new FileInputStream(sliceFile)) {
try (FileChannel fisChannel = fis.getChannel()) {
fisChannel.transferTo(0, fisChannel.size(), fosChannel);
}
}
}
}
}
String md5 = SecureUtil.md5(cacheFile);
File targetFile = new File(StrUtil.format("{}/{}", uploadFolderPath, md5));
if (!targetFile.exists()) {
FileUtil.move(cacheFile, targetFile, true);
}
String absolutePath = FileUtil.getAbsolutePath(targetFile);
dataFileService.updateDataFile(
request.uploadId,
absolutePath,
FileUtil.size(targetFile),
SecureUtil.md5(targetFile),
FileUtil.getMimeType(absolutePath)
);
return AmisResponse.responseSuccess(new FinishResponse(
request.uploadId,
request.filename,
request.uploadId.toString(),
StrUtil.format("{}/upload/download/{}", knowledgeConfiguration.getDownloadPrefix(), request.uploadId)
));
} else {
throw new RuntimeException("合并文件失败");
}
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
} finally {
FileUtil.del(StrUtil.format("{}/{}", cacheFolderPath, request.uploadId));
FileUtil.del(StrUtil.format("{}/{}", sliceFolderPath, request.uploadId));
}
}
public static final class StartRequest {
private String name;
private String filename;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
@Override
public String toString() {
return "StartRequest{" +
"name='" + name + '\'' +
", filename='" + filename + '\'' +
'}';
}
}
public static final class StartResponse {
private String uploadId;
public StartResponse() {
}
public StartResponse(String uploadId) {
this.uploadId = uploadId;
}
public String getUploadId() {
return uploadId;
}
public void setUploadId(String uploadId) {
this.uploadId = uploadId;
}
@Override
public String toString() {
return "StartResponse{" +
"uploadId='" + uploadId + '\'' +
'}';
}
}
public static final class SliceResponse {
@JsonProperty("eTag")
private String eTag;
public SliceResponse() {
}
public SliceResponse(String eTag) {
this.eTag = eTag;
}
public String geteTag() {
return eTag;
}
public void seteTag(String eTag) {
this.eTag = eTag;
}
@Override
public String toString() {
return "SliceResponse{" +
"eTag='" + eTag + '\'' +
'}';
}
}
public static final class FinishRequest {
private String filename;
private Long uploadId;
private ImmutableList<Part> partList;
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public Long getUploadId() {
return uploadId;
}
public void setUploadId(Long uploadId) {
this.uploadId = uploadId;
}
public ImmutableList<Part> getPartList() {
return partList;
}
public void setPartList(ImmutableList<Part> partList) {
this.partList = partList;
}
@Override
public String toString() {
return "FinishRequest{" +
"filename='" + filename + '\'' +
", uploadId=" + uploadId +
", partList=" + partList +
'}';
}
public static final class Part {
private Integer partNumber;
@JsonProperty("eTag")
private String eTag;
public Integer getPartNumber() {
return partNumber;
}
public void setPartNumber(Integer partNumber) {
this.partNumber = partNumber;
}
public String geteTag() {
return eTag;
}
public void seteTag(String eTag) {
this.eTag = eTag;
}
@Override
public String toString() {
return "Part{" +
"partNumber=" + partNumber +
", eTag='" + eTag + '\'' +
'}';
}
}
}
public static final class FinishResponse {
private Long id;
private String filename;
private String value;
private String url;
public FinishResponse() {
}
public FinishResponse(Long id, String filename, String value, String url) {
this.id = id;
this.filename = filename;
this.value = value;
this.url = url;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return "FinishResponse{" +
"id=" + id +
", filename='" + filename + '\'' +
", value='" + value + '\'' +
", url='" + url + '\'' +
'}';
}
}
}

View File

@@ -0,0 +1,38 @@
package com.lanyuanxiaoyao.service.ai.knowledge.controller;
import com.lanyuanxiaoyao.service.ai.core.entity.amis.AmisResponse;
import com.lanyuanxiaoyao.service.ai.knowledge.service.GroupService;
import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lanyuanxiaoyao
* @version 20250528
*/
@RestController
@RequestMapping("group")
public class GroupController {
private static final Logger logger = LoggerFactory.getLogger(GroupController.class);
private final GroupService groupService;
public GroupController(GroupService groupService) {
this.groupService = groupService;
}
@GetMapping("list")
public AmisResponse<?> list(@RequestParam("knowledge_id") Long knowledgeId) {
return AmisResponse.responseCrudData(groupService.list(knowledgeId));
}
@GetMapping("delete")
public AmisResponse<?> delete(@RequestParam("id") Long id) throws ExecutionException, InterruptedException {
groupService.remove(id);
return AmisResponse.responseSuccess();
}
}

View File

@@ -1,28 +1,17 @@
package com.lanyuanxiaoyao.service.ai.knowledge.controller;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.service.ai.core.entity.amis.AmisMapResponse;
import com.lanyuanxiaoyao.service.ai.core.entity.amis.AmisResponse;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.vo.PointVO;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.vo.SegmentVO;
import com.lanyuanxiaoyao.service.ai.knowledge.service.EmbeddingService;
import com.lanyuanxiaoyao.service.ai.knowledge.service.KnowledgeService;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.grpc.Points;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -38,14 +27,10 @@ public class KnowledgeController {
private final KnowledgeService knowledgeService;
private final EmbeddingService embeddingService;
private final QdrantClient client;
private final EmbeddingModel embeddingModel;
public KnowledgeController(KnowledgeService knowledgeService, EmbeddingService embeddingService, VectorStore vectorStore, EmbeddingModel embeddingModel) {
public KnowledgeController(KnowledgeService knowledgeService, EmbeddingService embeddingService) {
this.knowledgeService = knowledgeService;
this.embeddingService = embeddingService;
client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
this.embeddingModel = embeddingModel;
}
@PostMapping("add")
@@ -56,73 +41,68 @@ public class KnowledgeController {
knowledgeService.add(name, strategy);
}
@GetMapping("name")
public AmisMapResponse name(@RequestParam("id") Long id) {
return AmisResponse.responseMapData()
.setData("name", knowledgeService.getName(id));
}
@GetMapping("list")
public AmisResponse<?> list() {
return AmisResponse.responseCrudData(knowledgeService.list());
}
@GetMapping("list_points")
public ImmutableList<PointVO> listPoints(@RequestParam("name") String name) throws ExecutionException, InterruptedException {
Points.ScrollResponse response = client.scrollAsync(
Points.ScrollPoints.newBuilder()
.setCollectionName(name)
// .setLimit(2)
.setWithPayload(Points.WithPayloadSelector.newBuilder().setEnable(true).build())
.setWithVectors(Points.WithVectorsSelector.newBuilder().setEnable(false).build())
.build()
)
.get();
return response.getResultList()
.stream()
.collect(Collectors.toCollection(Lists.mutable::empty))
.collect(point -> {
PointVO vo = new PointVO();
vo.setId(point.getId().getUuid());
vo.setText(point.getPayloadMap().get("doc_content").getStringValue());
return vo;
})
.toImmutable();
}
@GetMapping("delete")
public void delete(@RequestParam("name") String name) throws ExecutionException, InterruptedException {
knowledgeService.remove(name);
public void delete(@RequestParam("id") Long id) throws ExecutionException, InterruptedException {
knowledgeService.remove(id);
}
@PostMapping("preview_text")
public AmisResponse<?> previewText(
@RequestParam(value = "mode", defaultValue = "NORMAL") String mode,
@RequestParam(value = "type", defaultValue = "text") String type,
@RequestParam("content") String content
@RequestParam(value = "content", required = false) String content,
@RequestParam(value = "files", required = false) String files
) {
return AmisResponse.responseCrudData(
embeddingService.split(mode, content)
.collect(doc -> {
PointVO vo = new PointVO();
vo.setId(doc.getId());
vo.setText(doc.getText());
return vo;
})
);
if (StrUtil.equals("text", type)) {
return AmisResponse.responseCrudData(
embeddingService.preview(mode, content)
.collect(doc -> {
SegmentVO vo = new SegmentVO();
vo.setId(doc.getId());
vo.setText(doc.getText());
return vo;
})
);
} else if (StrUtil.equals("file", type)) {
return AmisResponse.responseCrudData(
embeddingService.preview(mode, Lists.immutable.of(files.split(",")))
.collect(doc -> {
SegmentVO vo = new SegmentVO();
vo.setId(doc.getId());
vo.setText(doc.getText());
return vo;
})
);
} else {
throw new IllegalArgumentException("Unsupported type: " + type);
}
}
@PostMapping(value = "process_text", consumes = "text/plain;charset=utf-8")
public void processText(
@RequestParam("name") String name,
@RequestBody String text
@PostMapping("submit_text")
public void submitText(
@RequestParam(value = "id") Long id,
@RequestParam(value = "mode", defaultValue = "NORMAL") String mode,
@RequestParam(value = "type", defaultValue = "text") String type,
@RequestParam(value = "content", required = false) String content,
@RequestParam(value = "files", required = false) String files
) {
VectorStore source = QdrantVectorStore.builder(client, embeddingModel)
.collectionName(name)
.initializeSchema(true)
.build();
MarkdownDocumentReader reader = new MarkdownDocumentReader(
new ByteArrayResource(text.getBytes(StandardCharsets.UTF_8)),
MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true)
.withIncludeCodeBlock(false)
.withIncludeBlockquote(false)
.build()
);
source.add(reader.get());
if (StrUtil.equals("text", type)) {
embeddingService.submit(id, mode, content);
} else if (StrUtil.equals("file", type)) {
embeddingService.submit(id, mode, Lists.immutable.of(files.split(",")));
} else {
throw new IllegalArgumentException("Unsupported type: " + type);
}
}
}

View File

@@ -0,0 +1,38 @@
package com.lanyuanxiaoyao.service.ai.knowledge.controller;
import com.lanyuanxiaoyao.service.ai.core.entity.amis.AmisResponse;
import com.lanyuanxiaoyao.service.ai.knowledge.service.SegmentService;
import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lanyuanxiaoyao
* @version 20250528
*/
@RestController
@RequestMapping("segment")
public class SegmentController {
private static final Logger logger = LoggerFactory.getLogger(SegmentController.class);
private final SegmentService segmentService;
public SegmentController(SegmentService segmentService) {
this.segmentService = segmentService;
}
@GetMapping("list")
public AmisResponse<?> list(@RequestParam("knowledge_id") Long knowledgeId, @RequestParam("group_id") Long groupId) throws ExecutionException, InterruptedException {
return AmisResponse.responseCrudData(segmentService.list(knowledgeId, groupId));
}
@GetMapping("delete")
public AmisResponse<?> delete(@RequestParam("knowledge_id") Long knowledgeId, @RequestParam("segment_id") Long segmentId) throws ExecutionException, InterruptedException {
segmentService.remove(knowledgeId, segmentId);
return AmisResponse.responseSuccess();
}
}

View File

@@ -1,9 +1,5 @@
package com.lanyuanxiaoyao.service.ai.knowledge.entity;
import cn.hutool.core.util.StrUtil;
import java.io.File;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.collections.api.factory.Lists;
@@ -15,36 +11,42 @@ import org.springframework.ai.document.Document;
* @version 20250523
*/
public class EmbeddingContext {
private Long vectorSourceId;
private Long groupId;
private Config config;
private String content;
private String file;
private String fileFormat;
private List<Document> documents = Lists.mutable.empty();
private Map<String, Object> metadata = Maps.mutable.empty();
public EmbeddingContext(String content) {
this(content, new Config());
private EmbeddingContext(Builder builder) {
setVectorSourceId(builder.vectorSourceId);
setGroupId(builder.groupId);
setConfig(builder.config);
setContent(builder.content);
setFile(builder.file);
setFileFormat(builder.fileFormat);
}
public EmbeddingContext(String content, Config config) {
this.content = StrUtil.trim(content);
this.config = config;
public static Builder builder() {
return new Builder();
}
public EmbeddingContext(File file) {
this(file, new Config());
public Long getGroupId() {
return groupId;
}
public EmbeddingContext(File file, Config config) {
this.file = file.getAbsolutePath();
this.config = config;
public void setGroupId(Long groupId) {
this.groupId = groupId;
}
public EmbeddingContext(Path path) {
this(path.toFile());
public Long getVectorSourceId() {
return vectorSourceId;
}
public EmbeddingContext(Path path, Config config) {
this(path.toFile(), config);
public void setVectorSourceId(Long vectorSourceId) {
this.vectorSourceId = vectorSourceId;
}
public Config getConfig() {
@@ -71,6 +73,14 @@ public class EmbeddingContext {
this.file = file;
}
public String getFileFormat() {
return fileFormat;
}
public void setFileFormat(String fileFormat) {
this.fileFormat = fileFormat;
}
public List<Document> getDocuments() {
return documents;
}
@@ -90,9 +100,12 @@ public class EmbeddingContext {
@Override
public String toString() {
return "EmbeddingContext{" +
"config=" + config +
"vectorSourceId=" + vectorSourceId +
", groupId=" + groupId +
", config=" + config +
", content='" + content + '\'' +
", file='" + file + '\'' +
", fileFormat='" + fileFormat + '\'' +
", documents=" + documents +
", metadata=" + metadata +
'}';
@@ -101,11 +114,10 @@ public class EmbeddingContext {
public static final class Config {
private SplitStrategy splitStrategy = SplitStrategy.NORMAL;
public Config() {
}
private Config(Builder builder) {setSplitStrategy(builder.splitStrategy);}
public Config(SplitStrategy splitStrategy) {
this.splitStrategy = splitStrategy;
public static Builder builder() {
return new Builder();
}
public SplitStrategy getSplitStrategy() {
@@ -126,5 +138,65 @@ public class EmbeddingContext {
public enum SplitStrategy {
NORMAL, LLM, QA
}
public static final class Builder {
private SplitStrategy splitStrategy;
private Builder() {}
public Builder splitStrategy(SplitStrategy val) {
splitStrategy = val;
return this;
}
public Config build() {
return new Config(this);
}
}
}
public static final class Builder {
private Long vectorSourceId;
private Long groupId;
private Config config;
private String content;
private String file;
private String fileFormat;
private Builder() {}
public Builder vectorSourceId(Long val) {
vectorSourceId = val;
return this;
}
public Builder groupId(Long val) {
groupId = val;
return this;
}
public Builder config(Config val) {
config = val;
return this;
}
public Builder content(String val) {
content = val;
return this;
}
public Builder file(String val) {
file = val;
return this;
}
public Builder fileFormat(String val) {
fileFormat = val;
return this;
}
public EmbeddingContext build() {
return new EmbeddingContext(this);
}
}
}

View File

@@ -0,0 +1,64 @@
package com.lanyuanxiaoyao.service.ai.knowledge.entity;
/**
* @author lanyuanxiaoyao
* @version 20250527
*/
public class Group {
private String id;
private String name;
private String status;
private Long createdTime;
private Long modifiedTime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Long getCreatedTime() {
return createdTime;
}
public void setCreatedTime(Long createdTime) {
this.createdTime = createdTime;
}
public Long getModifiedTime() {
return modifiedTime;
}
public void setModifiedTime(Long modifiedTime) {
this.modifiedTime = modifiedTime;
}
@Override
public String toString() {
return "GroupVO{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", status='" + status + '\'' +
", createdTime=" + createdTime +
", modifiedTime=" + modifiedTime +
'}';
}
}

View File

@@ -9,6 +9,8 @@ public class Knowledge {
private Long vectorSourceId;
private String name;
private String strategy;
private Long createdTime;
private Long modifiedTime;
public Long getId() {
return id;
@@ -42,6 +44,22 @@ public class Knowledge {
this.strategy = strategy;
}
public Long getCreatedTime() {
return createdTime;
}
public void setCreatedTime(Long createdTime) {
this.createdTime = createdTime;
}
public Long getModifiedTime() {
return modifiedTime;
}
public void setModifiedTime(Long modifiedTime) {
this.modifiedTime = modifiedTime;
}
@Override
public String toString() {
return "Knowledge{" +
@@ -49,6 +67,8 @@ public class Knowledge {
", vectorSourceId=" + vectorSourceId +
", name='" + name + '\'' +
", strategy='" + strategy + '\'' +
", createdTime=" + createdTime +
", modifiedTime=" + modifiedTime +
'}';
}
}

View File

@@ -0,0 +1,74 @@
package com.lanyuanxiaoyao.service.ai.knowledge.entity.vo;
/**
* @author lanyuanxiaoyao
* @version 20250527
*/
public class DataFileVO {
private String id;
private String filename;
private Long size;
private String md5;
private String path;
private String type;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public Long getSize() {
return size;
}
public void setSize(Long size) {
this.size = size;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String toString() {
return "DataFile{" +
"id='" + id + '\'' +
", filename='" + filename + '\'' +
", size=" + size +
", md5='" + md5 + '\'' +
", path='" + path + '\'' +
", type='" + type + '\'' +
'}';
}
}

View File

@@ -5,12 +5,23 @@ package com.lanyuanxiaoyao.service.ai.knowledge.entity.vo;
* @version 20250516
*/
public class KnowledgeVO {
private String id;
private String name;
private String strategy;
private Long size;
private Long points;
private Long segments;
private String status;
private Long createdTime;
private Long modifiedTime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
@@ -60,15 +71,34 @@ public class KnowledgeVO {
this.status = status;
}
public Long getCreatedTime() {
return createdTime;
}
public void setCreatedTime(Long createdTime) {
this.createdTime = createdTime;
}
public Long getModifiedTime() {
return modifiedTime;
}
public void setModifiedTime(Long modifiedTime) {
this.modifiedTime = modifiedTime;
}
@Override
public String toString() {
return "CollectionVO{" +
"name='" + name + '\'' +
return "KnowledgeVO{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", strategy='" + strategy + '\'' +
", size=" + size +
", points=" + points +
", segments=" + segments +
", status='" + status + '\'' +
", createdTime=" + createdTime +
", modifiedTime=" + modifiedTime +
'}';
}
}

View File

@@ -4,7 +4,7 @@ package com.lanyuanxiaoyao.service.ai.knowledge.entity.vo;
* @author lanyuanxiaoyao
* @version 20250516
*/
public class PointVO {
public class SegmentVO {
private String id;
private String text;

View File

@@ -0,0 +1,90 @@
package com.lanyuanxiaoyao.service.ai.knowledge.service;
import club.kingon.sql.builder.SqlBuilder;
import cn.hutool.core.util.IdUtil;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.vo.DataFileVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lanyuanxiaoyao
* @version 20250527
*/
@Service
public class DataFileService {
private static final Logger log = LoggerFactory.getLogger(DataFileService.class);
private static final String DATA_FILE_TABLE_NAME = "service_ai_file";
private final JdbcTemplate template;
public DataFileService(JdbcTemplate template) {
this.template = template;
}
public DataFileVO downloadFile(Long id) {
return template.queryForObject(
SqlBuilder.select("id", "filename", "size", "md5", "path", "type")
.from(DATA_FILE_TABLE_NAME)
.whereEq("id", "?")
.precompileSql(),
(rs, row) -> {
DataFileVO vo = new DataFileVO();
vo.setId(String.valueOf(rs.getLong(1)));
vo.setFilename(rs.getString(2));
vo.setSize(rs.getLong(3));
vo.setMd5(rs.getString(4));
vo.setPath(rs.getString(5));
vo.setType(rs.getString(6));
return vo;
},
id
);
}
@Transactional(rollbackFor = Exception.class)
public Long initialDataFile(String filename) {
long id = IdUtil.getSnowflakeNextId();
template.update(
SqlBuilder.insertInto(DATA_FILE_TABLE_NAME, "id", "filename")
.values()
.addValue("?", "?")
.precompileSql(),
id,
filename
);
return id;
}
@Transactional(rollbackFor = Exception.class)
public void updateDataFile(Long id, String path, Long size, String md5, String type) {
template.update(
SqlBuilder.update(DATA_FILE_TABLE_NAME)
.set("size", "?")
.addSet("md5", "?")
.addSet("path", "?")
.addSet("type", "?")
.whereEq("id", "?")
.precompileSql(),
size,
md5,
path,
type,
id
);
}
public static final class DataFileNotFoundException extends RuntimeException {
public DataFileNotFoundException() {
super("文件未找到,请重新上传");
}
}
public static final class UpdateDataFileFailedException extends RuntimeException {
public UpdateDataFileFailedException() {
super("更新文件信息失败,请重新上传");
}
}
}

View File

@@ -1,8 +1,15 @@
package com.lanyuanxiaoyao.service.ai.knowledge.service;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.EmbeddingContext;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.Knowledge;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.vo.DataFileVO;
import com.yomahub.liteflow.core.FlowExecutor;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger;
@@ -18,19 +25,75 @@ import org.springframework.stereotype.Service;
public class EmbeddingService {
private static final Logger logger = LoggerFactory.getLogger(EmbeddingService.class);
private final DataFileService dataFileService;
private final FlowExecutor executor;
private final KnowledgeService knowledgeService;
private final GroupService groupService;
private final ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public EmbeddingService(FlowExecutor executor) {
public EmbeddingService(DataFileService dataFileService, FlowExecutor executor, KnowledgeService knowledgeService, GroupService groupService) {
this.dataFileService = dataFileService;
this.executor = executor;
this.knowledgeService = knowledgeService;
this.groupService = groupService;
}
public ImmutableList<Document> split(String mode, String content) {
EmbeddingContext context = new EmbeddingContext(
content,
new EmbeddingContext.Config(EmbeddingContext.Config.SplitStrategy.valueOf(mode))
);
executor.execute2Resp("embedding", null, context);
public ImmutableList<Document> preview(String mode, String content) {
if (content.length() > 2000) {
content = content.substring(0, 2000);
}
EmbeddingContext context = EmbeddingContext.builder()
.content(content)
.config(EmbeddingContext.Config.builder()
.splitStrategy(EmbeddingContext.Config.SplitStrategy.valueOf(mode))
.build())
.build();
executor.execute2Resp("embedding_preview", null, context);
return Lists.immutable.ofAll(context.getDocuments());
}
public ImmutableList<Document> preview(String mode, ImmutableList<String> ids) {
DataFileVO vo = dataFileService.downloadFile(Long.parseLong(ids.get(0)));
String content = FileUtil.readString(vo.getPath(), StandardCharsets.UTF_8);
return preview(mode, content);
}
public void submit(Long id, String mode, String content) {
executors.submit(() -> {
Knowledge knowledge = knowledgeService.get(id);
Long groupId = groupService.add(knowledge.getId(), StrUtil.format("文本-{}", IdUtil.nanoId(10)));
EmbeddingContext context = EmbeddingContext.builder()
.vectorSourceId(knowledge.getVectorSourceId())
.groupId(groupId)
.content(content)
.config(EmbeddingContext.Config.builder()
.splitStrategy(EmbeddingContext.Config.SplitStrategy.valueOf(mode))
.build())
.build();
executor.execute2Resp("embedding_submit", null, context);
groupService.finish(groupId);
});
}
public void submit(Long id, String mode, ImmutableList<String> ids) {
executors.submit(() -> {
Knowledge knowledge = knowledgeService.get(id);
for (String fileId : ids) {
DataFileVO vo = dataFileService.downloadFile(Long.parseLong(fileId));
Long groupId = groupService.add(id, vo.getFilename());
EmbeddingContext context = EmbeddingContext.builder()
.vectorSourceId(knowledge.getVectorSourceId())
.groupId(groupId)
.file(vo.getPath())
.fileFormat(vo.getFilename())
.config(EmbeddingContext.Config.builder()
.splitStrategy(EmbeddingContext.Config.SplitStrategy.valueOf(mode))
.build())
.build();
executor.execute2Resp("embedding_submit", null, context);
groupService.finish(groupId);
}
});
}
}

View File

@@ -0,0 +1,135 @@
package com.lanyuanxiaoyao.service.ai.knowledge.service;
import club.kingon.sql.builder.SqlBuilder;
import club.kingon.sql.builder.entry.Alias;
import club.kingon.sql.builder.entry.Column;
import cn.hutool.core.util.IdUtil;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.Group;
import io.qdrant.client.ConditionFactory;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.grpc.Points;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lanyuanxiaoyao
* @version 20250522
*/
@Service
public class GroupService {
public static final String GROUP_TABLE_NAME = "service_ai_group";
private static final Logger logger = LoggerFactory.getLogger(GroupService.class);
private static final RowMapper<Group> groupMapper = (rs, row) -> {
Group vo = new Group();
vo.setId(String.valueOf(rs.getLong(1)));
vo.setName(rs.getString(2));
vo.setStatus(rs.getString(3));
vo.setCreatedTime(rs.getTimestamp(4).getTime());
vo.setModifiedTime(rs.getTimestamp(5).getTime());
return vo;
};
private final JdbcTemplate template;
private final QdrantClient client;
public GroupService(JdbcTemplate template, VectorStore vectorStore) {
this.template = template;
this.client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
}
public Group get(Long id) {
return template.queryForObject(
SqlBuilder.select("id", "name", "status", "created_time", "modified_time")
.from(GROUP_TABLE_NAME)
.whereEq("id", id)
.orderByDesc("created_time")
.build(),
groupMapper
);
}
@Transactional(rollbackFor = Exception.class)
public Long add(Long knowledgeId, String name) {
long id = IdUtil.getSnowflakeNextId();
template.update(
SqlBuilder.insertInto(GROUP_TABLE_NAME, "id", "knowledge_id", "name", "status")
.values()
.addValue("?", "?", "?", "?")
.precompileSql(),
id,
knowledgeId,
name,
"RUNNING"
);
return id;
}
public ImmutableList<Group> list(Long knowledgeId) {
return template.query(
SqlBuilder.select("id", "name", "status", "created_time", "modified_time")
.from(GROUP_TABLE_NAME)
.whereEq("knowledge_id", knowledgeId)
.orderByDesc("created_time")
.build(),
groupMapper
)
.stream()
.collect(Collectors.toCollection(Lists.mutable::empty))
.toImmutable();
}
@Transactional(rollbackFor = Exception.class)
public void finish(Long groupId) {
template.update(
SqlBuilder.update(GROUP_TABLE_NAME)
.set("status", "FINISHED")
.whereEq("id", groupId)
.build()
);
}
@Transactional(rollbackFor = Exception.class)
public void remove(Long groupId) throws ExecutionException, InterruptedException {
Long vectorSourceId = template.queryForObject(
SqlBuilder.select("k.vector_source_id")
.from(Alias.of(GROUP_TABLE_NAME, "g"), Alias.of(KnowledgeService.KNOWLEDGE_TABLE_NAME, "k"))
.whereEq("g.knowledge_id", Column.as("k.id"))
.andEq("g.id", groupId)
.precompileSql(),
Long.class,
groupId
);
logger.info("Delete {} {}", vectorSourceId, groupId);
client.deleteAsync(
String.valueOf(vectorSourceId),
Points.Filter.newBuilder()
.addMust(ConditionFactory.matchKeyword("vector_source_id", String.valueOf(vectorSourceId)))
.addMust(ConditionFactory.matchKeyword("group_id", String.valueOf(groupId)))
.build()
).get();
template.update(
SqlBuilder.delete(GROUP_TABLE_NAME)
.whereEq("id", groupId)
.build()
);
}
@Transactional(rollbackFor = Exception.class)
public void removeByKnowledgeId(Long knowledgeId) {
template.update(
SqlBuilder.delete(GROUP_TABLE_NAME)
.whereEq("knowledge_id", "?")
.precompileSql(),
knowledgeId
);
}
}

View File

@@ -1,58 +0,0 @@
package com.lanyuanxiaoyao.service.ai.knowledge.service;
import club.kingon.sql.builder.SqlBuilder;
import cn.hutool.core.util.IdUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lanyuanxiaoyao
* @version 20250522
*/
@Service
public class KnowledgeGroupService {
private static final Logger logger = LoggerFactory.getLogger(KnowledgeGroupService.class);
private static final String GROUP_TABLE_NAME = "service_ai_group";
private final JdbcTemplate template;
public KnowledgeGroupService(JdbcTemplate template) {
this.template = template;
}
@Transactional(rollbackFor = Exception.class)
public void add(Long knowledgeId, String name) {
template.update(
SqlBuilder.insertInto(GROUP_TABLE_NAME, "id", "knowledge_id", "name")
.values()
.addValue("?", "?", "?")
.precompileSql(),
IdUtil.getSnowflakeNextId(),
knowledgeId,
name
);
}
@Transactional(rollbackFor = Exception.class)
public void remove(Long groupId) {
template.update(
SqlBuilder.delete(GROUP_TABLE_NAME)
.whereEq("id", "?")
.precompileSql(),
groupId
);
}
@Transactional(rollbackFor = Exception.class)
public void removeByKnowledgeId(Long knowledgeId) {
template.update(
SqlBuilder.delete(GROUP_TABLE_NAME)
.whereEq("knowledge_id", "?")
.precompileSql(),
knowledgeId
);
}
}

View File

@@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -26,43 +27,41 @@ import org.springframework.transaction.annotation.Transactional;
*/
@Service
public class KnowledgeService {
public static final String KNOWLEDGE_TABLE_NAME = "service_ai_knowledge";
private static final Logger logger = LoggerFactory.getLogger(KnowledgeService.class);
private static final String KNOWLEDGE_TABLE_NAME = "service_ai_knowledge";
private static final RowMapper<Knowledge> knowledgeMapper = (rs, row) -> {
Knowledge knowledge = new Knowledge();
knowledge.setId(rs.getLong(1));
knowledge.setVectorSourceId(rs.getLong(2));
knowledge.setName(rs.getString(3));
knowledge.setStrategy(rs.getString(4));
knowledge.setCreatedTime(rs.getTimestamp(5).getTime());
knowledge.setModifiedTime(rs.getTimestamp(6).getTime());
return knowledge;
};
private final JdbcTemplate template;
private final EmbeddingModel embeddingModel;
private final QdrantClient client;
private final KnowledgeGroupService knowledgeGroupService;
private final GroupService groupService;
public KnowledgeService(JdbcTemplate template, EmbeddingModel embeddingModel, VectorStore vectorStore, KnowledgeGroupService knowledgeGroupService) {
public KnowledgeService(JdbcTemplate template, EmbeddingModel embeddingModel, VectorStore vectorStore, GroupService groupService) {
this.template = template;
this.embeddingModel = embeddingModel;
this.client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
this.knowledgeGroupService = knowledgeGroupService;
this.groupService = groupService;
}
public Knowledge get(Long id) {
return template.queryForObject(
SqlBuilder.select("id", "vector_source_id", "name", "strategy")
SqlBuilder.select("id", "vector_source_id", "name", "strategy", "created_time", "modified_time")
.from(KNOWLEDGE_TABLE_NAME)
.whereEq("id", "?")
.precompileSql(),
Knowledge.class,
knowledgeMapper,
id
);
}
public Knowledge get(String name) {
return template.queryForObject(
SqlBuilder.select("id", "vector_source_id", "name", "strategy")
.from(KNOWLEDGE_TABLE_NAME)
.whereEq("name", "?")
.precompileSql(),
Knowledge.class,
name
);
}
@Transactional(rollbackFor = Exception.class)
public void add(String name, String strategy) throws ExecutionException, InterruptedException {
Integer count = template.queryForObject(
@@ -98,25 +97,31 @@ public class KnowledgeService {
).get();
}
public String getName(Long id) {
return template.queryForObject(
SqlBuilder.select("name")
.from(KNOWLEDGE_TABLE_NAME)
.whereEq("id", id)
.orderByDesc("created_time")
.build(),
String.class
);
}
public ImmutableList<KnowledgeVO> list() {
return template.query(
SqlBuilder.select("id", "vector_source_id", "name", "strategy")
SqlBuilder.select("id", "vector_source_id", "name", "strategy", "created_time", "modified_time")
.from(KNOWLEDGE_TABLE_NAME)
.orderByDesc("created_time")
.build(),
(rs, index) -> {
Knowledge knowledge = new Knowledge();
knowledge.setId(rs.getLong(1));
knowledge.setVectorSourceId(rs.getLong(2));
knowledge.setName(rs.getString(3));
knowledge.setStrategy(rs.getString(4));
return knowledge;
}
knowledgeMapper
)
.stream()
.map(knowledge -> {
try {
Collections.CollectionInfo info = client.getCollectionInfoAsync(String.valueOf(knowledge.getVectorSourceId())).get();
KnowledgeVO vo = new KnowledgeVO();
vo.setId(String.valueOf(knowledge.getId()));
vo.setName(knowledge.getName());
vo.setPoints(info.getPointsCount());
vo.setSegments(info.getSegmentsCount());
@@ -124,6 +129,8 @@ public class KnowledgeService {
Collections.VectorParams vectorParams = info.getConfig().getParams().getVectorsConfig().getParams();
vo.setStrategy(vectorParams.getDistance().name());
vo.setSize(vectorParams.getSize());
vo.setCreatedTime(vo.getCreatedTime());
vo.setModifiedTime(vo.getModifiedTime());
return vo;
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
@@ -134,8 +141,8 @@ public class KnowledgeService {
}
@Transactional(rollbackFor = Exception.class)
public void remove(String name) throws ExecutionException, InterruptedException {
Knowledge knowledge = get(name);
public void remove(Long id) throws ExecutionException, InterruptedException {
Knowledge knowledge = get(id);
if (ObjectUtil.isNull(knowledge)) {
throw new RuntimeException(StrUtil.format("{} 不存在"));
}
@@ -145,7 +152,7 @@ public class KnowledgeService {
.precompileSql(),
knowledge.getId()
);
knowledgeGroupService.removeByKnowledgeId(knowledge.getId());
groupService.removeByKnowledgeId(knowledge.getId());
client.deleteCollectionAsync(String.valueOf(knowledge.getVectorSourceId())).get();
}
}

View File

@@ -0,0 +1,71 @@
package com.lanyuanxiaoyao.service.ai.knowledge.service;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.Knowledge;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.vo.SegmentVO;
import io.qdrant.client.ConditionFactory;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.grpc.Points;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
/**
* @author lanyuanxiaoyao
* @version 20250528
*/
@Service
public class SegmentService {
private static final Logger logger = LoggerFactory.getLogger(SegmentService.class);
private final KnowledgeService knowledgeService;
private final QdrantClient client;
public SegmentService(KnowledgeService knowledgeService, VectorStore vectorStore) {
this.knowledgeService = knowledgeService;
this.client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
}
public ImmutableList<SegmentVO> list(Long id, Long groupId) throws ExecutionException, InterruptedException {
Knowledge knowledge = knowledgeService.get(id);
Points.ScrollResponse response = client.scrollAsync(
Points.ScrollPoints.newBuilder()
.setCollectionName(String.valueOf(knowledge.getVectorSourceId()))
.setWithPayload(Points.WithPayloadSelector.newBuilder().setEnable(true).build())
.setWithVectors(Points.WithVectorsSelector.newBuilder().setEnable(false).build())
.setFilter(
Points.Filter.newBuilder()
.addMust(ConditionFactory.matchKeyword("group_id", String.valueOf(groupId)))
.build()
)
.build()
)
.get();
return response.getResultList()
.stream()
.collect(Collectors.toCollection(Lists.mutable::empty))
.collect(point -> {
SegmentVO vo = new SegmentVO();
vo.setId(point.getId().getUuid());
vo.setText(point.getPayloadMap().get("doc_content").getStringValue());
return vo;
})
.toImmutable();
}
public void remove(Long knowledgeId, Long segmentId) throws ExecutionException, InterruptedException {
Knowledge knowledge = knowledgeService.get(knowledgeId);
client.deletePayloadAsync(
String.valueOf(knowledgeId),
List.of(String.valueOf(segmentId)),
null,
null,
null
).get();
}
}

View File

@@ -2,6 +2,7 @@ package com.lanyuanxiaoyao.service.ai.knowledge.service.node;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.service.ai.knowledge.entity.EmbeddingContext;
import com.yomahub.liteflow.annotation.LiteflowComponent;
@@ -9,21 +10,27 @@ import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import io.qdrant.client.QdrantClient;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.core.io.PathResource;
/**
@@ -35,9 +42,13 @@ public class EmbeddingNodes {
private static final Logger logger = LoggerFactory.getLogger(EmbeddingNodes.class);
private final ChatClient chatClient;
private final QdrantClient qdrantClient;
private final EmbeddingModel embeddingModel;
public EmbeddingNodes(ChatClient.Builder builder) {
public EmbeddingNodes(ChatClient.Builder builder, VectorStore vectorStore, EmbeddingModel embeddingModel) {
this.chatClient = builder.build();
this.qdrantClient = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
this.embeddingModel = embeddingModel;
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = "embedding_check_if_file_needed", nodeName = "判断是否需要读取文件", nodeType = NodeTypeEnum.BOOLEAN)
@@ -52,16 +63,10 @@ public class EmbeddingNodes {
return false;
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "test_print", nodeType = NodeTypeEnum.COMMON)
public void testPrint(NodeComponent node) {
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
logger.info(context.getContent());
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_SWITCH, nodeId = "file_reader_switch", nodeName = "判断文件格式", nodeType = NodeTypeEnum.SWITCH)
public String fileReaderSwitch(NodeComponent node) {
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
String extName = FileUtil.extName(context.getFile());
String extName = FileUtil.extName(context.getFileFormat());
return switch (extName.toLowerCase()) {
case "txt", "md", "markdown" -> "txt_file_reader";
case "pdf" -> "pdf_file_reader";
@@ -132,16 +137,11 @@ public class EmbeddingNodes {
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
context.getDocuments().addAll(llmSplit(
"""
用户输入的文本,生成高质量的分段。请遵循以下指南:
1. 分段原则:
分段按文本内容的语义进行分割,每个分段都尽可能保持完整连续的内容表达
避免从词句的中间进行分割
2. 格式:
分段之间用两个空行分隔,以提高可读性。
避免使用任何Markdown格式
3. 内容要求:
确保每个分段的内容文字完全依照原文。
避免添加任何原文中不存在的文字。
请你将用户输入的文本进行语义切分,生成用于知识库检索的文本段。
每个文本段要尽可能多地覆盖用户输入文本的各方面知识和细节,包括但不限于主题、概念、关键信息等。对于关键的数字、理论、细节等,要严格遵循原文,不能进行任何虚构和捏造不存在的知识,确保输出内容准确、真实且全面。
输出格式为纯文本段,分段之间使用“---”作为分割,方便后续使用代码进行切分
输出文本避免添加markdown格式保持文本格式紧凑
切分过程中,要注重保持文本的完整性和逻辑性,确保每个文本段都能独立地表达出清晰、准确的信息,以便更好地进行知识库检索。
""",
context.getContent(),
context.getMetadata()
@@ -153,7 +153,7 @@ public class EmbeddingNodes {
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
context.getDocuments().addAll(llmSplit(
"""
对用户输入的文本,生成组高质量的问答对。请遵循以下指南:
对用户输入的文本,生成组高质量的问答对。请遵循以下指南:
1. 问题部分:
为同一个主题创建尽可能多的不同表述的问题,确保问题的多样性。
每个问题应考虑用户可能的多种问法,例如:
@@ -168,14 +168,20 @@ public class EmbeddingNodes {
答案应直接基于给定文本,确保准确性和一致性。
包含相关的细节,如日期、名称、职位等具体信息,必要时提供背景信息以增强理解。
3. 格式:
使用"Q:"标记问题集合的开始,所有问题应在一个段落内,问题之间用空格分隔。
使用"A:"标记答案的开始,答案应清晰分段,便于阅读。
问答对之间用两个空行分隔,以提高可读性。
避免使用任何Markdown格式
使用"问:"标记问题集合的开始,所有问题应在一个段落内,问题之间用空格分隔。
使用"答:"标记答案的开始,答案应清晰分段,便于阅读。
问答对之间用“---”分隔,以提高可读性。
4. 内容要求:
确保问答对紧密围绕文本主题,避免偏离主题。
避免添加文本中未提及的信息,确保信息的真实性。
一个问题搭配一个答案,避免一组问答对中同时涉及多个问题。
如果文本信息不足以回答某个方面,可以在答案中说明 "根据给定信息无法确定",并尽量提供相关的上下文。
格式样例:
问:苹果通常是什么颜色的?
答:红色。
---
问:苹果长在树上还是地上?
答:苹果长在树上。
""",
context.getContent(),
context.getMetadata()
@@ -189,13 +195,33 @@ public class EmbeddingNodes {
.call()
.content();
Assert.notBlank(response, "LLM response is empty");
logger.info("{}", response);
// noinspection DataFlowIssue
return Arrays.stream(StrUtil.trim(response).split("(s?)\\s*\\n\\n"))
return Arrays.stream(StrUtil.trim(response).split("---"))
.map(text -> text.replaceAll("(?!^.+) +$", ""))
.map(StrUtil::trim)
.map(text -> Document.builder()
.text(text)
.metadata(metadata)
.metadata(Optional.ofNullable(metadata).orElse(new HashMap<>()))
.build())
.toList();
}
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = "import_vector_source", nodeName = "导入向量库", nodeType = NodeTypeEnum.COMMON)
public void importVectorSource(NodeComponent node) {
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
if (ObjectUtil.isNotEmpty(context.getDocuments())) {
VectorStore vs = QdrantVectorStore.builder(qdrantClient, embeddingModel)
.collectionName(String.valueOf(context.getVectorSourceId()))
.build();
for (Document document : context.getDocuments()) {
Map<String, Object> metadata = document.getMetadata();
metadata.put("filename", context.getFileFormat());
metadata.put("filepath", context.getFile());
metadata.put("group_id", String.valueOf(context.getGroupId()));
metadata.put("vector_source_id", String.valueOf(context.getVectorSourceId()));
}
vs.add(context.getDocuments());
}
}
}

View File

@@ -36,7 +36,7 @@ spring:
model: 'Qwen3-1.7-vllm'
embedding:
options:
model: 'Bge-m3'
model: 'Bge-m3-vllm'
vectorstore:
qdrant:
api-key: lanyuanxiaoyao
@@ -49,3 +49,6 @@ liteflow:
rule-source: config/flow.xml
print-banner: false
check-node-exists: false
knowledge:
download-prefix: "http://localhost:8080"
upload-path: /Users/lanyuanxiaoyao/Project/IdeaProjects/hudi-service/service-ai/temp

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flow PUBLIC "liteflow" "https://liteflow.cc/liteflow.dtd">
<flow>
<chain id="embedding">
<chain id="embedding_preview">
SER(
IF(
embedding_check_if_file_needed,
@@ -17,4 +17,7 @@
)
)
</chain>
<chain id="embedding_submit">
SER(embedding_preview, import_vector_source)
</chain>
</flow>

View File

@@ -0,0 +1,59 @@
package com.lanyuanxiaoyao.service.ai.knowledge;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections;
import java.net.http.HttpClient;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.reactive.JdkClientHttpConnector;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author lanyuanxiaoyao
* @version 20250527
*/
public class TestEmbedding {
public static void main(String[] args) throws ExecutionException, InterruptedException {
HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
EmbeddingModel model = new OpenAiEmbeddingModel(
OpenAiApi.builder()
.baseUrl("http://132.121.206.65:10086")
.apiKey("*XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj<MYQd$&@$sY7CgqNyea4giJi4")
.webClientBuilder(WebClient.builder().clientConnector(new JdkClientHttpConnector(httpClient)))
.restClientBuilder(RestClient.builder().requestFactory(new JdkClientHttpRequestFactory(httpClient)))
.build(),
MetadataMode.ALL,
OpenAiEmbeddingOptions.builder()
.model("Bge-m3-vllm")
.build()
);
QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build()
);
client.createCollectionAsync(
"test",
Collections.VectorParams.newBuilder()
.setDistance(Collections.Distance.Cosine)
.setSize(1024)
.build()
).get();
VectorStore store = QdrantVectorStore.builder(client, model)
.initializeSchema(true)
.collectionName("test")
.build();
store.add(List.of(new Document("hello world")));
}
}

View File

@@ -0,0 +1,116 @@
package com.lanyuanxiaoyao.service.ai.knowledge;
import java.net.http.HttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.reactive.JdkClientHttpConnector;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
/**
* @author lanyuanxiaoyao
* @version 20250526
*/
public class TestLlm {
private static final Logger log = LoggerFactory.getLogger(TestLlm.class);
public static void main(String[] args) {
HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
ChatClient client = ChatClient.builder(
OpenAiChatModel.builder()
.openAiApi(
OpenAiApi.builder()
.baseUrl("http://132.121.206.65:10086")
.apiKey("*XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj<MYQd$&@$sY7CgqNyea4giJi4")
.webClientBuilder(WebClient.builder().clientConnector(new JdkClientHttpConnector(httpClient)))
.restClientBuilder(RestClient.builder().requestFactory(new JdkClientHttpRequestFactory(httpClient)))
.build()
)
.defaultOptions(
OpenAiChatOptions.builder()
.model("Qwen3-1.7-vllm")
.build()
)
.build()
)
.build();
String content = client.prompt()
.system("""
对用户输入的文本,生成多组高质量的问答对。请遵循以下指南:
1. 问题部分:
为同一个主题创建尽可能多的不同表述的问题,确保问题的多样性。
每个问题应考虑用户可能的多种问法,例如:
直接询问(如“什么是...?”)
请求确认(如“是否可以说...?”)
寻求解释(如“请解释一下...的含义。”)
假设性问题(如“如果...会怎样?”)
例子请求(如“能否举个例子说明...?”)
问题应涵盖文本中的关键信息、主要概念和细节,确保不遗漏重要内容。
2. 答案部分:
提供一个全面、信息丰富的答案,涵盖问题的所有可能角度,确保逻辑连贯。
答案应直接基于给定文本,确保准确性和一致性。
包含相关的细节,如日期、名称、职位等具体信息,必要时提供背景信息以增强理解。
3. 格式:
使用"问:"标记问题集合的开始,所有问题应在一个段落内,问题之间用空格分隔。
使用"答:"标记答案的开始,答案应清晰分段,便于阅读。
问答对之间用“---”分隔,以提高可读性。
4. 内容要求:
确保问答对紧密围绕文本主题,避免偏离主题。
避免添加文本中未提及的信息,确保信息的真实性。
一个问题搭配一个答案,避免一组问答对中同时涉及多个问题。
如果文本信息不足以回答某个方面,可以在答案中说明 "根据给定信息无法确定",并尽量提供相关的上下文。
""")
.user("""
Apache Hudi 是一款开源的数据湖管理框架,专注于高效处理大规模数据集的增量更新和实时操作。以下是其核心要点介绍:
核心功能与特性
增量数据处理支持插入Insert、更新Update、删除Delete操作并通过“Upsert”插入或更新机制高效处理变更数据避免重写整个数据集。
事务支持:提供 ACID 事务保证,确保数据一致性和可靠性,支持原子提交和回滚。
存储优化:自动合并小文件并维护文件最佳大小,减少存储碎片化问题,提升查询性能。
多查询类型:
快照查询:获取最新数据状态(对 MoR 表合并基础文件和增量文件)。
增量查询:捕获自某次提交后的变更数据流,适用于增量管道。
读取优化查询:针对 MoR 表展示最新压缩后的数据。
存储模型
写入时复制Copy-on-Write, CoW数据以列式格式如 Parquet存储每次更新生成新版本文件适合读多写少的场景。
读取时合并Merge-on-Read, MoR结合列式Parquet和行式Avro存储更新写入增量文件后异步合并适合写频繁的场景。
适用场景
合规性需求:支持 GDPR、CCPA 等法规要求的数据删除和更新。
实时数据流处理:如 IoT 设备数据、CDC变更数据捕获系统实现近实时分析。
数据湖管理:优化大规模数据集的存储和查询效率,支持时间旅行查询和历史版本回溯。
技术生态与兼容性
多引擎支持:与 Spark、Flink、Hive、Presto、Trino 等计算和查询引擎集成。
存储格式:基于 Parquet 和 Avro兼容 Hadoop 生态及云存储(如 Amazon S3
优势对比
相较于同类产品(如 Apache Iceberg、Delta LakeHudi 在实时更新和增量处理方面表现突出,尤其适合需要频繁数据变更的场景。其独特的 MoR 模型在写入性能上优于传统批处理方案。
总结
Apache Hudi 通过灵活的存储模型、高效的事务管理和广泛的生态系统集成,成为构建现代化数据湖的核心工具,适用于金融、物联网、实时分析等对数据新鲜度和操作效率要求高的领域。
""")
.call()
.content();
log.info(content);
}
}

View File

@@ -1,30 +1,85 @@
import React from 'react'
import {useParams} from 'react-router'
import {amisRender, crudCommonOptions} from '../../../util/amis.tsx'
import {useNavigate, useParams} from 'react-router'
import {amisRender, crudCommonOptions, mappingField, mappingItem} from '../../../util/amis.tsx'
const statusMapping = [
mappingItem('解析中', 'RUNNING', 'bg-warning'),
mappingItem('使用中', 'FINISHED', 'bg-success'),
]
const DataDetail: React.FC = () => {
const {name} = useParams()
const {knowledge_id} = useParams()
const navigate = useNavigate()
return (
<div className="import-detail h-full">
{amisRender(
{
className: 'h-full',
type: 'page',
title: `数据详情 (知识库:${name}`,
title: {
type: 'wrapper',
size: 'none',
body: [
'数据详情 (知识库:',
{
type: 'service',
className: 'inline',
api: {
method: 'get',
url: 'http://127.0.0.1:8080/knowledge/name',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
data: {
id: knowledge_id,
},
},
body: {
type: 'tpl',
tpl: '${name}',
},
},
'',
],
},
size: 'lg',
actions: [],
body: [
{
type: 'crud',
api: {
url: 'http://127.0.0.1:8080/knowledge/list_points?name=${name}',
method: 'get',
url: 'http://127.0.0.1:8080/group/list',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
data: {
knowledge_id,
},
},
...crudCommonOptions(),
headerToolbar: [
'reload',
{
type: 'action',
icon: 'fa fa-plus',
label: '',
tooltip: '新增',
tooltipPlacement: 'top',
onEvent: {
click: {
actions: [
{
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate(`/ai/knowledge/import/${knowledge_id}`)
},
},
],
},
},
},
],
columns: [
{
@@ -32,8 +87,15 @@ const DataDetail: React.FC = () => {
hidden: true,
},
{
name: 'text',
label: '内容',
name: 'name',
label: '文件名',
},
{
name: 'status',
label: '状态',
width: 50,
align: 'center',
...mappingField('status', statusMapping),
},
{
type: 'operation',
@@ -42,30 +104,17 @@ const DataDetail: React.FC = () => {
buttons: [
{
type: 'action',
label: '编辑',
label: '查看',
level: 'link',
size: 'lg',
actionType: 'dialog',
dialog: {
title: '编辑文段',
size: 'md',
body: {
type: 'form',
body: [
size: 'sm',
onEvent: {
click: {
actions: [
{
type: 'input-text',
name: 'id',
disabled: true,
label: '文段ID',
},
{
type: 'editor',
label: '内容',
name: 'text',
language: 'plaintext',
options: {
lineNumbers: 'off',
wordWrap: 'bounded',
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate(`/ai/knowledge/detail/${knowledge_id}/segment/${context.props.data['id']}`)
},
},
],
@@ -77,13 +126,17 @@ const DataDetail: React.FC = () => {
label: '删除',
className: 'text-danger hover:text-red-600',
level: 'link',
size: 'xs',
size: 'sm',
actionType: 'ajax',
api: {
method: 'get',
url: 'http://127.0.0.1:8080/group/delete',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
data: {
id: '${id}',
},
},
confirmText: '确认删除',
confirmTitle: '删除',

View File

@@ -10,12 +10,37 @@ const ImportDataDiv = styled.div`
`
const DataImport: React.FC = () => {
const {name} = useParams()
const {knowledge_id} = useParams()
return (
<ImportDataDiv className="import-data h-full">
{amisRender({
type: 'page',
title: `数据导入 (知识库:${name}`,
title: {
type: 'wrapper',
size: 'none',
body: [
'数据导入 (知识库:',
{
type: 'service',
className: 'inline',
api: {
method: 'get',
url: 'http://127.0.0.1:8080/knowledge/name',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
data: {
id: knowledge_id,
},
},
body: {
type: 'tpl',
tpl: '${name}',
},
},
'',
],
},
body: [
[
{
@@ -53,7 +78,7 @@ const DataImport: React.FC = () => {
name: 'type',
type: 'radios',
label: '数据形式',
value: 'text',
value: 'file',
options: [
{
value: 'text',
@@ -62,7 +87,6 @@ const DataImport: React.FC = () => {
{
value: 'file',
label: '文件',
disabled: true,
},
],
},
@@ -82,10 +106,38 @@ const DataImport: React.FC = () => {
type: 'input-file',
name: 'files',
label: '数据文件',
accept: '.txt,.csv',
autoUpload: false,
drag: true,
multiple: true,
useChunk: true,
accept: '*',
// 5MB 5242880
// 100MB 104857600
// 500MB 524288000
// 1GB 1073741824
maxSize: '',
maxLength: 0,
startChunkApi: {
method: 'post',
url: 'http://127.0.0.1:8080/upload/start',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
},
chunkApi: {
method: 'post',
url: 'http://127.0.0.1:8080/upload/slice',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
},
finishChunkApi: {
method: 'post',
url: 'http://127.0.0.1:8080/upload/finish',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
},
},
{
className: 'text-right',
@@ -103,16 +155,10 @@ const DataImport: React.FC = () => {
},
dataType: 'form',
data: {
mode: '${mode}',
type: '${type}',
content: '${content}',
},
// @ts-ignore
adaptor: (payload, response, api, context) => {
console.log(payload)
return {
items: payload,
}
mode: '${mode|default:undefined}',
type: '${type|default:undefined}',
content: '${content|default:undefined}',
files: '${files|default:undefined}',
},
},
reload: 'preview_list?rows=${items}',
@@ -121,6 +167,22 @@ const DataImport: React.FC = () => {
type: 'submit',
label: '提交',
level: 'primary',
actionType: 'ajax',
api: {
method: 'post',
url: 'http://127.0.0.1:8080/knowledge/submit_text',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
dataType: 'form',
data: {
id: knowledge_id,
mode: '${mode|default:undefined}',
type: '${type|default:undefined}',
content: '${content|default:undefined}',
files: '${files|default:undefined}',
},
},
},
],
},
@@ -155,6 +217,7 @@ const DataImport: React.FC = () => {
],
listItem: {
body: {
className: 'white-space-pre-line',
type: 'tpl',
tpl: '${text}',
},

View File

@@ -0,0 +1,133 @@
import React from 'react'
import {useParams} from 'react-router'
import {amisRender, crudCommonOptions} from '../../../util/amis.tsx'
const DataDetail: React.FC = () => {
const {knowledge_id, group_id} = useParams()
return (
<div className="import-detail h-full">
{amisRender(
{
className: 'h-full',
type: 'page',
title: {
type: 'wrapper',
size: 'none',
body: [
'数据详情 (知识库:',
{
type: 'service',
className: 'inline',
api: {
method: 'get',
url: 'http://127.0.0.1:8080/knowledge/name',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
data: {
id: knowledge_id,
},
},
body: {
type: 'tpl',
tpl: '${name}',
},
},
'',
],
},
size: 'lg',
actions: [],
body: [
{
type: 'crud',
api: {
method: 'get',
url: 'http://127.0.0.1:8080/segment/list',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
data: {
knowledge_id,
group_id,
},
},
...crudCommonOptions(),
headerToolbar: [
'reload',
],
columns: [
{
name: 'id',
hidden: true,
},
{
name: 'text',
label: '内容',
className: 'white-space-pre-line',
},
{
type: 'operation',
label: '操作',
width: 50,
buttons: [
/*{
type: 'action',
label: '编辑',
level: 'link',
size: 'lg',
actionType: 'dialog',
dialog: {
title: '编辑文段',
size: 'md',
body: {
type: 'form',
body: [
{
type: 'input-text',
name: 'id',
disabled: true,
label: '文段ID',
},
{
type: 'editor',
label: '内容',
name: 'text',
language: 'plaintext',
options: {
lineNumbers: 'off',
wordWrap: 'bounded',
},
},
],
},
},
},*/
{
type: 'action',
label: '删除',
className: 'text-danger hover:text-red-600',
level: 'link',
size: 'sm',
actionType: 'ajax',
api: {
method: 'get',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
},
confirmText: '删除后无法恢复,确认删除该记录?',
confirmTitle: '删除',
},
],
},
],
},
],
},
)}
</div>
)
}
export default DataDetail

View File

@@ -38,6 +38,8 @@ const Knowledge: React.FC = () => {
type: 'action',
label: '',
icon: 'fa fa-plus',
tooltip: '新增',
tooltipPlacement: 'top',
actionType: 'dialog',
dialog: {
title: '新增知识库',
@@ -111,7 +113,7 @@ const Knowledge: React.FC = () => {
type: 'action',
label: '详情',
level: 'link',
size: 'xs',
size: 'sm',
onEvent: {
click: {
actions: [
@@ -119,7 +121,7 @@ const Knowledge: React.FC = () => {
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate(`/ai/knowledge/detail/${context.props.data['name']}`)
navigate(`/ai/knowledge/detail/${context.props.data['id']}`)
},
},
],
@@ -130,7 +132,7 @@ const Knowledge: React.FC = () => {
type: 'action',
label: '导入',
level: 'link',
size: 'xs',
size: 'sm',
onEvent: {
click: {
actions: [
@@ -138,7 +140,7 @@ const Knowledge: React.FC = () => {
actionType: 'custom',
// @ts-ignore
script: (context, action, event) => {
navigate(`/ai/knowledge/import/${context.props.data['name']}`)
navigate(`/ai/knowledge/import/${context.props.data['id']}`)
},
},
],
@@ -150,14 +152,17 @@ const Knowledge: React.FC = () => {
label: '删除',
className: 'text-danger hover:text-red-600',
level: 'link',
size: 'xs',
size: 'sm',
actionType: 'ajax',
api: {
method: 'get',
url: 'http://127.0.0.1:8080/knowledge/delete?name=${name}',
url: 'http://127.0.0.1:8080/knowledge/delete',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
},
data: {
id: '${id}',
},
},
confirmText: '确认删除',
confirmTitle: '删除',

View File

@@ -17,6 +17,7 @@ import Conversation from './pages/ai/Conversation.tsx'
import Inspection from './pages/ai/Inspection.tsx'
import DataDetail from './pages/ai/knowledge/DataDetail.tsx'
import DataImport from './pages/ai/knowledge/DataImport.tsx'
import DataSegment from './pages/ai/knowledge/DataSegment.tsx'
import Knowledge from './pages/ai/knowledge/Knowledge.tsx'
import App from './pages/App.tsx'
import Cloud from './pages/overview/Cloud.tsx'
@@ -95,13 +96,17 @@ export const routes: RouteObject[] = [
Component: Knowledge,
},
{
path: 'knowledge/import/:name',
path: 'knowledge/import/:knowledge_id',
Component: DataImport,
},
{
path: 'knowledge/detail/:name',
path: 'knowledge/detail/:knowledge_id',
Component: DataDetail,
},
{
path: 'knowledge/detail/:knowledge_id/segment/:group_id',
Component: DataSegment,
},
],
},
],

View File

@@ -77,6 +77,7 @@ export const amisRender = (schema: Schema, data: Record<any, any> = {}) => {
theme: theme,
},
{
enableAMISDebug: true,
fetcher: async (api: any) => {
let {url, method, data, responseType, config, headers} = api
config = config || {}