|
@@ -1,39 +1,40 @@
|
|
|
package org.jeecg.modules.adweb.gpt.service.impl;
|
|
|
|
|
|
-import cn.hutool.core.util.StrUtil;
|
|
|
-import cn.hutool.json.JSONUtil;
|
|
|
-import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.google.common.collect.Lists;
|
|
|
import com.unfbx.chatgpt.OpenAiStreamClient;
|
|
|
import com.unfbx.chatgpt.entity.chat.ChatCompletion;
|
|
|
import com.unfbx.chatgpt.entity.chat.Message;
|
|
|
import com.unfbx.chatgpt.exception.BaseException;
|
|
|
+
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
+
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
import org.apache.shiro.SecurityUtils;
|
|
|
import org.jeecg.common.api.vo.Result;
|
|
|
import org.jeecg.common.exception.JeecgBootException;
|
|
|
import org.jeecg.common.system.vo.LoginUser;
|
|
|
+import org.jeecg.common.util.FastJsonUtil;
|
|
|
import org.jeecg.common.util.SpringContextUtils;
|
|
|
import org.jeecg.common.util.UUIDGenerator;
|
|
|
+import org.jeecg.modules.adweb.common.util.AdwebRedisUtil;
|
|
|
import org.jeecg.modules.adweb.gpt.cache.LocalCache;
|
|
|
import org.jeecg.modules.adweb.gpt.entity.ChatHistory;
|
|
|
import org.jeecg.modules.adweb.gpt.listeners.OpenAISSEEventSourceListener;
|
|
|
-import org.jeecg.modules.adweb.gpt.mapper.ChatHistoryMapper;
|
|
|
import org.jeecg.modules.adweb.gpt.service.ChatService;
|
|
|
+import org.jeecg.modules.adweb.gpt.service.IChatHistoryService;
|
|
|
import org.jeecg.modules.adweb.gpt.vo.ChatHistoryVO;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
-import org.springframework.data.redis.core.RedisTemplate;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
|
|
|
|
import java.io.IOException;
|
|
|
-import java.util.ArrayList;
|
|
|
-import java.util.Date;
|
|
|
-import java.util.List;
|
|
|
-
|
|
|
-//update-begin---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
* AI助手聊天Service
|
|
|
+ *
|
|
|
* @author chenrui
|
|
|
* @date 2024/1/26 20:07
|
|
|
*/
|
|
@@ -41,90 +42,62 @@ import java.util.List;
|
|
|
@Slf4j
|
|
|
public class ChatServiceImpl implements ChatService {
|
|
|
|
|
|
- @Autowired
|
|
|
- private ChatHistoryMapper chatHistoryMapper;
|
|
|
-
|
|
|
- //update-begin---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
|
|
- private static final String CACHE_KEY_PREFIX = "ai:chart:";
|
|
|
+ @Autowired private IChatHistoryService chatHistoryService;
|
|
|
|
|
|
- /**
|
|
|
- *
|
|
|
- */
|
|
|
- private static final String CACHE_KEY_MSG_CONTEXT = "msg_content";
|
|
|
+ // private static final String CACHE_KEY_PREFIX = "ai:chat:";
|
|
|
|
|
|
+ // private static final String CACHE_KEY_MSG_HISTORY = "msg_history";
|
|
|
|
|
|
- /**
|
|
|
- *
|
|
|
- */
|
|
|
- private static final String CACHE_KEY_MSG_HISTORY = "msg_history";
|
|
|
+ private static final String REDIS_KEY_PREFIX_MSG_CONTEXT = "ai:chat:msg_content";
|
|
|
|
|
|
- @Autowired
|
|
|
- RedisTemplate redisTemplate;
|
|
|
- //update-end---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
|
|
+ @Autowired AdwebRedisUtil adwebRedisUtil;
|
|
|
|
|
|
- private OpenAiStreamClient openAiStreamClient = null;
|
|
|
-
|
|
|
- //update-begin---author:chenrui ---date:20240131 for:[QQYUN-8212]fix 没有配置启动报错------------
|
|
|
- public ChatServiceImpl() {
|
|
|
- try {
|
|
|
- this.openAiStreamClient = SpringContextUtils.getBean(OpenAiStreamClient.class);
|
|
|
- } catch (Exception ignored) {
|
|
|
- }
|
|
|
- }
|
|
|
+ @Autowired private OpenAiStreamClient openAiStreamClient;
|
|
|
|
|
|
- /**
|
|
|
- * 防止client不能成功注入
|
|
|
- * @return
|
|
|
- * @author chenrui
|
|
|
- * @date 2024/2/3 23:08
|
|
|
- */
|
|
|
- private OpenAiStreamClient ensureClient(){
|
|
|
- if(null == this.openAiStreamClient){
|
|
|
+ /** 防止client不能成功注入 */
|
|
|
+ private OpenAiStreamClient ensureClient() {
|
|
|
+ if (Objects.isNull(this.openAiStreamClient)) {
|
|
|
this.openAiStreamClient = SpringContextUtils.getBean(OpenAiStreamClient.class);
|
|
|
}
|
|
|
return this.openAiStreamClient;
|
|
|
}
|
|
|
- //update-end---author:chenrui ---date:20240131 for:[QQYUN-8212]fix 没有配置启动报错------------
|
|
|
-
|
|
|
- private String getUserId() {
|
|
|
- LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
|
|
- return sysUser.getId();
|
|
|
- }
|
|
|
|
|
|
@Override
|
|
|
public SseEmitter createChat() {
|
|
|
String uid = getUserId();
|
|
|
- //默认30秒超时,设置为0L则永不超时
|
|
|
+ // 默认30秒超时,设置为0L则永不超时
|
|
|
SseEmitter sseEmitter = new SseEmitter(-0L);
|
|
|
- //完成后回调
|
|
|
- sseEmitter.onCompletion(() -> {
|
|
|
- log.info("[{}]结束连接...................",uid);
|
|
|
- LocalCache.CACHE.remove(uid);
|
|
|
- });
|
|
|
- //超时回调
|
|
|
- sseEmitter.onTimeout(() -> {
|
|
|
- log.info("[{}]连接超时...................", uid);
|
|
|
- });
|
|
|
- //异常回调
|
|
|
+ // 完成后回调
|
|
|
+ sseEmitter.onCompletion(
|
|
|
+ () -> {
|
|
|
+ log.info("[{}]结束连接...................", uid);
|
|
|
+ LocalCache.CACHE.remove(uid);
|
|
|
+ });
|
|
|
+ // 超时回调
|
|
|
+ sseEmitter.onTimeout(
|
|
|
+ () -> {
|
|
|
+ log.info("[{}]连接超时...................", uid);
|
|
|
+ });
|
|
|
+ // 异常回调
|
|
|
sseEmitter.onError(
|
|
|
throwable -> {
|
|
|
try {
|
|
|
log.info("[{}]连接异常,{}", uid, throwable.toString());
|
|
|
- sseEmitter.send(SseEmitter.event()
|
|
|
- .id(uid)
|
|
|
- .name("发生异常!")
|
|
|
- .data(Message.builder().content("发生异常请重试!").build())
|
|
|
- .reconnectTime(3000));
|
|
|
+ sseEmitter.send(
|
|
|
+ SseEmitter.event()
|
|
|
+ .id(uid)
|
|
|
+ .name("发生异常!")
|
|
|
+ .data(Message.builder().content("发生异常请重试!").build())
|
|
|
+ .reconnectTime(3000));
|
|
|
LocalCache.CACHE.put(uid, sseEmitter);
|
|
|
} catch (IOException e) {
|
|
|
- log.error(e.getMessage(),e);
|
|
|
+ log.error(e.getMessage(), e);
|
|
|
}
|
|
|
- }
|
|
|
- );
|
|
|
+ });
|
|
|
try {
|
|
|
sseEmitter.send(SseEmitter.event().reconnectTime(5000));
|
|
|
} catch (IOException e) {
|
|
|
- log.error(e.getMessage(),e);
|
|
|
+ log.error(e.getMessage(), e);
|
|
|
}
|
|
|
LocalCache.CACHE.put(uid, sseEmitter);
|
|
|
log.info("[{}]创建sse连接成功!", uid);
|
|
@@ -137,7 +110,7 @@ public class ChatServiceImpl implements ChatService {
|
|
|
SseEmitter sse = (SseEmitter) LocalCache.CACHE.get(uid);
|
|
|
if (sse != null) {
|
|
|
sse.complete();
|
|
|
- //移除
|
|
|
+ // 移除
|
|
|
LocalCache.CACHE.remove(uid);
|
|
|
}
|
|
|
}
|
|
@@ -145,71 +118,101 @@ public class ChatServiceImpl implements ChatService {
|
|
|
@Override
|
|
|
public void sendMessage(String topicId, String message) {
|
|
|
String uid = getUserId();
|
|
|
- if (StrUtil.isBlank(message)) {
|
|
|
+ if (StringUtils.isBlank(message)) {
|
|
|
log.info("参数异常,message为null");
|
|
|
throw new BaseException("参数异常,message不能为空~");
|
|
|
}
|
|
|
- if (StrUtil.isBlank(topicId)) {
|
|
|
+ if (StringUtils.isBlank(topicId)) {
|
|
|
topicId = UUIDGenerator.generate();
|
|
|
}
|
|
|
- //update-begin---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
|
|
- log.info("话题id:{}", topicId);
|
|
|
- String cacheKey = CACHE_KEY_PREFIX + uid + "_" + topicId;
|
|
|
- String messageContext = (String) redisTemplate.opsForHash().get(cacheKey, CACHE_KEY_MSG_CONTEXT);
|
|
|
- List<Message> msgHistory = new ArrayList<>();
|
|
|
- if (StrUtil.isNotBlank(messageContext)) {
|
|
|
- List<Message> messages = JSONArray.parseArray(messageContext, Message.class);
|
|
|
- msgHistory = messages == null ? new ArrayList<>() : messages;
|
|
|
- }
|
|
|
- Message currentMessage = Message.builder().content(message).role(Message.Role.USER).build();
|
|
|
- msgHistory.add(currentMessage);
|
|
|
|
|
|
+ log.info("话题id:{}", topicId);
|
|
|
+ // 1. 获取当前话题下的message context
|
|
|
+ String contextKey = String.format("%s:%s:%s", REDIS_KEY_PREFIX_MSG_CONTEXT, uid, topicId);
|
|
|
+ String contextValue = adwebRedisUtil.getString(contextKey);
|
|
|
+ List<Message> messageContext =
|
|
|
+ StringUtils.isNotEmpty(contextValue)
|
|
|
+ ? FastJsonUtil.parseList(contextValue, Message.class)
|
|
|
+ : Lists.newArrayList();
|
|
|
+ // 1.1 添加当前message到context
|
|
|
+ messageContext.add(Message.builder().content(message).role(Message.Role.USER).build());
|
|
|
+
|
|
|
+ // 2. 发送GPT请求
|
|
|
SseEmitter sseEmitter = (SseEmitter) LocalCache.CACHE.get(uid);
|
|
|
if (sseEmitter == null) {
|
|
|
log.info("聊天消息推送失败uid:[{}],没有创建连接,请重试。", uid);
|
|
|
throw new JeecgBootException("聊天消息推送失败uid:[{}],没有创建连接,请重试。~");
|
|
|
}
|
|
|
- OpenAISSEEventSourceListener openAIEventSourceListener = new OpenAISSEEventSourceListener(topicId, sseEmitter);
|
|
|
- ChatCompletion completion = ChatCompletion
|
|
|
- .builder()
|
|
|
- .messages(msgHistory)
|
|
|
- .model(ChatCompletion.Model.GPT_3_5_TURBO.getName())
|
|
|
- .build();
|
|
|
+ OpenAISSEEventSourceListener openAIEventSourceListener =
|
|
|
+ new OpenAISSEEventSourceListener(topicId, sseEmitter);
|
|
|
+ ChatCompletion completion =
|
|
|
+ ChatCompletion.builder()
|
|
|
+ .messages(messageContext)
|
|
|
+ .model(ChatCompletion.Model.GPT_3_5_TURBO.getName())
|
|
|
+ .build();
|
|
|
ensureClient().streamChatCompletion(completion, openAIEventSourceListener);
|
|
|
- redisTemplate.opsForHash().put(cacheKey, CACHE_KEY_MSG_CONTEXT, JSONUtil.toJsonStr(msgHistory));
|
|
|
- //update-end---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
|
|
+
|
|
|
+ // 3. 将message context保存到Redis
|
|
|
+ adwebRedisUtil.set(
|
|
|
+ contextKey,
|
|
|
+ FastJsonUtil.toJSONString(
|
|
|
+ messageContext.stream()
|
|
|
+ // 每个话题仅保存最后100条message
|
|
|
+ .skip(Math.max(0, messageContext.size() - 100))
|
|
|
+ .collect(Collectors.toList())),
|
|
|
+ // 话题 TTL设置为3小时
|
|
|
+ 3 * 60 * 60);
|
|
|
+
|
|
|
Result.ok(completion.tokens());
|
|
|
}
|
|
|
|
|
|
- //update-begin---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
|
|
@Override
|
|
|
-// public Result<?> saveHistory(ChatHistoryVO chatHistoryVO) {
|
|
|
-// String uid = getUserId();
|
|
|
-// String cacheKey = CACHE_KEY_PREFIX + CACHE_KEY_MSG_HISTORY + ":" + uid;
|
|
|
-// redisTemplate.opsForValue().set(cacheKey, chatHistoryVO.getContent());
|
|
|
-// return Result.OK("保存成功");
|
|
|
-// }
|
|
|
public Result<?> saveHistory(ChatHistoryVO chatHistoryVO) {
|
|
|
+ // String uid = getUserId();
|
|
|
+ // String cacheKey = CACHE_KEY_PREFIX + CACHE_KEY_MSG_HISTORY + ":" + uid;
|
|
|
+ // redisTemplate.opsForValue().set(cacheKey, chatHistoryVO.getContent());
|
|
|
+ // return Result.OK("保存成功");
|
|
|
+
|
|
|
+ // AdWeb重构 - 聊天记录保存到DB,不存Redis
|
|
|
String uid = getUserId();
|
|
|
- ChatHistory chatHistory = new ChatHistory();
|
|
|
- chatHistory.setUserId(uid);
|
|
|
- chatHistory.setRole("USER");
|
|
|
+ ChatHistory chatHistory =
|
|
|
+ chatHistoryService.getOne(
|
|
|
+ new LambdaQueryWrapper<ChatHistory>().eq(ChatHistory::getUid, uid));
|
|
|
+
|
|
|
+ if (Objects.isNull(chatHistory)) {
|
|
|
+ chatHistory = new ChatHistory();
|
|
|
+ chatHistory.setUid(uid);
|
|
|
+ chatHistory.setRole(Message.Role.USER.getName());
|
|
|
+ }
|
|
|
+
|
|
|
chatHistory.setContent(chatHistoryVO.getContent());
|
|
|
- chatHistory.setCreateTime(new Date());
|
|
|
- chatHistoryMapper.insert(chatHistory);
|
|
|
+ chatHistoryService.saveOrUpdate(chatHistory);
|
|
|
+
|
|
|
return Result.OK("保存成功");
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public Result<ChatHistoryVO> getHistoryByTopic() {
|
|
|
+ // String uid = getUserId();
|
|
|
+ // String cacheKey = CACHE_KEY_PREFIX + CACHE_KEY_MSG_HISTORY + ":" + uid;
|
|
|
+ // String historyContent = (String) redisTemplate.opsForValue().get(cacheKey);
|
|
|
+ // ChatHistoryVO chatHistoryVO = new ChatHistoryVO();
|
|
|
+ // chatHistoryVO.setContent(historyContent);
|
|
|
+ // return Result.OK(chatHistoryVO);
|
|
|
+
|
|
|
+ // AdWeb重构 - 聊天记录保存到DB,不存Redis
|
|
|
String uid = getUserId();
|
|
|
- String cacheKey = CACHE_KEY_PREFIX + CACHE_KEY_MSG_HISTORY + ":" + uid;
|
|
|
- String historyContent = (String) redisTemplate.opsForValue().get(cacheKey);
|
|
|
+ ChatHistory chatHistory =
|
|
|
+ chatHistoryService.getOne(
|
|
|
+ new LambdaQueryWrapper<ChatHistory>().eq(ChatHistory::getUid, uid));
|
|
|
+
|
|
|
ChatHistoryVO chatHistoryVO = new ChatHistoryVO();
|
|
|
- chatHistoryVO.setContent(historyContent);
|
|
|
+ chatHistoryVO.setContent(Objects.nonNull(chatHistory) ? chatHistory.getContent() : null);
|
|
|
return Result.OK(chatHistoryVO);
|
|
|
}
|
|
|
- //update-end---author:chenrui ---date:20240223 for:[QQYUN-8225]聊天记录保存------------
|
|
|
-}
|
|
|
|
|
|
-//update-end---author:chenrui ---date:20240126 for:【QQYUN-7932】AI助手------------
|
|
|
+ private String getUserId() {
|
|
|
+ LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
|
|
+ return sysUser.getId();
|
|
|
+ }
|
|
|
+}
|