feat(knowledge): 完成知识库基本功能开发
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<spring-boot.version>3.4.3</spring-boot.version>
|
<spring-boot.version>3.4.3</spring-boot.version>
|
||||||
<spring-cloud.version>2024.0.1</spring-cloud.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>
|
<eclipse-collections.version>11.1.0</eclipse-collections.version>
|
||||||
<curator.version>5.1.0</curator.version>
|
<curator.version>5.1.0</curator.version>
|
||||||
<hutool.version>5.8.27</hutool.version>
|
<hutool.version>5.8.27</hutool.version>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
|||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||||
import org.springframework.retry.annotation.EnableRetry;
|
import org.springframework.retry.annotation.EnableRetry;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author lanyuanxiaoyao
|
* @author lanyuanxiaoyao
|
||||||
@@ -18,6 +19,7 @@ import org.springframework.retry.annotation.EnableRetry;
|
|||||||
@EnableConfigurationProperties
|
@EnableConfigurationProperties
|
||||||
@EnableEncryptableProperties
|
@EnableEncryptableProperties
|
||||||
@EnableRetry
|
@EnableRetry
|
||||||
|
@EnableScheduling
|
||||||
public class KnowledgeApplication implements ApplicationRunner {
|
public class KnowledgeApplication implements ApplicationRunner {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(KnowledgeApplication.class, args);
|
SpringApplication.run(KnowledgeApplication.class, args);
|
||||||
|
|||||||
@@ -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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,17 @@
|
|||||||
package com.lanyuanxiaoyao.service.ai.knowledge.controller;
|
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.AmisMapResponse;
|
||||||
import com.lanyuanxiaoyao.service.ai.core.entity.amis.AmisResponse;
|
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.EmbeddingService;
|
||||||
import com.lanyuanxiaoyao.service.ai.knowledge.service.KnowledgeService;
|
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.concurrent.ExecutionException;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import org.eclipse.collections.api.factory.Lists;
|
import org.eclipse.collections.api.factory.Lists;
|
||||||
import org.eclipse.collections.api.list.ImmutableList;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -38,14 +27,10 @@ public class KnowledgeController {
|
|||||||
|
|
||||||
private final KnowledgeService knowledgeService;
|
private final KnowledgeService knowledgeService;
|
||||||
private final EmbeddingService embeddingService;
|
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.knowledgeService = knowledgeService;
|
||||||
this.embeddingService = embeddingService;
|
this.embeddingService = embeddingService;
|
||||||
client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
|
|
||||||
this.embeddingModel = embeddingModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("add")
|
@PostMapping("add")
|
||||||
@@ -56,73 +41,68 @@ public class KnowledgeController {
|
|||||||
knowledgeService.add(name, strategy);
|
knowledgeService.add(name, strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("name")
|
||||||
|
public AmisMapResponse name(@RequestParam("id") Long id) {
|
||||||
|
return AmisResponse.responseMapData()
|
||||||
|
.setData("name", knowledgeService.getName(id));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("list")
|
@GetMapping("list")
|
||||||
public AmisResponse<?> list() {
|
public AmisResponse<?> list() {
|
||||||
return AmisResponse.responseCrudData(knowledgeService.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")
|
@GetMapping("delete")
|
||||||
public void delete(@RequestParam("name") String name) throws ExecutionException, InterruptedException {
|
public void delete(@RequestParam("id") Long id) throws ExecutionException, InterruptedException {
|
||||||
knowledgeService.remove(name);
|
knowledgeService.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("preview_text")
|
@PostMapping("preview_text")
|
||||||
public AmisResponse<?> previewText(
|
public AmisResponse<?> previewText(
|
||||||
@RequestParam(value = "mode", defaultValue = "NORMAL") String mode,
|
@RequestParam(value = "mode", defaultValue = "NORMAL") String mode,
|
||||||
@RequestParam(value = "type", defaultValue = "text") String type,
|
@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(
|
if (StrUtil.equals("text", type)) {
|
||||||
embeddingService.split(mode, content)
|
return AmisResponse.responseCrudData(
|
||||||
.collect(doc -> {
|
embeddingService.preview(mode, content)
|
||||||
PointVO vo = new PointVO();
|
.collect(doc -> {
|
||||||
vo.setId(doc.getId());
|
SegmentVO vo = new SegmentVO();
|
||||||
vo.setText(doc.getText());
|
vo.setId(doc.getId());
|
||||||
return vo;
|
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")
|
@PostMapping("submit_text")
|
||||||
public void processText(
|
public void submitText(
|
||||||
@RequestParam("name") String name,
|
@RequestParam(value = "id") Long id,
|
||||||
@RequestBody String text
|
@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)
|
if (StrUtil.equals("text", type)) {
|
||||||
.collectionName(name)
|
embeddingService.submit(id, mode, content);
|
||||||
.initializeSchema(true)
|
} else if (StrUtil.equals("file", type)) {
|
||||||
.build();
|
embeddingService.submit(id, mode, Lists.immutable.of(files.split(",")));
|
||||||
MarkdownDocumentReader reader = new MarkdownDocumentReader(
|
} else {
|
||||||
new ByteArrayResource(text.getBytes(StandardCharsets.UTF_8)),
|
throw new IllegalArgumentException("Unsupported type: " + type);
|
||||||
MarkdownDocumentReaderConfig.builder()
|
}
|
||||||
.withHorizontalRuleCreateDocument(true)
|
|
||||||
.withIncludeCodeBlock(false)
|
|
||||||
.withIncludeBlockquote(false)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
source.add(reader.get());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
package com.lanyuanxiaoyao.service.ai.knowledge.entity;
|
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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.eclipse.collections.api.factory.Lists;
|
import org.eclipse.collections.api.factory.Lists;
|
||||||
@@ -15,36 +11,42 @@ import org.springframework.ai.document.Document;
|
|||||||
* @version 20250523
|
* @version 20250523
|
||||||
*/
|
*/
|
||||||
public class EmbeddingContext {
|
public class EmbeddingContext {
|
||||||
|
private Long vectorSourceId;
|
||||||
|
private Long groupId;
|
||||||
private Config config;
|
private Config config;
|
||||||
private String content;
|
private String content;
|
||||||
private String file;
|
private String file;
|
||||||
|
private String fileFormat;
|
||||||
private List<Document> documents = Lists.mutable.empty();
|
private List<Document> documents = Lists.mutable.empty();
|
||||||
private Map<String, Object> metadata = Maps.mutable.empty();
|
private Map<String, Object> metadata = Maps.mutable.empty();
|
||||||
|
|
||||||
public EmbeddingContext(String content) {
|
private EmbeddingContext(Builder builder) {
|
||||||
this(content, new Config());
|
setVectorSourceId(builder.vectorSourceId);
|
||||||
|
setGroupId(builder.groupId);
|
||||||
|
setConfig(builder.config);
|
||||||
|
setContent(builder.content);
|
||||||
|
setFile(builder.file);
|
||||||
|
setFileFormat(builder.fileFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbeddingContext(String content, Config config) {
|
public static Builder builder() {
|
||||||
this.content = StrUtil.trim(content);
|
return new Builder();
|
||||||
this.config = config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbeddingContext(File file) {
|
public Long getGroupId() {
|
||||||
this(file, new Config());
|
return groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbeddingContext(File file, Config config) {
|
public void setGroupId(Long groupId) {
|
||||||
this.file = file.getAbsolutePath();
|
this.groupId = groupId;
|
||||||
this.config = config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbeddingContext(Path path) {
|
public Long getVectorSourceId() {
|
||||||
this(path.toFile());
|
return vectorSourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbeddingContext(Path path, Config config) {
|
public void setVectorSourceId(Long vectorSourceId) {
|
||||||
this(path.toFile(), config);
|
this.vectorSourceId = vectorSourceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Config getConfig() {
|
public Config getConfig() {
|
||||||
@@ -71,6 +73,14 @@ public class EmbeddingContext {
|
|||||||
this.file = file;
|
this.file = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFileFormat() {
|
||||||
|
return fileFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFileFormat(String fileFormat) {
|
||||||
|
this.fileFormat = fileFormat;
|
||||||
|
}
|
||||||
|
|
||||||
public List<Document> getDocuments() {
|
public List<Document> getDocuments() {
|
||||||
return documents;
|
return documents;
|
||||||
}
|
}
|
||||||
@@ -90,9 +100,12 @@ public class EmbeddingContext {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "EmbeddingContext{" +
|
return "EmbeddingContext{" +
|
||||||
"config=" + config +
|
"vectorSourceId=" + vectorSourceId +
|
||||||
|
", groupId=" + groupId +
|
||||||
|
", config=" + config +
|
||||||
", content='" + content + '\'' +
|
", content='" + content + '\'' +
|
||||||
", file='" + file + '\'' +
|
", file='" + file + '\'' +
|
||||||
|
", fileFormat='" + fileFormat + '\'' +
|
||||||
", documents=" + documents +
|
", documents=" + documents +
|
||||||
", metadata=" + metadata +
|
", metadata=" + metadata +
|
||||||
'}';
|
'}';
|
||||||
@@ -101,11 +114,10 @@ public class EmbeddingContext {
|
|||||||
public static final class Config {
|
public static final class Config {
|
||||||
private SplitStrategy splitStrategy = SplitStrategy.NORMAL;
|
private SplitStrategy splitStrategy = SplitStrategy.NORMAL;
|
||||||
|
|
||||||
public Config() {
|
private Config(Builder builder) {setSplitStrategy(builder.splitStrategy);}
|
||||||
}
|
|
||||||
|
|
||||||
public Config(SplitStrategy splitStrategy) {
|
public static Builder builder() {
|
||||||
this.splitStrategy = splitStrategy;
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SplitStrategy getSplitStrategy() {
|
public SplitStrategy getSplitStrategy() {
|
||||||
@@ -126,5 +138,65 @@ public class EmbeddingContext {
|
|||||||
public enum SplitStrategy {
|
public enum SplitStrategy {
|
||||||
NORMAL, LLM, QA
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ public class Knowledge {
|
|||||||
private Long vectorSourceId;
|
private Long vectorSourceId;
|
||||||
private String name;
|
private String name;
|
||||||
private String strategy;
|
private String strategy;
|
||||||
|
private Long createdTime;
|
||||||
|
private Long modifiedTime;
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
@@ -42,6 +44,22 @@ public class Knowledge {
|
|||||||
this.strategy = strategy;
|
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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Knowledge{" +
|
return "Knowledge{" +
|
||||||
@@ -49,6 +67,8 @@ public class Knowledge {
|
|||||||
", vectorSourceId=" + vectorSourceId +
|
", vectorSourceId=" + vectorSourceId +
|
||||||
", name='" + name + '\'' +
|
", name='" + name + '\'' +
|
||||||
", strategy='" + strategy + '\'' +
|
", strategy='" + strategy + '\'' +
|
||||||
|
", createdTime=" + createdTime +
|
||||||
|
", modifiedTime=" + modifiedTime +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,23 @@ package com.lanyuanxiaoyao.service.ai.knowledge.entity.vo;
|
|||||||
* @version 20250516
|
* @version 20250516
|
||||||
*/
|
*/
|
||||||
public class KnowledgeVO {
|
public class KnowledgeVO {
|
||||||
|
private String id;
|
||||||
private String name;
|
private String name;
|
||||||
private String strategy;
|
private String strategy;
|
||||||
private Long size;
|
private Long size;
|
||||||
private Long points;
|
private Long points;
|
||||||
private Long segments;
|
private Long segments;
|
||||||
private String status;
|
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() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
@@ -60,15 +71,34 @@ public class KnowledgeVO {
|
|||||||
this.status = 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
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "CollectionVO{" +
|
return "KnowledgeVO{" +
|
||||||
"name='" + name + '\'' +
|
"id='" + id + '\'' +
|
||||||
|
", name='" + name + '\'' +
|
||||||
", strategy='" + strategy + '\'' +
|
", strategy='" + strategy + '\'' +
|
||||||
", size=" + size +
|
", size=" + size +
|
||||||
", points=" + points +
|
", points=" + points +
|
||||||
", segments=" + segments +
|
", segments=" + segments +
|
||||||
", status='" + status + '\'' +
|
", status='" + status + '\'' +
|
||||||
|
", createdTime=" + createdTime +
|
||||||
|
", modifiedTime=" + modifiedTime +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ package com.lanyuanxiaoyao.service.ai.knowledge.entity.vo;
|
|||||||
* @author lanyuanxiaoyao
|
* @author lanyuanxiaoyao
|
||||||
* @version 20250516
|
* @version 20250516
|
||||||
*/
|
*/
|
||||||
public class PointVO {
|
public class SegmentVO {
|
||||||
private String id;
|
private String id;
|
||||||
private String text;
|
private String text;
|
||||||
|
|
||||||
@@ -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("更新文件信息失败,请重新上传");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
package com.lanyuanxiaoyao.service.ai.knowledge.service;
|
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.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 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.factory.Lists;
|
||||||
import org.eclipse.collections.api.list.ImmutableList;
|
import org.eclipse.collections.api.list.ImmutableList;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -18,19 +25,75 @@ import org.springframework.stereotype.Service;
|
|||||||
public class EmbeddingService {
|
public class EmbeddingService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(EmbeddingService.class);
|
private static final Logger logger = LoggerFactory.getLogger(EmbeddingService.class);
|
||||||
|
|
||||||
|
private final DataFileService dataFileService;
|
||||||
private final FlowExecutor executor;
|
private final FlowExecutor executor;
|
||||||
|
private final KnowledgeService knowledgeService;
|
||||||
|
private final GroupService groupService;
|
||||||
|
private final ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||||
|
|
||||||
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
||||||
public EmbeddingService(FlowExecutor executor) {
|
public EmbeddingService(DataFileService dataFileService, FlowExecutor executor, KnowledgeService knowledgeService, GroupService groupService) {
|
||||||
|
this.dataFileService = dataFileService;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
|
this.knowledgeService = knowledgeService;
|
||||||
|
this.groupService = groupService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImmutableList<Document> split(String mode, String content) {
|
public ImmutableList<Document> preview(String mode, String content) {
|
||||||
EmbeddingContext context = new EmbeddingContext(
|
if (content.length() > 2000) {
|
||||||
content,
|
content = content.substring(0, 2000);
|
||||||
new EmbeddingContext.Config(EmbeddingContext.Config.SplitStrategy.valueOf(mode))
|
}
|
||||||
);
|
EmbeddingContext context = EmbeddingContext.builder()
|
||||||
executor.execute2Resp("embedding", null, context);
|
.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());
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.ai.embedding.EmbeddingModel;
|
import org.springframework.ai.embedding.EmbeddingModel;
|
||||||
import org.springframework.ai.vectorstore.VectorStore;
|
import org.springframework.ai.vectorstore.VectorStore;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -26,43 +27,41 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class KnowledgeService {
|
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 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 JdbcTemplate template;
|
||||||
private final EmbeddingModel embeddingModel;
|
private final EmbeddingModel embeddingModel;
|
||||||
private final QdrantClient client;
|
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.template = template;
|
||||||
this.embeddingModel = embeddingModel;
|
this.embeddingModel = embeddingModel;
|
||||||
this.client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
|
this.client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
|
||||||
this.knowledgeGroupService = knowledgeGroupService;
|
this.groupService = groupService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Knowledge get(Long id) {
|
public Knowledge get(Long id) {
|
||||||
return template.queryForObject(
|
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)
|
.from(KNOWLEDGE_TABLE_NAME)
|
||||||
.whereEq("id", "?")
|
.whereEq("id", "?")
|
||||||
.precompileSql(),
|
.precompileSql(),
|
||||||
Knowledge.class,
|
knowledgeMapper,
|
||||||
id
|
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)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void add(String name, String strategy) throws ExecutionException, InterruptedException {
|
public void add(String name, String strategy) throws ExecutionException, InterruptedException {
|
||||||
Integer count = template.queryForObject(
|
Integer count = template.queryForObject(
|
||||||
@@ -98,25 +97,31 @@ public class KnowledgeService {
|
|||||||
).get();
|
).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() {
|
public ImmutableList<KnowledgeVO> list() {
|
||||||
return template.query(
|
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)
|
.from(KNOWLEDGE_TABLE_NAME)
|
||||||
|
.orderByDesc("created_time")
|
||||||
.build(),
|
.build(),
|
||||||
(rs, index) -> {
|
knowledgeMapper
|
||||||
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;
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.stream()
|
.stream()
|
||||||
.map(knowledge -> {
|
.map(knowledge -> {
|
||||||
try {
|
try {
|
||||||
Collections.CollectionInfo info = client.getCollectionInfoAsync(String.valueOf(knowledge.getVectorSourceId())).get();
|
Collections.CollectionInfo info = client.getCollectionInfoAsync(String.valueOf(knowledge.getVectorSourceId())).get();
|
||||||
KnowledgeVO vo = new KnowledgeVO();
|
KnowledgeVO vo = new KnowledgeVO();
|
||||||
|
vo.setId(String.valueOf(knowledge.getId()));
|
||||||
vo.setName(knowledge.getName());
|
vo.setName(knowledge.getName());
|
||||||
vo.setPoints(info.getPointsCount());
|
vo.setPoints(info.getPointsCount());
|
||||||
vo.setSegments(info.getSegmentsCount());
|
vo.setSegments(info.getSegmentsCount());
|
||||||
@@ -124,6 +129,8 @@ public class KnowledgeService {
|
|||||||
Collections.VectorParams vectorParams = info.getConfig().getParams().getVectorsConfig().getParams();
|
Collections.VectorParams vectorParams = info.getConfig().getParams().getVectorsConfig().getParams();
|
||||||
vo.setStrategy(vectorParams.getDistance().name());
|
vo.setStrategy(vectorParams.getDistance().name());
|
||||||
vo.setSize(vectorParams.getSize());
|
vo.setSize(vectorParams.getSize());
|
||||||
|
vo.setCreatedTime(vo.getCreatedTime());
|
||||||
|
vo.setModifiedTime(vo.getModifiedTime());
|
||||||
return vo;
|
return vo;
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
@@ -134,8 +141,8 @@ public class KnowledgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void remove(String name) throws ExecutionException, InterruptedException {
|
public void remove(Long id) throws ExecutionException, InterruptedException {
|
||||||
Knowledge knowledge = get(name);
|
Knowledge knowledge = get(id);
|
||||||
if (ObjectUtil.isNull(knowledge)) {
|
if (ObjectUtil.isNull(knowledge)) {
|
||||||
throw new RuntimeException(StrUtil.format("{} 不存在"));
|
throw new RuntimeException(StrUtil.format("{} 不存在"));
|
||||||
}
|
}
|
||||||
@@ -145,7 +152,7 @@ public class KnowledgeService {
|
|||||||
.precompileSql(),
|
.precompileSql(),
|
||||||
knowledge.getId()
|
knowledge.getId()
|
||||||
);
|
);
|
||||||
knowledgeGroupService.removeByKnowledgeId(knowledge.getId());
|
groupService.removeByKnowledgeId(knowledge.getId());
|
||||||
client.deleteCollectionAsync(String.valueOf(knowledge.getVectorSourceId())).get();
|
client.deleteCollectionAsync(String.valueOf(knowledge.getVectorSourceId())).get();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.lanyuanxiaoyao.service.ai.knowledge.service.node;
|
|||||||
|
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.lang.Assert;
|
||||||
|
import cn.hutool.core.util.ObjectUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.lanyuanxiaoyao.service.ai.knowledge.entity.EmbeddingContext;
|
import com.lanyuanxiaoyao.service.ai.knowledge.entity.EmbeddingContext;
|
||||||
import com.yomahub.liteflow.annotation.LiteflowComponent;
|
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.core.NodeComponent;
|
||||||
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
|
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
|
||||||
import com.yomahub.liteflow.enums.NodeTypeEnum;
|
import com.yomahub.liteflow.enums.NodeTypeEnum;
|
||||||
|
import io.qdrant.client.QdrantClient;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
import org.springframework.ai.document.Document;
|
import org.springframework.ai.document.Document;
|
||||||
import org.springframework.ai.document.DocumentReader;
|
import org.springframework.ai.document.DocumentReader;
|
||||||
|
import org.springframework.ai.embedding.EmbeddingModel;
|
||||||
import org.springframework.ai.reader.ExtractedTextFormatter;
|
import org.springframework.ai.reader.ExtractedTextFormatter;
|
||||||
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
|
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
|
||||||
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
|
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
|
||||||
import org.springframework.ai.reader.tika.TikaDocumentReader;
|
import org.springframework.ai.reader.tika.TikaDocumentReader;
|
||||||
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
|
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;
|
import org.springframework.core.io.PathResource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,9 +42,13 @@ public class EmbeddingNodes {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(EmbeddingNodes.class);
|
private static final Logger logger = LoggerFactory.getLogger(EmbeddingNodes.class);
|
||||||
|
|
||||||
private final ChatClient chatClient;
|
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.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)
|
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = "embedding_check_if_file_needed", nodeName = "判断是否需要读取文件", nodeType = NodeTypeEnum.BOOLEAN)
|
||||||
@@ -52,16 +63,10 @@ public class EmbeddingNodes {
|
|||||||
return false;
|
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)
|
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_SWITCH, nodeId = "file_reader_switch", nodeName = "判断文件格式", nodeType = NodeTypeEnum.SWITCH)
|
||||||
public String fileReaderSwitch(NodeComponent node) {
|
public String fileReaderSwitch(NodeComponent node) {
|
||||||
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
|
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
|
||||||
String extName = FileUtil.extName(context.getFile());
|
String extName = FileUtil.extName(context.getFileFormat());
|
||||||
return switch (extName.toLowerCase()) {
|
return switch (extName.toLowerCase()) {
|
||||||
case "txt", "md", "markdown" -> "txt_file_reader";
|
case "txt", "md", "markdown" -> "txt_file_reader";
|
||||||
case "pdf" -> "pdf_file_reader";
|
case "pdf" -> "pdf_file_reader";
|
||||||
@@ -132,16 +137,11 @@ public class EmbeddingNodes {
|
|||||||
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
|
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
|
||||||
context.getDocuments().addAll(llmSplit(
|
context.getDocuments().addAll(llmSplit(
|
||||||
"""
|
"""
|
||||||
对用户输入的文本,生成高质量的分段。请遵循以下指南:
|
请你将用户输入的文本进行语义切分,生成用于知识库检索的文本段。
|
||||||
1. 分段原则:
|
每个文本段要尽可能多地覆盖用户输入文本的各方面知识和细节,包括但不限于主题、概念、关键信息等。对于关键的数字、理论、细节等,要严格遵循原文,不能进行任何虚构和捏造不存在的知识,确保输出内容准确、真实且全面。
|
||||||
分段按文本内容的语义进行分割,每个分段都尽可能保持完整连续的内容表达。
|
输出格式为纯文本段,分段之间使用“---”作为分割,方便后续使用代码进行切分。
|
||||||
避免从词句的中间进行分割。
|
输出文本避免添加markdown格式,保持文本格式紧凑。
|
||||||
2. 格式:
|
切分过程中,要注重保持文本的完整性和逻辑性,确保每个文本段都能独立地表达出清晰、准确的信息,以便更好地进行知识库检索。
|
||||||
分段之间用两个空行分隔,以提高可读性。
|
|
||||||
避免使用任何Markdown格式
|
|
||||||
3. 内容要求:
|
|
||||||
确保每个分段的内容文字完全依照原文。
|
|
||||||
避免添加任何原文中不存在的文字。
|
|
||||||
""",
|
""",
|
||||||
context.getContent(),
|
context.getContent(),
|
||||||
context.getMetadata()
|
context.getMetadata()
|
||||||
@@ -153,7 +153,7 @@ public class EmbeddingNodes {
|
|||||||
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
|
EmbeddingContext context = node.getContextBean(EmbeddingContext.class);
|
||||||
context.getDocuments().addAll(llmSplit(
|
context.getDocuments().addAll(llmSplit(
|
||||||
"""
|
"""
|
||||||
对用户输入的文本,生成一组高质量的问答对。请遵循以下指南:
|
对用户输入的文本,生成多组高质量的问答对。请遵循以下指南:
|
||||||
1. 问题部分:
|
1. 问题部分:
|
||||||
为同一个主题创建尽可能多的不同表述的问题,确保问题的多样性。
|
为同一个主题创建尽可能多的不同表述的问题,确保问题的多样性。
|
||||||
每个问题应考虑用户可能的多种问法,例如:
|
每个问题应考虑用户可能的多种问法,例如:
|
||||||
@@ -168,14 +168,20 @@ public class EmbeddingNodes {
|
|||||||
答案应直接基于给定文本,确保准确性和一致性。
|
答案应直接基于给定文本,确保准确性和一致性。
|
||||||
包含相关的细节,如日期、名称、职位等具体信息,必要时提供背景信息以增强理解。
|
包含相关的细节,如日期、名称、职位等具体信息,必要时提供背景信息以增强理解。
|
||||||
3. 格式:
|
3. 格式:
|
||||||
使用"Q:"标记问题集合的开始,所有问题应在一个段落内,问题之间用空格分隔。
|
使用"问:"标记问题集合的开始,所有问题应在一个段落内,问题之间用空格分隔。
|
||||||
使用"A:"标记答案的开始,答案应清晰分段,便于阅读。
|
使用"答:"标记答案的开始,答案应清晰分段,便于阅读。
|
||||||
问答对之间用两个空行分隔,以提高可读性。
|
问答对之间用“---”分隔,以提高可读性。
|
||||||
避免使用任何Markdown格式
|
|
||||||
4. 内容要求:
|
4. 内容要求:
|
||||||
确保问答对紧密围绕文本主题,避免偏离主题。
|
确保问答对紧密围绕文本主题,避免偏离主题。
|
||||||
避免添加文本中未提及的信息,确保信息的真实性。
|
避免添加文本中未提及的信息,确保信息的真实性。
|
||||||
|
一个问题搭配一个答案,避免一组问答对中同时涉及多个问题。
|
||||||
如果文本信息不足以回答某个方面,可以在答案中说明 "根据给定信息无法确定",并尽量提供相关的上下文。
|
如果文本信息不足以回答某个方面,可以在答案中说明 "根据给定信息无法确定",并尽量提供相关的上下文。
|
||||||
|
格式样例:
|
||||||
|
问:苹果通常是什么颜色的?
|
||||||
|
答:红色。
|
||||||
|
---
|
||||||
|
问:苹果长在树上还是地上?
|
||||||
|
答:苹果长在树上。
|
||||||
""",
|
""",
|
||||||
context.getContent(),
|
context.getContent(),
|
||||||
context.getMetadata()
|
context.getMetadata()
|
||||||
@@ -189,13 +195,33 @@ public class EmbeddingNodes {
|
|||||||
.call()
|
.call()
|
||||||
.content();
|
.content();
|
||||||
Assert.notBlank(response, "LLM response is empty");
|
Assert.notBlank(response, "LLM response is empty");
|
||||||
|
logger.info("{}", response);
|
||||||
// noinspection DataFlowIssue
|
// 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(StrUtil::trim)
|
||||||
.map(text -> Document.builder()
|
.map(text -> Document.builder()
|
||||||
.text(text)
|
.text(text)
|
||||||
.metadata(metadata)
|
.metadata(Optional.ofNullable(metadata).orElse(new HashMap<>()))
|
||||||
.build())
|
.build())
|
||||||
.toList();
|
.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ spring:
|
|||||||
model: 'Qwen3-1.7-vllm'
|
model: 'Qwen3-1.7-vllm'
|
||||||
embedding:
|
embedding:
|
||||||
options:
|
options:
|
||||||
model: 'Bge-m3'
|
model: 'Bge-m3-vllm'
|
||||||
vectorstore:
|
vectorstore:
|
||||||
qdrant:
|
qdrant:
|
||||||
api-key: lanyuanxiaoyao
|
api-key: lanyuanxiaoyao
|
||||||
@@ -49,3 +49,6 @@ liteflow:
|
|||||||
rule-source: config/flow.xml
|
rule-source: config/flow.xml
|
||||||
print-banner: false
|
print-banner: false
|
||||||
check-node-exists: false
|
check-node-exists: false
|
||||||
|
knowledge:
|
||||||
|
download-prefix: "http://localhost:8080"
|
||||||
|
upload-path: /Users/lanyuanxiaoyao/Project/IdeaProjects/hudi-service/service-ai/temp
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE flow PUBLIC "liteflow" "https://liteflow.cc/liteflow.dtd">
|
<!DOCTYPE flow PUBLIC "liteflow" "https://liteflow.cc/liteflow.dtd">
|
||||||
<flow>
|
<flow>
|
||||||
<chain id="embedding">
|
<chain id="embedding_preview">
|
||||||
SER(
|
SER(
|
||||||
IF(
|
IF(
|
||||||
embedding_check_if_file_needed,
|
embedding_check_if_file_needed,
|
||||||
@@ -17,4 +17,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
</chain>
|
</chain>
|
||||||
|
<chain id="embedding_submit">
|
||||||
|
SER(embedding_preview, import_vector_source)
|
||||||
|
</chain>
|
||||||
</flow>
|
</flow>
|
||||||
@@ -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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 Lake),Hudi 在实时更新和增量处理方面表现突出,尤其适合需要频繁数据变更的场景。其独特的 MoR 模型在写入性能上优于传统批处理方案。
|
||||||
|
|
||||||
|
总结
|
||||||
|
|
||||||
|
Apache Hudi 通过灵活的存储模型、高效的事务管理和广泛的生态系统集成,成为构建现代化数据湖的核心工具,适用于金融、物联网、实时分析等对数据新鲜度和操作效率要求高的领域。
|
||||||
|
""")
|
||||||
|
.call()
|
||||||
|
.content();
|
||||||
|
log.info(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +1,85 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {useParams} from 'react-router'
|
import {useNavigate, useParams} from 'react-router'
|
||||||
import {amisRender, crudCommonOptions} from '../../../util/amis.tsx'
|
import {amisRender, crudCommonOptions, mappingField, mappingItem} from '../../../util/amis.tsx'
|
||||||
|
|
||||||
|
const statusMapping = [
|
||||||
|
mappingItem('解析中', 'RUNNING', 'bg-warning'),
|
||||||
|
mappingItem('使用中', 'FINISHED', 'bg-success'),
|
||||||
|
]
|
||||||
|
|
||||||
const DataDetail: React.FC = () => {
|
const DataDetail: React.FC = () => {
|
||||||
const {name} = useParams()
|
const {knowledge_id} = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
return (
|
return (
|
||||||
<div className="import-detail h-full">
|
<div className="import-detail h-full">
|
||||||
{amisRender(
|
{amisRender(
|
||||||
{
|
{
|
||||||
className: 'h-full',
|
className: 'h-full',
|
||||||
type: 'page',
|
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',
|
size: 'lg',
|
||||||
actions: [],
|
actions: [],
|
||||||
body: [
|
body: [
|
||||||
{
|
{
|
||||||
type: 'crud',
|
type: 'crud',
|
||||||
api: {
|
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: {
|
headers: {
|
||||||
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
knowledge_id,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...crudCommonOptions(),
|
...crudCommonOptions(),
|
||||||
headerToolbar: [
|
headerToolbar: [
|
||||||
'reload',
|
'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: [
|
columns: [
|
||||||
{
|
{
|
||||||
@@ -32,8 +87,15 @@ const DataDetail: React.FC = () => {
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'text',
|
name: 'name',
|
||||||
label: '内容',
|
label: '文件名',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: '状态',
|
||||||
|
width: 50,
|
||||||
|
align: 'center',
|
||||||
|
...mappingField('status', statusMapping),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'operation',
|
type: 'operation',
|
||||||
@@ -42,30 +104,17 @@ const DataDetail: React.FC = () => {
|
|||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
type: 'action',
|
type: 'action',
|
||||||
label: '编辑',
|
label: '查看',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
size: 'lg',
|
size: 'sm',
|
||||||
actionType: 'dialog',
|
onEvent: {
|
||||||
dialog: {
|
click: {
|
||||||
title: '编辑文段',
|
actions: [
|
||||||
size: 'md',
|
|
||||||
body: {
|
|
||||||
type: 'form',
|
|
||||||
body: [
|
|
||||||
{
|
{
|
||||||
type: 'input-text',
|
actionType: 'custom',
|
||||||
name: 'id',
|
// @ts-ignore
|
||||||
disabled: true,
|
script: (context, action, event) => {
|
||||||
label: '文段ID',
|
navigate(`/ai/knowledge/detail/${knowledge_id}/segment/${context.props.data['id']}`)
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'editor',
|
|
||||||
label: '内容',
|
|
||||||
name: 'text',
|
|
||||||
language: 'plaintext',
|
|
||||||
options: {
|
|
||||||
lineNumbers: 'off',
|
|
||||||
wordWrap: 'bounded',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -77,13 +126,17 @@ const DataDetail: React.FC = () => {
|
|||||||
label: '删除',
|
label: '删除',
|
||||||
className: 'text-danger hover:text-red-600',
|
className: 'text-danger hover:text-red-600',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
size: 'xs',
|
size: 'sm',
|
||||||
actionType: 'ajax',
|
actionType: 'ajax',
|
||||||
api: {
|
api: {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
url: 'http://127.0.0.1:8080/group/delete',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
id: '${id}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
confirmText: '确认删除',
|
confirmText: '确认删除',
|
||||||
confirmTitle: '删除',
|
confirmTitle: '删除',
|
||||||
|
|||||||
@@ -10,12 +10,37 @@ const ImportDataDiv = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const DataImport: React.FC = () => {
|
const DataImport: React.FC = () => {
|
||||||
const {name} = useParams()
|
const {knowledge_id} = useParams()
|
||||||
return (
|
return (
|
||||||
<ImportDataDiv className="import-data h-full">
|
<ImportDataDiv className="import-data h-full">
|
||||||
{amisRender({
|
{amisRender({
|
||||||
type: 'page',
|
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: [
|
body: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -53,7 +78,7 @@ const DataImport: React.FC = () => {
|
|||||||
name: 'type',
|
name: 'type',
|
||||||
type: 'radios',
|
type: 'radios',
|
||||||
label: '数据形式',
|
label: '数据形式',
|
||||||
value: 'text',
|
value: 'file',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
value: 'text',
|
value: 'text',
|
||||||
@@ -62,7 +87,6 @@ const DataImport: React.FC = () => {
|
|||||||
{
|
{
|
||||||
value: 'file',
|
value: 'file',
|
||||||
label: '文件',
|
label: '文件',
|
||||||
disabled: true,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -82,10 +106,38 @@ const DataImport: React.FC = () => {
|
|||||||
type: 'input-file',
|
type: 'input-file',
|
||||||
name: 'files',
|
name: 'files',
|
||||||
label: '数据文件',
|
label: '数据文件',
|
||||||
accept: '.txt,.csv',
|
|
||||||
autoUpload: false,
|
autoUpload: false,
|
||||||
drag: true,
|
drag: true,
|
||||||
multiple: 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',
|
className: 'text-right',
|
||||||
@@ -103,16 +155,10 @@ const DataImport: React.FC = () => {
|
|||||||
},
|
},
|
||||||
dataType: 'form',
|
dataType: 'form',
|
||||||
data: {
|
data: {
|
||||||
mode: '${mode}',
|
mode: '${mode|default:undefined}',
|
||||||
type: '${type}',
|
type: '${type|default:undefined}',
|
||||||
content: '${content}',
|
content: '${content|default:undefined}',
|
||||||
},
|
files: '${files|default:undefined}',
|
||||||
// @ts-ignore
|
|
||||||
adaptor: (payload, response, api, context) => {
|
|
||||||
console.log(payload)
|
|
||||||
return {
|
|
||||||
items: payload,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reload: 'preview_list?rows=${items}',
|
reload: 'preview_list?rows=${items}',
|
||||||
@@ -121,6 +167,22 @@ const DataImport: React.FC = () => {
|
|||||||
type: 'submit',
|
type: 'submit',
|
||||||
label: '提交',
|
label: '提交',
|
||||||
level: 'primary',
|
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: {
|
listItem: {
|
||||||
body: {
|
body: {
|
||||||
|
className: 'white-space-pre-line',
|
||||||
type: 'tpl',
|
type: 'tpl',
|
||||||
tpl: '${text}',
|
tpl: '${text}',
|
||||||
},
|
},
|
||||||
|
|||||||
133
service-web/client/src/pages/ai/knowledge/DataSegment.tsx
Normal file
133
service-web/client/src/pages/ai/knowledge/DataSegment.tsx
Normal 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
|
||||||
@@ -38,6 +38,8 @@ const Knowledge: React.FC = () => {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
label: '',
|
label: '',
|
||||||
icon: 'fa fa-plus',
|
icon: 'fa fa-plus',
|
||||||
|
tooltip: '新增',
|
||||||
|
tooltipPlacement: 'top',
|
||||||
actionType: 'dialog',
|
actionType: 'dialog',
|
||||||
dialog: {
|
dialog: {
|
||||||
title: '新增知识库',
|
title: '新增知识库',
|
||||||
@@ -111,7 +113,7 @@ const Knowledge: React.FC = () => {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
label: '详情',
|
label: '详情',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
size: 'xs',
|
size: 'sm',
|
||||||
onEvent: {
|
onEvent: {
|
||||||
click: {
|
click: {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -119,7 +121,7 @@ const Knowledge: React.FC = () => {
|
|||||||
actionType: 'custom',
|
actionType: 'custom',
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
script: (context, action, event) => {
|
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',
|
type: 'action',
|
||||||
label: '导入',
|
label: '导入',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
size: 'xs',
|
size: 'sm',
|
||||||
onEvent: {
|
onEvent: {
|
||||||
click: {
|
click: {
|
||||||
actions: [
|
actions: [
|
||||||
@@ -138,7 +140,7 @@ const Knowledge: React.FC = () => {
|
|||||||
actionType: 'custom',
|
actionType: 'custom',
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
script: (context, action, event) => {
|
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: '删除',
|
label: '删除',
|
||||||
className: 'text-danger hover:text-red-600',
|
className: 'text-danger hover:text-red-600',
|
||||||
level: 'link',
|
level: 'link',
|
||||||
size: 'xs',
|
size: 'sm',
|
||||||
actionType: 'ajax',
|
actionType: 'ajax',
|
||||||
api: {
|
api: {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: 'http://127.0.0.1:8080/knowledge/delete?name=${name}',
|
url: 'http://127.0.0.1:8080/knowledge/delete',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
id: '${id}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
confirmText: '确认删除',
|
confirmText: '确认删除',
|
||||||
confirmTitle: '删除',
|
confirmTitle: '删除',
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import Conversation from './pages/ai/Conversation.tsx'
|
|||||||
import Inspection from './pages/ai/Inspection.tsx'
|
import Inspection from './pages/ai/Inspection.tsx'
|
||||||
import DataDetail from './pages/ai/knowledge/DataDetail.tsx'
|
import DataDetail from './pages/ai/knowledge/DataDetail.tsx'
|
||||||
import DataImport from './pages/ai/knowledge/DataImport.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 Knowledge from './pages/ai/knowledge/Knowledge.tsx'
|
||||||
import App from './pages/App.tsx'
|
import App from './pages/App.tsx'
|
||||||
import Cloud from './pages/overview/Cloud.tsx'
|
import Cloud from './pages/overview/Cloud.tsx'
|
||||||
@@ -95,13 +96,17 @@ export const routes: RouteObject[] = [
|
|||||||
Component: Knowledge,
|
Component: Knowledge,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'knowledge/import/:name',
|
path: 'knowledge/import/:knowledge_id',
|
||||||
Component: DataImport,
|
Component: DataImport,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'knowledge/detail/:name',
|
path: 'knowledge/detail/:knowledge_id',
|
||||||
Component: DataDetail,
|
Component: DataDetail,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'knowledge/detail/:knowledge_id/segment/:group_id',
|
||||||
|
Component: DataSegment,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export const amisRender = (schema: Schema, data: Record<any, any> = {}) => {
|
|||||||
theme: theme,
|
theme: theme,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
enableAMISDebug: true,
|
||||||
fetcher: async (api: any) => {
|
fetcher: async (api: any) => {
|
||||||
let {url, method, data, responseType, config, headers} = api
|
let {url, method, data, responseType, config, headers} = api
|
||||||
config = config || {}
|
config = config || {}
|
||||||
|
|||||||
Reference in New Issue
Block a user