mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2025-01-18 19:20:05 +08:00
commit
1596ca3309
@ -34,6 +34,11 @@ public enum AiChatRoleEnum {
|
||||
### 支付宝
|
||||
### 微信
|
||||
除此之外不要任何解释性语句。
|
||||
"""),
|
||||
|
||||
AI_KNOWLEDGE_ROLE("知识库助手", """
|
||||
给你提供一些数据参考:{info},请回答我的问题。
|
||||
请你跟进数据参考与工具返回结果回复用户的请求。
|
||||
""");
|
||||
|
||||
/**
|
||||
|
@ -10,4 +10,7 @@ public class AiChatConversationCreateMyReqVO {
|
||||
@Schema(description = "聊天角色编号", example = "666")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "知识库编号", example = "1204")
|
||||
private Long knowledgeId;
|
||||
|
||||
}
|
||||
|
@ -21,6 +21,9 @@ public class AiChatConversationUpdateMyReqVO {
|
||||
@Schema(description = "模型编号", example = "1")
|
||||
private Long modelId;
|
||||
|
||||
@Schema(description = "知识库编号", example = "1")
|
||||
private Long knowledgeId;
|
||||
|
||||
@Schema(description = "角色设定", example = "一个快乐的程序员")
|
||||
private String systemMessage;
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
|
||||
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -28,24 +28,23 @@ public class AiKnowledgeController {
|
||||
@Resource
|
||||
private AiKnowledgeService knowledgeService;
|
||||
|
||||
@GetMapping("/my-page")
|
||||
@Operation(summary = "获取【我的】知识库分页")
|
||||
public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePageMy(@Validated PageParam pageReqVO) {
|
||||
PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePageMy(getLoginUserId(), pageReqVO);
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取知识库分页")
|
||||
public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) {
|
||||
PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePage(getLoginUserId(), pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class));
|
||||
}
|
||||
|
||||
@PostMapping("/create-my")
|
||||
@Operation(summary = "创建【我的】知识库")
|
||||
public CommonResult<Long> createKnowledgeMy(@RequestBody @Valid AiKnowledgeCreateMyReqVO createReqVO) {
|
||||
return success(knowledgeService.createKnowledgeMy(createReqVO, getLoginUserId()));
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建知识库")
|
||||
public CommonResult<Long> createKnowledge(@RequestBody @Valid AiKnowledgeCreateReqVO createReqVO) {
|
||||
return success(knowledgeService.createKnowledge(createReqVO, getLoginUserId()));
|
||||
}
|
||||
|
||||
@PutMapping("/update-my")
|
||||
@Operation(summary = "更新【我的】知识库")
|
||||
public CommonResult<Boolean> updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) {
|
||||
knowledgeService.updateKnowledgeMy(updateReqVO, getLoginUserId());
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新知识库")
|
||||
public CommonResult<Boolean> updateKnowledge(@RequestBody @Valid AiKnowledgeUpdateReqVO updateReqVO) {
|
||||
knowledgeService.updateKnowledge(updateReqVO, getLoginUserId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ public class AiKnowledgeDocumentController {
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取文档分页")
|
||||
public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPageMy(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
|
||||
public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPage(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
|
||||
PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getKnowledgeDocumentPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class));
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public class AiKnowledgeSegmentController {
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取段落分页")
|
||||
public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPageMy(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
|
||||
public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPage(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
|
||||
PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO);
|
||||
return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class));
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO")
|
||||
@Schema(description = "管理后台 - AI 知识库创建 Request VO")
|
||||
@Data
|
||||
public class AiKnowledgeCreateMyReqVO {
|
||||
public class AiKnowledgeCreateReqVO {
|
||||
|
||||
@Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南")
|
||||
@NotBlank(message = "知识库名称不能为空")
|
||||
@ -18,7 +18,7 @@ public class AiKnowledgeCreateMyReqVO {
|
||||
@Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
|
||||
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]")
|
||||
private List<Long> visibilityPermissions;
|
||||
|
||||
@Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
@ -23,21 +23,21 @@ public class AiKnowledgeDocumentCreateReqVO {
|
||||
@URL(message = "文档 URL 格式不正确")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "每个文本块的目标 token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800")
|
||||
@NotNull(message = "每个文本块的目标 token 数不能为空")
|
||||
private Integer defaultChunkSize;
|
||||
@Schema(description = "每个段落的目标 token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800")
|
||||
@NotNull(message = "每个段落的目标 token 数不能为空")
|
||||
private Integer defaultSegmentTokens;
|
||||
|
||||
@Schema(description = "每个文本块的最小字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "350")
|
||||
@NotNull(message = "每个文本块的最小字符数不能为空")
|
||||
private Integer minChunkSizeChars;
|
||||
@Schema(description = "每个段落的最小字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "350")
|
||||
@NotNull(message = "每个段落的最小字符数不能为空")
|
||||
private Integer minSegmentWordCount;
|
||||
|
||||
@Schema(description = "丢弃阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||
@Schema(description = "丢弃阈值:低于此阈值的段落会被丢弃", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||
@NotNull(message = "丢弃阈值不能为空")
|
||||
private Integer minChunkLengthToEmbed;
|
||||
|
||||
@Schema(description = "最大块数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
|
||||
@NotNull(message = "最大块数不能为空")
|
||||
private Integer maxNumChunks;
|
||||
@Schema(description = "最大段落数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
|
||||
@NotNull(message = "最大段落数不能为空")
|
||||
private Integer maxNumSegments;
|
||||
|
||||
@Schema(description = "分块是否保留分隔符", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@NotNull(message = "分块是否保留分隔符不能为空")
|
||||
|
@ -0,0 +1,14 @@
|
||||
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - AI 知识库的分页 Request VO")
|
||||
@Data
|
||||
public class AiKnowledgePageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "知识库名称", example = "Java 开发手册")
|
||||
private String name;
|
||||
|
||||
}
|
@ -9,7 +9,7 @@ import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO")
|
||||
@Data
|
||||
public class AiKnowledgeUpdateMyReqVO {
|
||||
public class AiKnowledgeUpdateReqVO {
|
||||
|
||||
@Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204")
|
||||
@NotNull(message = "知识库编号不能为空")
|
||||
@ -22,7 +22,7 @@ public class AiKnowledgeUpdateMyReqVO {
|
||||
@Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]")
|
||||
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
|
||||
private List<Long> visibilityPermissions;
|
||||
|
||||
@Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
@ -64,6 +65,13 @@ public class AiChatConversationDO extends BaseDO {
|
||||
*/
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 知识库编号
|
||||
* <p>
|
||||
* 关联 {@link AiKnowledgeDO#getId()}
|
||||
*/
|
||||
private Long knowledgeId;
|
||||
|
||||
/**
|
||||
* 模型编号
|
||||
*
|
||||
|
@ -1,13 +1,18 @@
|
||||
package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import org.springframework.ai.chat.messages.MessageType;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.*;
|
||||
import org.springframework.ai.chat.messages.MessageType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI Chat 消息 DO
|
||||
@ -66,6 +71,15 @@ public class AiChatMessageDO extends BaseDO {
|
||||
*/
|
||||
private Long roleId;
|
||||
|
||||
|
||||
/**
|
||||
* 段落编号数组
|
||||
*
|
||||
* 关联 {@link AiKnowledgeSegmentDO#getId()} 字段
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<Long> segmentIds;
|
||||
|
||||
/**
|
||||
* 模型标志
|
||||
*/
|
||||
|
@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
@ -38,11 +38,13 @@ public class AiKnowledgeDO extends BaseDO {
|
||||
* 知识库描述
|
||||
*/
|
||||
private String description;
|
||||
// TODO @新:如果全部可见,需要怎么设置?
|
||||
|
||||
/**
|
||||
* 可见权限,只能选择哪些人可见
|
||||
* 可见权限,选择哪些人可见
|
||||
* <p>
|
||||
* -1 所有人可见,其他为各自用户编号
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@TableField(typeHandler = LongListTypeHandler.class)
|
||||
private List<Long> visibilityPermissions;
|
||||
/**
|
||||
* 嵌入模型编号
|
||||
|
@ -40,23 +40,25 @@ public class AiKnowledgeDocumentDO extends BaseDO {
|
||||
*/
|
||||
private String url;
|
||||
/**
|
||||
* token 数量
|
||||
* 文档 token 数量
|
||||
*/
|
||||
private Integer tokens;
|
||||
/**
|
||||
* 字符数
|
||||
* 文档字符数
|
||||
*/
|
||||
private Integer wordCount;
|
||||
// TODO @新:chunk 1)是不是 segment,这样命名保持一致会好点哈?2)Size 是不是改成 Tokens 会统一点;3)defaultChunkSize、defaultChunkSize、minChunkSizeChars、maxNumChunks 这几个字段的命名,可能要微信一起讨论下。尽量命名保持风格统一哈。
|
||||
|
||||
|
||||
// ========== 自定义分段所用参数 ==========
|
||||
// TODO @新:3)defaultChunkSize、defaultChunkSize、minChunkSizeChars、maxNumChunks 这几个字段的命名,可能要微信一起讨论下。尽量命名保持风格统一哈。
|
||||
/**
|
||||
* 每个文本块的目标 token 数
|
||||
*/
|
||||
private Integer defaultChunkSize;
|
||||
// TODO @xin:SizeChars 和 wordCount 好像是一个意思,是不是也要统一哈。
|
||||
private Integer defaultSegmentTokens;
|
||||
/**
|
||||
* 每个文本块的最小字符数
|
||||
*/
|
||||
private Integer minChunkSizeChars;
|
||||
private Integer minSegmentWordCount;
|
||||
/**
|
||||
* 低于此值的块会被丢弃
|
||||
*/
|
||||
@ -64,11 +66,13 @@ public class AiKnowledgeDocumentDO extends BaseDO {
|
||||
/**
|
||||
* 最大块数
|
||||
*/
|
||||
private Integer maxNumChunks;
|
||||
private Integer maxNumSegments;
|
||||
/**
|
||||
* 分块是否保留分隔符
|
||||
*/
|
||||
private Boolean keepSeparator;
|
||||
// ===================================
|
||||
|
||||
/**
|
||||
* 切片状态
|
||||
* <p>
|
||||
|
@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
@ -27,7 +25,6 @@ public class AiKnowledgeSegmentDO extends BaseDO {
|
||||
/**
|
||||
* 向量库的编号
|
||||
*/
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS) // TODO @新:尽量规避要这个注解。万一后面加个 status 单独更新,可能会踩坑。
|
||||
private String vectorId;
|
||||
/**
|
||||
* 知识库编号
|
||||
|
@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@ -16,10 +16,11 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper
|
||||
public interface AiKnowledgeMapper extends BaseMapperX<AiKnowledgeDO> {
|
||||
|
||||
default PageResult<AiKnowledgeDO> selectPageByMy(Long userId, PageParam pageReqVO) {
|
||||
default PageResult<AiKnowledgeDO> selectPage(Long userId, AiKnowledgePageReqVO pageReqVO) {
|
||||
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiKnowledgeDO>()
|
||||
.eq(AiKnowledgeDO::getUserId, userId)
|
||||
.eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
|
||||
.likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName())
|
||||
.and(e -> e.apply("FIND_IN_SET(" + userId + ",visibility_permissions)").or(m -> m.apply("FIND_IN_SET(-1,visibility_permissions)")))
|
||||
.orderByDesc(AiKnowledgeDO::getId));
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,7 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegment
|
||||
.orderByDesc(AiKnowledgeSegmentDO::getId));
|
||||
}
|
||||
|
||||
// TODO @新:selectListByXXX 哈
|
||||
default List<AiKnowledgeSegmentDO> selectList(List<String> vectorIdList) {
|
||||
default List<AiKnowledgeSegmentDO> selectListByVectorIds(List<String> vectorIdList) {
|
||||
return selectList(new LambdaQueryWrapperX<AiKnowledgeSegmentDO>()
|
||||
.in(AiKnowledgeSegmentDO::getVectorId, vectorIdList)
|
||||
.orderByDesc(AiKnowledgeSegmentDO::getId));
|
||||
|
@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper;
|
||||
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
|
||||
import jakarta.annotation.Resource;
|
||||
@ -22,6 +23,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
@ -45,6 +47,8 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
|
||||
private AiChatModelService chatModalService;
|
||||
@Resource
|
||||
private AiChatRoleService chatRoleService;
|
||||
@Resource
|
||||
private AiKnowledgeService knowledgeService;
|
||||
|
||||
@Override
|
||||
public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) {
|
||||
@ -56,9 +60,14 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
|
||||
Assert.notNull(model, "必须找到默认模型");
|
||||
validateChatModel(model);
|
||||
|
||||
// 1.3 校验知识库
|
||||
if (Objects.nonNull(createReqVO.getKnowledgeId())) {
|
||||
knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId());
|
||||
}
|
||||
|
||||
// 2. 创建 AiChatConversationDO 聊天对话
|
||||
AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false)
|
||||
.setModelId(model.getId()).setModel(model.getModel())
|
||||
.setModelId(model.getId()).setModel(model.getModel()).setKnowledgeId(createReqVO.getKnowledgeId())
|
||||
.setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts());
|
||||
if (role != null) {
|
||||
conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage());
|
||||
@ -82,6 +91,11 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
|
||||
model = chatModalService.validateChatModel(updateReqVO.getModelId());
|
||||
}
|
||||
|
||||
// 1.3 校验知识库是否存在
|
||||
if (updateReqVO.getKnowledgeId() != null) {
|
||||
knowledgeService.validateKnowledgeExists(updateReqVO.getKnowledgeId());
|
||||
}
|
||||
|
||||
// 2. 更新对话信息
|
||||
AiChatConversationDO updateObj = BeanUtils.toBean(updateReqVO, AiChatConversationDO.class);
|
||||
if (Boolean.TRUE.equals(updateReqVO.getPinned())) {
|
||||
|
@ -12,21 +12,29 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
|
||||
import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum;
|
||||
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.messages.*;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.MessageType;
|
||||
import org.springframework.ai.chat.messages.SystemMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.model.StreamingChatModel;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import reactor.core.publisher.Flux;
|
||||
@ -59,6 +67,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
private AiChatModelService chatModalService;
|
||||
@Resource
|
||||
private AiApiKeyService apiKeyService;
|
||||
@Resource
|
||||
private AiKnowledgeSegmentService knowledgeSegmentService;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
|
||||
@ -80,13 +90,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
|
||||
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
|
||||
|
||||
// 3.2 创建 chat 需要的 Prompt
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
|
||||
// 3.2 召回段落
|
||||
List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
|
||||
|
||||
// 3.3 创建 chat 需要的 Prompt
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
|
||||
ChatResponse chatResponse = chatModel.call(prompt);
|
||||
|
||||
// 3.3 段式返回
|
||||
// 3.4 段式返回
|
||||
String newContent = chatResponse.getResult().getOutput().getContent();
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)).setContent(newContent));
|
||||
return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
|
||||
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent));
|
||||
}
|
||||
@ -111,11 +124,15 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
|
||||
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
|
||||
|
||||
// 3.2 构建 Prompt,并进行调用
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
|
||||
|
||||
// 3.2 召回段落
|
||||
List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
|
||||
|
||||
// 3.3 构建 Prompt,并进行调用
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
|
||||
Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
|
||||
|
||||
// 3.3 流式返回
|
||||
// 3.4 流式返回
|
||||
// TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
|
||||
StringBuffer contentBuffer = new StringBuffer();
|
||||
return streamResponse.map(chunk -> {
|
||||
@ -128,7 +145,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
}).doOnComplete(() -> {
|
||||
// 忽略租户,因为 Flux 异步无法透传租户
|
||||
TenantUtils.executeIgnore(() ->
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())));
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId))
|
||||
.setContent(contentBuffer.toString())));
|
||||
}).doOnError(throwable -> {
|
||||
log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
|
||||
// 忽略租户,因为 Flux 异步无法透传租户
|
||||
@ -137,18 +155,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
|
||||
}
|
||||
|
||||
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
|
||||
private List<AiKnowledgeSegmentDO> recallSegment(String content, Long knowledgeId) {
|
||||
if (Objects.isNull(knowledgeId)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content));
|
||||
}
|
||||
|
||||
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,List<AiKnowledgeSegmentDO> segmentList,
|
||||
AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) {
|
||||
// 1. 构建 Prompt Message 列表
|
||||
List<Message> chatMessages = new ArrayList<>();
|
||||
// 1.1 system context 角色设定
|
||||
|
||||
// 1.1 召回内容消息构建
|
||||
if (CollUtil.isNotEmpty(segmentList)) {
|
||||
PromptTemplate promptTemplate = new PromptTemplate(AiChatRoleEnum.AI_KNOWLEDGE_ROLE.getSystemMessage());
|
||||
StringBuilder infoBuilder = StrUtil.builder();
|
||||
segmentList.forEach(segment -> infoBuilder.append(System.lineSeparator()).append(segment.getContent()));
|
||||
Message message = promptTemplate.createMessage(Map.of("info", infoBuilder.toString()));
|
||||
chatMessages.add(message);
|
||||
}
|
||||
|
||||
// 1.2 system context 角色设定
|
||||
if (StrUtil.isNotBlank(conversation.getSystemMessage())) {
|
||||
chatMessages.add(new SystemMessage(conversation.getSystemMessage()));
|
||||
}
|
||||
// 1.2 history message 历史消息
|
||||
// 1.3 history message 历史消息
|
||||
List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
|
||||
contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
|
||||
// 1.3 user message 新发送消息
|
||||
// 1.4 user message 新发送消息
|
||||
chatMessages.add(new UserMessage(sendReqVO.getContent()));
|
||||
|
||||
// 2. 构建 ChatOptions 对象
|
||||
@ -160,7 +195,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
|
||||
/**
|
||||
* 从历史消息中,获得倒序的 n 组消息作为消息上下文
|
||||
*
|
||||
* <p>
|
||||
* n 组:指的是 user + assistant 形成一组
|
||||
*
|
||||
* @param messages 消息列表
|
||||
|
@ -71,8 +71,8 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
|
||||
}
|
||||
|
||||
// 2 构造文本分段器
|
||||
TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(createReqVO.getDefaultChunkSize(), createReqVO.getMinChunkSizeChars(), createReqVO.getMinChunkLengthToEmbed(),
|
||||
createReqVO.getMaxNumChunks(), createReqVO.getKeepSeparator());
|
||||
TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(createReqVO.getDefaultSegmentTokens(), createReqVO.getMinSegmentWordCount(), createReqVO.getMinChunkLengthToEmbed(),
|
||||
createReqVO.getMaxNumSegments(), createReqVO.getKeepSeparator());
|
||||
// 2.1 文档分段
|
||||
List<Document> segments = tokenTextSplitter.apply(documents);
|
||||
// 2.2 分段内容入库
|
||||
|
@ -90,7 +90,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
|
||||
} else {
|
||||
// 2.2 禁用删除向量
|
||||
vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId()));
|
||||
knowledgeSegment.setVectorId(null);
|
||||
knowledgeSegment.setVectorId("");
|
||||
}
|
||||
// 3 更新段落状态
|
||||
segmentMapper.updateById(knowledgeSegment);
|
||||
@ -114,7 +114,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
|
||||
return ListUtil.empty();
|
||||
}
|
||||
// 3.2 段落召回
|
||||
return segmentMapper.selectList(CollUtil.getFieldValues(documentList, "id", String.class));
|
||||
return segmentMapper.selectListByVectorIds(CollUtil.getFieldValues(documentList, "id", String.class));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,9 +1,9 @@
|
||||
package cn.iocoder.yudao.module.ai.service.knowledge;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
|
||||
@ -15,21 +15,21 @@ import org.springframework.ai.vectorstore.VectorStore;
|
||||
public interface AiKnowledgeService {
|
||||
|
||||
/**
|
||||
* 创建【我的】知识库
|
||||
* 创建知识库
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @param userId 用户编号
|
||||
* @return 编号
|
||||
*/
|
||||
Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId);
|
||||
Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 创建【我的】知识库
|
||||
* 更新知识库
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
* @param userId 用户编号
|
||||
*/
|
||||
void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId);
|
||||
void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId);
|
||||
|
||||
/**
|
||||
* 校验知识库是否存在
|
||||
@ -39,21 +39,20 @@ public interface AiKnowledgeService {
|
||||
AiKnowledgeDO validateKnowledgeExists(Long id);
|
||||
|
||||
/**
|
||||
* 获得【我的】知识库分页
|
||||
* 获得知识库分页
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 知识库分页
|
||||
*/
|
||||
PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO);
|
||||
PageResult<AiKnowledgeDO> getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO);
|
||||
|
||||
// TODO @新:knowledgeId 和 validateKnowledgeExists 的 id 是同一个么?如果是的话,建议变量也用 id 哈,然后两边的 id 注释,保持一致
|
||||
/**
|
||||
* 根据知识库编号获取向量存储实例
|
||||
*
|
||||
* @param knowledgeId 知识库编号
|
||||
* @param id 知识库编号
|
||||
* @return 向量存储实例
|
||||
*/
|
||||
VectorStore getVectorStoreById(Long knowledgeId);
|
||||
VectorStore getVectorStoreById(Long id);
|
||||
|
||||
}
|
||||
|
@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.ai.service.knowledge;
|
||||
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
|
||||
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper;
|
||||
@ -29,21 +29,18 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_
|
||||
@Slf4j
|
||||
public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
|
||||
@Resource
|
||||
private AiChatModelService chatModalService;
|
||||
|
||||
@Resource
|
||||
private AiKnowledgeMapper knowledgeMapper;
|
||||
|
||||
@Resource
|
||||
private AiChatModelService chatModelService;
|
||||
@Resource
|
||||
private AiApiKeyService apiKeyService;
|
||||
// TODO @新:chatModelService 和 apiKeyService 可以放到 33 行的 chatModalService 后面。尽量保持,想通类型的变量在一块。例如说,Service 一块,Mapper 一块。
|
||||
|
||||
@Override
|
||||
public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) {
|
||||
public Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId) {
|
||||
// 1. 校验模型配置
|
||||
AiChatModelDO model = chatModalService.validateChatModel(createReqVO.getModelId());
|
||||
AiChatModelDO model = chatModelService.validateChatModel(createReqVO.getModelId());
|
||||
|
||||
// 2. 插入知识库
|
||||
AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class)
|
||||
@ -53,14 +50,14 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) {
|
||||
public void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId) {
|
||||
// 1.1 校验知识库存在
|
||||
AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId());
|
||||
if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) {
|
||||
throw exception(KNOWLEDGE_NOT_EXISTS);
|
||||
}
|
||||
// 1.2 校验模型配置
|
||||
AiChatModelDO model = chatModalService.validateChatModel(updateReqVO.getModelId());
|
||||
AiChatModelDO model = chatModelService.validateChatModel(updateReqVO.getModelId());
|
||||
|
||||
// 2. 更新知识库
|
||||
AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class);
|
||||
@ -78,13 +75,13 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO) {
|
||||
return knowledgeMapper.selectPageByMy(userId, pageReqVO);
|
||||
public PageResult<AiKnowledgeDO> getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO) {
|
||||
return knowledgeMapper.selectPage(userId, pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VectorStore getVectorStoreById(Long knowledgeId) {
|
||||
AiKnowledgeDO knowledge = validateKnowledgeExists(knowledgeId);
|
||||
public VectorStore getVectorStoreById(Long id) {
|
||||
AiKnowledgeDO knowledge = validateKnowledgeExists(id);
|
||||
AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
|
||||
// 创建或获取 VectorStore 对象
|
||||
return apiKeyService.getOrCreateVectorStore(model.getKeyId());
|
||||
|
@ -197,7 +197,6 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO @新:貌似可以创建一个大的 VectorStore。然后搜的时候,通过 Filter.Expression 过滤对应的数据。
|
||||
@Override
|
||||
public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) {
|
||||
String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url);
|
||||
|
Loading…
Reference in New Issue
Block a user