Commit a48287611358f7b885ca0e7b99e14ede39af6347

Authored by YunaiV
1 parent 3b02bcf4

【优化】AI:聊天消息的 Service 实现

Showing 12 changed files with 166 additions and 195 deletions
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/ErrorCodeConstants.java
@@ -30,8 +30,8 @@ public interface ErrorCodeConstants { @@ -30,8 +30,8 @@ public interface ErrorCodeConstants {
30 ErrorCode CHAT_CONVERSATION_UPDATE_MAX_TOKENS_ERROR = new ErrorCode(1_040_003_002, "更新对话失败,最大 Token 超过上限"); 30 ErrorCode CHAT_CONVERSATION_UPDATE_MAX_TOKENS_ERROR = new ErrorCode(1_040_003_002, "更新对话失败,最大 Token 超过上限");
31 ErrorCode CHAT_CONVERSATION_UPDATE_MAX_CONTEXTS_ERROR = new ErrorCode(1_040_003_002, "更新对话失败,最大 Context 超过上限"); 31 ErrorCode CHAT_CONVERSATION_UPDATE_MAX_CONTEXTS_ERROR = new ErrorCode(1_040_003_002, "更新对话失败,最大 Context 超过上限");
32 32
33 - // chat  
34 - ErrorCode AI_CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_022_000_100, "提问的 MessageId 不存在!"); 33 + // ========== API 聊天消息 1-040-004-000 ==========
  34 + ErrorCode AI_CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!");
35 35
36 // midjourney 36 // midjourney
37 37
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java
1 package cn.iocoder.yudao.module.ai.controller.admin.chat; 1 package cn.iocoder.yudao.module.ai.controller.admin.chat;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
3 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
  7 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.*; 8 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.*;
5 -import cn.iocoder.yudao.module.ai.service.AiChatService; 9 +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
  10 +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
  11 +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
  12 +import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
  13 +import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService;
  14 +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
  15 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  16 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
6 import io.swagger.v3.oas.annotations.Operation; 17 import io.swagger.v3.oas.annotations.Operation;
7 import io.swagger.v3.oas.annotations.Parameter; 18 import io.swagger.v3.oas.annotations.Parameter;
8 import io.swagger.v3.oas.annotations.tags.Tag; 19 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -14,9 +25,12 @@ import org.springframework.validation.annotation.Validated; @@ -14,9 +25,12 @@ import org.springframework.validation.annotation.Validated;
14 import org.springframework.web.bind.annotation.*; 25 import org.springframework.web.bind.annotation.*;
15 import reactor.core.publisher.Flux; 26 import reactor.core.publisher.Flux;
16 27
  28 +import java.util.Collections;
17 import java.util.List; 29 import java.util.List;
  30 +import java.util.Map;
18 31
19 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; 32 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  33 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
20 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; 34 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
21 35
22 @Tag(name = "管理后台 - 聊天消息") 36 @Tag(name = "管理后台 - 聊天消息")
@@ -26,39 +40,65 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti @@ -26,39 +40,65 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
26 public class AiChatMessageController { 40 public class AiChatMessageController {
27 41
28 @Resource 42 @Resource
29 - private AiChatService chatService; 43 + private AiChatMessageService chatMessageService;
  44 + @Resource
  45 + private AiChatConversationService chatConversationService;
  46 + @Resource
  47 + private AiChatRoleService chatRoleService;
  48 +
  49 + @Resource
  50 + private AdminUserApi adminUserApi;
30 51
31 @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢") 52 @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢")
32 @PostMapping("/send") 53 @PostMapping("/send")
33 public CommonResult<AiChatMessageRespVO> sendMessage(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) { 54 public CommonResult<AiChatMessageRespVO> sendMessage(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
34 - return success(chatService.chat(sendReqVO)); 55 + return success(chatMessageService.sendMessage(sendReqVO));
35 } 56 }
36 57
37 @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快") 58 @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快")
38 @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) 59 @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
39 @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 60 @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
40 public Flux<AiChatMessageSendRespVO> sendChatMessageStream(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) { 61 public Flux<AiChatMessageSendRespVO> sendChatMessageStream(@Validated @RequestBody AiChatMessageSendReqVO sendReqVO) {
41 - return chatService.sendChatMessageStream(sendReqVO, getLoginUserId()); 62 + return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId());
42 } 63 }
43 64
44 @Operation(summary = "获得指定会话的消息列表") 65 @Operation(summary = "获得指定会话的消息列表")
45 @GetMapping("/list-by-conversation-id") 66 @GetMapping("/list-by-conversation-id")
46 @Parameter(name = "conversationId", required = true, description = "会话编号", example = "1024") 67 @Parameter(name = "conversationId", required = true, description = "会话编号", example = "1024")
47 - public CommonResult<List<AiChatMessageRespVO>> getMessageListByConversationId(@RequestParam("conversationId") Long conversationId) {  
48 - return success(chatService.getMessageListByConversationId(conversationId)); 68 + public CommonResult<List<AiChatMessageRespVO>> getChatMessageListByConversationId(
  69 + @RequestParam("conversationId") Long conversationId) {
  70 + AiChatConversationDO conversation = chatConversationService.getChatConversation(conversationId);
  71 + if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) {
  72 + return success(Collections.emptyList());
  73 + }
  74 + List<AiChatMessageDO> messageList = chatMessageService.getChatMessageListByConversationId(conversationId);
  75 + if (CollUtil.isEmpty(messageList)) {
  76 + return success(Collections.emptyList());
  77 + }
  78 +
  79 + // 拼接数据
  80 + Map<Long, AiChatRoleDO> roleMap = chatRoleService.getChatRoleMap(convertSet(messageList, AiChatMessageDO::getRoleId));
  81 + AdminUserRespDTO user = adminUserApi.getUser(getLoginUserId());
  82 + return success(BeanUtils.toBean(messageList, AiChatMessageRespVO.class, respVO -> {
  83 + MapUtils.findAndThen(roleMap, respVO.getRoleId(), role -> respVO.setRoleAvatar(role.getAvatar()));
  84 + respVO.setUserAvatar(user.getAvatar());
  85 + }));
49 } 86 }
50 87
51 @Operation(summary = "删除消息") 88 @Operation(summary = "删除消息")
52 @DeleteMapping("/delete") 89 @DeleteMapping("/delete")
53 @Parameter(name = "id", required = true, description = "消息编号", example = "1024") 90 @Parameter(name = "id", required = true, description = "消息编号", example = "1024")
54 - public CommonResult<Boolean> deleteMessage(@RequestParam("id") Long id) {  
55 - return success(chatService.deleteMessage(id)); 91 + public CommonResult<Boolean> deleteChatMessage(@RequestParam("id") Long id) {
  92 + chatMessageService.deleteMessage(id, getLoginUserId());
  93 + return success(true);
56 } 94 }
57 95
58 - @Operation(summary = "删除消息-对于对话全部消息") 96 + @Operation(summary = "删除指定会话的消息")
59 @DeleteMapping("/delete-by-conversation-id") 97 @DeleteMapping("/delete-by-conversation-id")
60 - @Parameter(name = "id", required = true, description = "消息编号", example = "1024")  
61 - public CommonResult<Boolean> deleteByConversationId(@RequestParam("conversationId") Long conversationId) {  
62 - return success(chatService.deleteByConversationId(conversationId)); 98 + @Parameter(name = "conversationId", required = true, description = "会话编号", example = "1024")
  99 + public CommonResult<Boolean> deleteChatMessageByConversationId(@RequestParam("conversationId") Long conversationId) {
  100 + chatMessageService.deleteChatMessageByConversationId(conversationId, getLoginUserId());
  101 + return success(true);
63 } 102 }
  103 +
64 } 104 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
@@ -31,13 +31,14 @@ public class AiChatMessageSendRespVO { @@ -31,13 +31,14 @@ public class AiChatMessageSendRespVO {
31 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) 31 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
32 private LocalDateTime createTime; 32 private LocalDateTime createTime;
33 33
34 - // ========= 扩展字段 34 + // ========== 扩展字段 ==========
35 35
36 - @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://xxx") 36 + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://iocoder.cn/1.png")
37 private String userAvatar; 37 private String userAvatar;
38 38
39 - @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://xxx") 39 + @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://iocoder.cn/2.png")
40 private String roleAvatar; 40 private String roleAvatar;
  41 +
41 } 42 }
42 43
43 } 44 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/convert/AiChatMessageConvert.java deleted
1 -package cn.iocoder.yudao.module.ai.convert;  
2 -  
3 -import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;  
4 -import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;  
5 -import org.mapstruct.Mapper;  
6 -import org.mapstruct.factory.Mappers;  
7 -  
8 -import java.util.List;  
9 -  
10 -/**  
11 - * 聊天 对话 convert  
12 - *  
13 - * @author fansili  
14 - * @time 2024/4/18 16:39  
15 - * @since 1.0  
16 - */  
17 -@Mapper  
18 -public interface AiChatMessageConvert {  
19 -  
20 - AiChatMessageConvert INSTANCE = Mappers.getMapper(AiChatMessageConvert.class);  
21 -  
22 - /**  
23 - * 转换 AiChatMessageRespVO  
24 - *  
25 - * @param aiChatMessageDOList  
26 - * @return  
27 - */  
28 - List<AiChatMessageRespVO> convertAiChatMessageRespVOList(List<AiChatMessageDO> aiChatMessageDOList);  
29 -  
30 -}  
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/AiChatMessageMapper.java deleted
1 -package cn.iocoder.yudao.module.ai.dal.mysql;  
2 -  
3 -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;  
4 -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;  
5 -import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;  
6 -import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;  
7 -import org.apache.ibatis.annotations.Mapper;  
8 -import org.springframework.stereotype.Repository;  
9 -  
10 -import java.util.List;  
11 -  
12 -/**  
13 - * message mapper  
14 - *  
15 - * @fansili  
16 - * @since v1.0  
17 - */  
18 -@Repository  
19 -@Mapper  
20 -public interface AiChatMessageMapper extends BaseMapperX<AiChatMessageDO> {  
21 -  
22 - /**  
23 - * 查询 - 根据 对话id查询  
24 - *  
25 - * @param conversationId  
26 - */  
27 - default List<AiChatMessageDO> selectByConversationId(Long conversationId) {  
28 - return this.selectList(  
29 - new LambdaQueryWrapperX<AiChatMessageDO>()  
30 - .eq(AiChatMessageDO::getConversationId, conversationId)  
31 - .orderByAsc(AiChatMessageDO::getId)  
32 - );  
33 - }  
34 -  
35 - /**  
36 - * 删除 - 根据 conversationId  
37 - *  
38 - * @param conversationId  
39 - */  
40 - default int deleteByConversationId(Long conversationId) {  
41 - return this.delete(new LambdaQueryWrapperX<AiChatMessageDO>().eq(AiChatMessageDO::getConversationId, conversationId));  
42 - }  
43 -}  
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/AiChatModelMapper.java
1 package cn.iocoder.yudao.module.ai.dal.mysql; 1 package cn.iocoder.yudao.module.ai.dal.mysql;
2 2
3 -import cn.hutool.core.collection.CollUtil;  
4 -import cn.iocoder.yudao.framework.common.pojo.PageParam;  
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
7 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
8 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
9 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; 7 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO;
10 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; 8 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
11 -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;  
12 import org.apache.ibatis.annotations.Mapper; 9 import org.apache.ibatis.annotations.Mapper;
13 10
14 -import java.util.List;  
15 -  
16 import java.util.Collection; 11 import java.util.Collection;
17 import java.util.List; 12 import java.util.List;
18 13
@@ -42,7 +37,6 @@ public interface AiChatModelMapper extends BaseMapperX&lt;AiChatModelDO&gt; { @@ -42,7 +37,6 @@ public interface AiChatModelMapper extends BaseMapperX&lt;AiChatModelDO&gt; {
42 return this.selectList(new LambdaQueryWrapperX<AiChatModelDO>().eq(AiChatModelDO::getId, modalIds)); 37 return this.selectList(new LambdaQueryWrapperX<AiChatModelDO>().eq(AiChatModelDO::getId, modalIds));
43 } 38 }
44 39
45 -  
46 default PageResult<AiChatModelDO> selectPage(AiChatModelPageReqVO reqVO) { 40 default PageResult<AiChatModelDO> selectPage(AiChatModelPageReqVO reqVO) {
47 return selectPage(reqVO, new LambdaQueryWrapperX<AiChatModelDO>() 41 return selectPage(reqVO, new LambdaQueryWrapperX<AiChatModelDO>()
48 .likeIfPresent(AiChatModelDO::getName, reqVO.getName()) 42 .likeIfPresent(AiChatModelDO::getName, reqVO.getName())
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/AiImageMapper.java
@@ -13,7 +13,6 @@ import org.springframework.stereotype.Repository; @@ -13,7 +13,6 @@ import org.springframework.stereotype.Repository;
13 * @time 2024/4/28 14:01 13 * @time 2024/4/28 14:01
14 * @since 1.0 14 * @since 1.0
15 */ 15 */
16 -@Repository  
17 @Mapper 16 @Mapper
18 public interface AiImageMapper extends BaseMapperX<AiImageDO> { 17 public interface AiImageMapper extends BaseMapperX<AiImageDO> {
19 18
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatMessageMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.mysql.chat;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  5 +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
  6 +import org.apache.ibatis.annotations.Mapper;
  7 +
  8 +import java.util.List;
  9 +
  10 +/**
  11 + * AI 聊天对话 Mapper
  12 + *
  13 + * @author fansili
  14 + */
  15 +@Mapper
  16 +public interface AiChatMessageMapper extends BaseMapperX<AiChatMessageDO> {
  17 +
  18 + default List<AiChatMessageDO> selectListByConversationId(Long conversationId) {
  19 + return selectList(new LambdaQueryWrapperX<AiChatMessageDO>()
  20 + .eq(AiChatMessageDO::getConversationId, conversationId)
  21 + .orderByAsc(AiChatMessageDO::getId));
  22 + }
  23 +
  24 +}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/AiChatService.java renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java
1 -package cn.iocoder.yudao.module.ai.service; 1 +package cn.iocoder.yudao.module.ai.service.chat;
2 2
3 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.*; 3 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.*;
  4 +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
4 import reactor.core.publisher.Flux; 5 import reactor.core.publisher.Flux;
5 6
6 import java.util.List; 7 import java.util.List;
7 8
8 /** 9 /**
9 - * 聊天 chat 10 + * AI 聊天消息 Service 接口
10 * 11 *
11 * @author fansili 12 * @author fansili
12 - * @time 2024/4/14 15:55  
13 - * @since 1.0  
14 */ 13 */
15 -public interface AiChatService { 14 +public interface AiChatMessageService {
16 15
17 /** 16 /**
18 - * chat 17 + * 发送消息
19 * 18 *
20 - * @param sendReqVO  
21 - * @return 19 + * @param sendReqVO 发送信息
  20 + * @return 发送结果
22 */ 21 */
23 - AiChatMessageRespVO chat(AiChatMessageSendReqVO sendReqVO); 22 + AiChatMessageRespVO sendMessage(AiChatMessageSendReqVO sendReqVO);
24 23
25 /** 24 /**
26 - * 获取 - 获取对话 message list 25 + * 发送消息
27 * 26 *
28 - * @param conversationId  
29 - * @return 27 + * @param sendReqVO 发送信息
  28 + * @param userId 用户编号
  29 + * @return 发送结果
30 */ 30 */
31 - List<AiChatMessageRespVO> getMessageListByConversationId(Long conversationId); 31 + Flux<AiChatMessageSendRespVO> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId);
32 32
33 /** 33 /**
34 - * 删除 - 删除message 34 + * 获得指定会话的消息列表
35 * 35 *
36 - * @param id  
37 - * @return 36 + * @param conversationId 会话编号
  37 + * @return 消息列表
38 */ 38 */
39 - Boolean deleteMessage(Long id); 39 + List<AiChatMessageDO> getChatMessageListByConversationId(Long conversationId);
40 40
41 /** 41 /**
42 - * 发送消息 42 + * 删除消息
43 * 43 *
44 - * @param sendReqVO  
45 - * @param userId  
46 - * @return 44 + * @param id 消息编号
  45 + * @param userId 用户编号
47 */ 46 */
48 - Flux<AiChatMessageSendRespVO> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId); 47 + void deleteMessage(Long id, Long userId);
49 48
50 /** 49 /**
51 - * 删除消息-对于对话全部消息 50 + * 删除指定会话的消息
52 * 51 *
53 - * @param conversationId  
54 - * @return 52 + * @param conversationId 会话编号
  53 + * @param userId 用户编号
55 */ 54 */
56 - Boolean deleteByConversationId(Long conversationId); 55 + void deleteChatMessageByConversationId(Long conversationId, Long userId);
  56 +
57 } 57 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/impl/AiChatServiceImpl.java renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
1 -package cn.iocoder.yudao.module.ai.service.impl; 1 +package cn.iocoder.yudao.module.ai.service.chat;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.ObjUtil; 4 import cn.hutool.core.util.ObjUtil;
@@ -20,13 +20,10 @@ import org.springframework.ai.chat.prompt.Prompt; @@ -20,13 +20,10 @@ import org.springframework.ai.chat.prompt.Prompt;
20 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; 20 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
21 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO; 21 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
22 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; 22 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
23 -import cn.iocoder.yudao.module.ai.convert.AiChatMessageConvert;  
24 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; 23 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
25 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; 24 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
26 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; 25 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
27 -import cn.iocoder.yudao.module.ai.dal.mysql.AiChatMessageMapper;  
28 -import cn.iocoder.yudao.module.ai.service.AiChatService;  
29 -import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; 26 +import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
30 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; 27 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
31 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; 28 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
32 import lombok.extern.slf4j.Slf4j; 29 import lombok.extern.slf4j.Slf4j;
@@ -37,21 +34,20 @@ import reactor.core.scheduler.Schedulers; @@ -37,21 +34,20 @@ import reactor.core.scheduler.Schedulers;
37 34
38 import java.time.LocalDateTime; 35 import java.time.LocalDateTime;
39 import java.util.*; 36 import java.util.*;
40 -import java.util.stream.Collectors;  
41 37
42 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 38 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  39 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  40 +import static cn.iocoder.yudao.module.ai.ErrorCodeConstants.AI_CHAT_MESSAGE_NOT_EXIST;
43 import static cn.iocoder.yudao.module.ai.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS; 41 import static cn.iocoder.yudao.module.ai.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
44 42
45 /** 43 /**
46 - * 聊天 service 44 + * AI 聊天消息 Service 实现类
47 * 45 *
48 * @author fansili 46 * @author fansili
49 - * @time 2024/4/14 15:55  
50 - * @since 1.0  
51 */ 47 */
52 -@Slf4j  
53 @Service 48 @Service
54 -public class AiChatServiceImpl implements AiChatService { 49 +@Slf4j
  50 +public class AiChatMessageServiceImpl implements AiChatMessageService {
55 51
56 @Resource 52 @Resource
57 private AiChatMessageMapper chatMessageMapper; 53 private AiChatMessageMapper chatMessageMapper;
@@ -72,7 +68,7 @@ public class AiChatServiceImpl implements AiChatService { @@ -72,7 +68,7 @@ public class AiChatServiceImpl implements AiChatService {
72 private AdminUserApi adminUserApi; 68 private AdminUserApi adminUserApi;
73 69
74 @Transactional(rollbackFor = Exception.class) 70 @Transactional(rollbackFor = Exception.class)
75 - public AiChatMessageRespVO chat(AiChatMessageSendReqVO req) { 71 + public AiChatMessageRespVO sendMessage(AiChatMessageSendReqVO req) {
76 return null; // TODO 芋艿:一起改 72 return null; // TODO 芋艿:一起改
77 // Long loginUserId = SecurityFrameworkUtils.getLoginUserId(); 73 // Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
78 // // 查询对话 74 // // 查询对话
@@ -117,10 +113,13 @@ public class AiChatServiceImpl implements AiChatService { @@ -117,10 +113,13 @@ public class AiChatServiceImpl implements AiChatService {
117 if (ObjUtil.notEqual(conversation.getUserId(), userId)) { 113 if (ObjUtil.notEqual(conversation.getUserId(), userId)) {
118 throw exception(CHAT_CONVERSATION_NOT_EXISTS); // TODO 芋艿:异常情况的对接; 114 throw exception(CHAT_CONVERSATION_NOT_EXISTS); // TODO 芋艿:异常情况的对接;
119 } 115 }
120 - List<AiChatMessageDO> historyMessages = chatMessageMapper.selectByConversationId(conversation.getId()); 116 + List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
121 // 1.2 校验模型 117 // 1.2 校验模型
122 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); 118 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
123 StreamingChatClient chatClient = apiKeyService.getStreamingChatClient(model.getKeyId()); 119 StreamingChatClient chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
  120 + // 1.3 获取用户头像、角色头像
  121 + AdminUserRespDTO user = adminUserApi.getUser(SecurityFrameworkUtils.getLoginUserId());
  122 + AiChatRoleDO role = conversation.getRoleId() != null ? chatRoleService.getChatRole(conversation.getRoleId()) : null;
124 123
125 // 2. 插入 user 发送消息 124 // 2. 插入 user 发送消息
126 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, 125 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
@@ -136,19 +135,17 @@ public class AiChatServiceImpl implements AiChatService { @@ -136,19 +135,17 @@ public class AiChatServiceImpl implements AiChatService {
136 135
137 // 3.3 流式返回 136 // 3.3 流式返回
138 // 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 137 // 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
139 -  
140 - // 3.4 获取用户头像、角色头像  
141 - AdminUserRespDTO user = adminUserApi.getUser(SecurityFrameworkUtils.getLoginUserId());  
142 - AiChatRoleDO chatRole = chatRoleService.getChatRole(assistantMessage.getRoleId());  
143 -  
144 StringBuffer contentBuffer = new StringBuffer(); 138 StringBuffer contentBuffer = new StringBuffer();
145 return streamResponse.publishOn(Schedulers.single()).map(chunk -> { 139 return streamResponse.publishOn(Schedulers.single()).map(chunk -> {
146 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; 140 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null;
147 newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 141 newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
148 contentBuffer.append(newContent); 142 contentBuffer.append(newContent);
149 // 响应结果 143 // 响应结果
150 - return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class).setUserAvatar(user.getAvatar()))  
151 - .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent).setRoleAvatar(chatRole == null ? null : chatRole.getAvatar())); 144 + AiChatMessageSendRespVO.Message send = BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class,
  145 + o -> o.setUserAvatar(user.getAvatar()));
  146 + AiChatMessageSendRespVO.Message receive = BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class,
  147 + o -> o.setRoleAvatar(role != null ? role.getAvatar() : null)).setContent(newContent);
  148 + return new AiChatMessageSendRespVO().setSend(send).setReceive(receive);
152 }).doOnComplete(() -> { 149 }).doOnComplete(() -> {
153 chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())); 150 chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()));
154 }).doOnError(throwable -> { 151 }).doOnError(throwable -> {
@@ -157,11 +154,6 @@ public class AiChatServiceImpl implements AiChatService { @@ -157,11 +154,6 @@ public class AiChatServiceImpl implements AiChatService {
157 }); 154 });
158 } 155 }
159 156
160 - @Override  
161 - public Boolean deleteByConversationId(Long conversationId) {  
162 - return chatMessageMapper.deleteByConversationId(conversationId) > 0;  
163 - }  
164 -  
165 private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, 157 private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
166 AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) { 158 AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) {
167 // 1. 构建 Prompt Message 列表 159 // 1. 构建 Prompt Message 列表
@@ -174,12 +166,11 @@ public class AiChatServiceImpl implements AiChatService { @@ -174,12 +166,11 @@ public class AiChatServiceImpl implements AiChatService {
174 // 1.3 user message 新发送消息 166 // 1.3 user message 新发送消息
175 chatMessages.add(new UserMessage(sendReqVO.getContent())); 167 chatMessages.add(new UserMessage(sendReqVO.getContent()));
176 168
177 - // 2. 构建 ChatOptions 对象 TODO 芋艿:临时注释掉;等文心一言兼容了; 169 + // 2. 构建 ChatOptions 对象
178 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); 170 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
179 ChatOptions chatOptions = clientFactory.buildChatOptions(platform, model.getModel(), 171 ChatOptions chatOptions = clientFactory.buildChatOptions(platform, model.getModel(),
180 conversation.getTemperature(), conversation.getMaxTokens()); 172 conversation.getTemperature(), conversation.getMaxTokens());
181 return new Prompt(chatMessages, chatOptions); 173 return new Prompt(chatMessages, chatOptions);
182 -// return new Prompt(chatMessages);  
183 } 174 }
184 175
185 /** 176 /**
@@ -231,42 +222,30 @@ public class AiChatServiceImpl implements AiChatService { @@ -231,42 +222,30 @@ public class AiChatServiceImpl implements AiChatService {
231 } 222 }
232 223
233 @Override 224 @Override
234 - public List<AiChatMessageRespVO> getMessageListByConversationId(Long conversationId) {  
235 - // 校验对话是否存在  
236 - chatConversationService.validateExists(conversationId);  
237 - // 获取对话所有 message  
238 - List<AiChatMessageDO> aiChatMessageDOList = chatMessageMapper.selectByConversationId(conversationId);  
239 - // 获取模型信息  
240 - Set<Long> roleIds = aiChatMessageDOList.stream().map(AiChatMessageDO::getRoleId).collect(Collectors.toSet());  
241 - List<AiChatRoleDO> roleList;  
242 - if (!CollUtil.isEmpty(roleIds)) {  
243 - roleList = chatRoleService.getChatRoles(roleIds);  
244 - } else {  
245 - roleList = Collections.emptyList(); 225 + public List<AiChatMessageDO> getChatMessageListByConversationId(Long conversationId) {
  226 + return chatMessageMapper.selectListByConversationId(conversationId);
  227 + }
  228 +
  229 + @Override
  230 + public void deleteMessage(Long id, Long userId) {
  231 + // 1. 校验消息存在
  232 + AiChatMessageDO message = chatMessageMapper.selectById(id);
  233 + if (message == null || ObjUtil.notEqual(message.getUserId(), userId)) {
  234 + throw exception(AI_CHAT_MESSAGE_NOT_EXIST);
246 } 235 }
247 - Map<Long, AiChatRoleDO> roleMap = roleList.stream().collect(Collectors.toMap(AiChatRoleDO::getId, o -> o));  
248 - // 转换 AiChatMessageRespVO  
249 - List<AiChatMessageRespVO> aiChatMessageRespList = AiChatMessageConvert.INSTANCE.convertAiChatMessageRespVOList(aiChatMessageDOList);  
250 - // 获取用户信息  
251 - AdminUserRespDTO user = adminUserApi.getUser(SecurityFrameworkUtils.getLoginUserId());  
252 - // 设置用户头像 和 模型头像  
253 - return aiChatMessageRespList.stream().map(item -> {  
254 - // 设置 role 头像  
255 - if (roleMap.containsKey(item.getRoleId())) {  
256 - AiChatRoleDO role = roleMap.get(item.getRoleId());  
257 - item.setRoleAvatar(role.getAvatar());  
258 - }  
259 - // 设置 user 头像  
260 - if (user != null) {  
261 - item.setUserAvatar(user.getAvatar());  
262 - }  
263 - return item;  
264 - }).collect(Collectors.toList()); 236 + // 2. 执行删除
  237 + chatMessageMapper.deleteById(id);
265 } 238 }
266 239
267 @Override 240 @Override
268 - public Boolean deleteMessage(Long id) {  
269 - return chatMessageMapper.deleteById(id) > 0; 241 + public void deleteChatMessageByConversationId(Long conversationId, Long userId) {
  242 + // 1. 校验消息存在
  243 + List<AiChatMessageDO> messages = chatMessageMapper.selectListByConversationId(conversationId);
  244 + if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) {
  245 + throw exception(AI_CHAT_MESSAGE_NOT_EXIST);
  246 + }
  247 + // 2. 执行删除
  248 + chatMessageMapper.deleteBatchIds(convertList(messages, AiChatMessageDO::getId));
270 } 249 }
271 250
272 } 251 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java
@@ -4,12 +4,14 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -4,12 +4,14 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO; 4 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO;
5 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveMyReqVO; 5 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveMyReqVO;
6 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; 6 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO;
7 -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;  
8 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; 7 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
9 import jakarta.validation.Valid; 8 import jakarta.validation.Valid;
10 9
  10 +import java.util.Collection;
11 import java.util.List; 11 import java.util.List;
12 -import java.util.Set; 12 +import java.util.Map;
  13 +
  14 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
13 15
14 /** 16 /**
15 * AI 聊天角色 Service 接口 17 * AI 聊天角色 Service 接口
@@ -74,12 +76,16 @@ public interface AiChatRoleService { @@ -74,12 +76,16 @@ public interface AiChatRoleService {
74 AiChatRoleDO getChatRole(Long id); 76 AiChatRoleDO getChatRole(Long id);
75 77
76 /** 78 /**
77 - * 获得聊天角色 - 根据 ids 79 + * 获得聊天角色列表
78 * 80 *
79 - * @param roleIds  
80 - * @return 81 + * @param ids 编号数组
  82 + * @return 聊天角色列表
81 */ 83 */
82 - List<AiChatRoleDO> getChatRoles(Set<Long> roleIds); 84 + List<AiChatRoleDO> getChatRoleList(Collection<Long> ids);
  85 +
  86 + default Map<Long, AiChatRoleDO> getChatRoleMap(Collection<Long> ids) {
  87 + return convertMap(getChatRoleList(ids), AiChatRoleDO::getId);
  88 + }
83 89
84 /** 90 /**
85 * 校验聊天角色是否合法 91 * 校验聊天角色是否合法
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java
1 package cn.iocoder.yudao.module.ai.service.model; 1 package cn.iocoder.yudao.module.ai.service.model;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.hutool.core.util.ObjectUtil; 4 import cn.hutool.core.util.ObjectUtil;
4 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 6 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@@ -14,12 +15,9 @@ import jakarta.annotation.Resource; @@ -14,12 +15,9 @@ import jakarta.annotation.Resource;
14 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
15 import org.springframework.stereotype.Service; 16 import org.springframework.stereotype.Service;
16 17
  18 +import java.util.Collection;
  19 +import java.util.Collections;
17 import java.util.List; 20 import java.util.List;
18 -import java.util.Objects;  
19 -import java.util.Set;  
20 -import java.util.function.Function;  
21 -import java.util.function.Predicate;  
22 -import java.util.stream.Collectors;  
23 21
24 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 22 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
25 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 23 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -107,8 +105,11 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @@ -107,8 +105,11 @@ public class AiChatRoleServiceImpl implements AiChatRoleService {
107 } 105 }
108 106
109 @Override 107 @Override
110 - public List<AiChatRoleDO> getChatRoles(Set<Long> roleIds) {  
111 - return chatRoleMapper.selectBatchIds(roleIds); 108 + public List<AiChatRoleDO> getChatRoleList(Collection<Long> ids) {
  109 + if (CollUtil.isEmpty(ids)) {
  110 + return Collections.emptyList();
  111 + }
  112 + return chatRoleMapper.selectBatchIds(ids);
112 } 113 }
113 114
114 @Override 115 @Override