Commit b7180d34818c7a7ff988c6c9fc74a25a1e20ebee

Authored by YunaiV
1 parent 6856f5f1

【新增】AI:会话接入 API KEY 逻辑

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.mysql.model;
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
6 7 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
7 8 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
8 9 import org.apache.ibatis.annotations.Mapper;
... ... @@ -23,4 +24,12 @@ public interface AiApiKeyMapper extends BaseMapperX<AiApiKeyDO> {
23 24 .orderByDesc(AiApiKeyDO::getId));
24 25 }
25 26  
  27 + default AiApiKeyDO selectFirstByPlatformAndStatus(String platform, Integer status) {
  28 + return selectOne(new QueryWrapperX<AiApiKeyDO>()
  29 + .eq("platform", platform)
  30 + .eq("status", status)
  31 + .limitN(1)
  32 + .orderByAsc("id"));
  33 + }
  34 +
26 35 }
27 36 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
... ... @@ -4,11 +4,13 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.hutool.core.util.StrUtil;
6 6 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
7   -import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
  7 +import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
  8 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
  9 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
  10 +import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
8 11 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
9 12 import cn.iocoder.yudao.framework.common.pojo.PageResult;
10 13 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
11   -import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
12 14 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
13 15 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
14 16 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
... ... @@ -18,6 +20,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
18 20 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
19 21 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
20 22 import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
  23 +import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
21 24 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
22 25 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
23 26 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
... ... @@ -28,6 +31,8 @@ import org.springframework.ai.chat.StreamingChatClient;
28 31 import org.springframework.ai.chat.messages.*;
29 32 import org.springframework.ai.chat.prompt.ChatOptions;
30 33 import org.springframework.ai.chat.prompt.Prompt;
  34 +import org.springframework.ai.ollama.api.OllamaOptions;
  35 +import org.springframework.ai.openai.OpenAiChatOptions;
31 36 import org.springframework.stereotype.Service;
32 37 import org.springframework.transaction.annotation.Transactional;
33 38 import reactor.core.publisher.Flux;
... ... @@ -55,9 +60,6 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
55 60 private AiChatMessageMapper chatMessageMapper;
56 61  
57 62 @Resource
58   - private AiClientFactory clientFactory;
59   -
60   - @Resource
61 63 private AiChatConversationService chatConversationService;
62 64 @Resource
63 65 private AiChatModelService chatModalService;
... ... @@ -168,11 +170,33 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
168 170  
169 171 // 2. 构建 ChatOptions 对象
170 172 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
171   - ChatOptions chatOptions = clientFactory.buildChatOptions(platform, model.getModel(),
  173 + ChatOptions chatOptions = buildChatOptions(platform, model.getModel(),
172 174 conversation.getTemperature(), conversation.getMaxTokens());
173 175 return new Prompt(chatMessages, chatOptions);
174 176 }
175 177  
  178 + private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
  179 + Float temperatureF = temperature != null ? temperature.floatValue() : null;
  180 + //noinspection EnhancedSwitchMigration
  181 + switch (platform) {
  182 + case OPENAI:
  183 + return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  184 + case OLLAMA:
  185 + return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
  186 + case YI_YAN:
  187 + // TODO @fan:增加一个 model
  188 + return new YiYanChatOptions().setTemperature(temperatureF).setMaxOutputTokens(maxTokens);
  189 + case XING_HUO:
  190 + return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
  191 + .setMaxTokens(maxTokens);
  192 + case QIAN_WEN:
  193 + // TODO @fan:增加 model、temperature 参数
  194 + return new QianWenOptions().setMaxTokens(maxTokens);
  195 + default:
  196 + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
  197 + }
  198 + }
  199 +
176 200 /**
177 201 * 从历史消息中,获得倒序的 n 组消息作为消息上下文
178 202 *
... ... @@ -183,7 +207,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
183 207 * @param sendReqVO 发送请求
184 208 * @return 消息上下文
185 209 */
186   - private List<AiChatMessageDO> filterContextMessages(List<AiChatMessageDO> messages, AiChatConversationDO conversation, AiChatMessageSendReqVO sendReqVO) {
  210 + private List<AiChatMessageDO> filterContextMessages(List<AiChatMessageDO> messages,
  211 + AiChatConversationDO conversation,
  212 + AiChatMessageSendReqVO sendReqVO) {
187 213 if (conversation.getMaxContexts() == null || ObjUtil.notEqual(sendReqVO.getUseContext(), Boolean.TRUE)) {
188 214 return Collections.emptyList();
189 215 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
... ... @@ -64,4 +64,5 @@ public interface AiImageService {
64 64 * @return
65 65 */
66 66 Boolean midjourneyNotify(Long loginUserId, MidjourneyNotifyReqVO notifyReqVO);
  67 +
67 68 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
... ... @@ -7,7 +7,6 @@ import cn.hutool.core.util.StrUtil;
7 7 import cn.hutool.extra.spring.SpringUtil;
8 8 import cn.hutool.http.HttpUtil;
9 9 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
10   -import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
11 10 import cn.iocoder.yudao.framework.common.pojo.PageParam;
12 11 import cn.iocoder.yudao.framework.common.pojo.PageResult;
13 12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ... @@ -23,6 +22,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageMidjourneyIma
23 22 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
24 23 import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper;
25 24 import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
  25 +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
26 26 import cn.iocoder.yudao.module.infra.api.file.FileApi;
27 27 import jakarta.annotation.Resource;
28 28 import lombok.extern.slf4j.Slf4j;
... ... @@ -57,7 +57,7 @@ public class AiImageServiceImpl implements AiImageService {
57 57 private FileApi fileApi;
58 58  
59 59 @Resource
60   - private AiClientFactory aiClientFactory;
  60 + private AiApiKeyService apiKeyService;
61 61  
62 62 @Autowired
63 63 private MidjourneyProxyClient midjourneyProxyClient;
... ... @@ -82,17 +82,17 @@ public class AiImageServiceImpl implements AiImageService {
82 82 .setWidth(drawReqVO.getWidth()).setHeight(drawReqVO.getHeight()).setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus());
83 83 imageMapper.insert(image);
84 84 // 2. 异步绘制,后续前端通过返回的 id 进行轮询结果
85   - getSelf().doDall(image, drawReqVO);
  85 + getSelf().executeDrawImage(image, drawReqVO);
86 86 return image.getId();
87 87 }
88 88  
89 89 @Async
90   - public void doDall(AiImageDO image, AiImageDrawReqVO req) {
  90 + public void executeDrawImage(AiImageDO image, AiImageDrawReqVO req) {
91 91 try {
92 92 // 1.1 构建请求
93 93 ImageOptions request = buildImageOptions(req);
94 94 // 1.2 执行请求
95   - ImageClient imageClient = aiClientFactory.getDefaultImageClient(AiPlatformEnum.validatePlatform(req.getPlatform()));
  95 + ImageClient imageClient = apiKeyService.getImageClient(AiPlatformEnum.validatePlatform(req.getPlatform()));
96 96 ImageResponse response = imageClient.call(new ImagePrompt(req.getPrompt(), request));
97 97  
98 98 // 2. 上传到文件服务
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
1 1 package cn.iocoder.yudao.module.ai.service.model;
2 2  
  3 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
5 6 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
6 7 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
7 8 import jakarta.validation.Valid;
8 9 import org.springframework.ai.chat.StreamingChatClient;
  10 +import org.springframework.ai.image.ImageClient;
9 11  
10 12 import java.util.List;
11 13  
... ... @@ -79,4 +81,14 @@ public interface AiApiKeyService {
79 81 */
80 82 StreamingChatClient getStreamingChatClient(Long id);
81 83  
  84 + /**
  85 + * 获得 ImageClient 对象
  86 + *
  87 + * TODO 可优化点:目前默认获取 platform 对应的第一个开启的配置用于绘画;后续可以支持配置选择
  88 + *
  89 + * @param platform 平台
  90 + * @return ImageClient 对象
  91 + */
  92 + ImageClient getImageClient(AiPlatformEnum platform);
  93 +
82 94 }
83 95 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
... ... @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
11 11 import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
12 12 import jakarta.annotation.Resource;
13 13 import org.springframework.ai.chat.StreamingChatClient;
  14 +import org.springframework.ai.image.ImageClient;
14 15 import org.springframework.stereotype.Service;
15 16 import org.springframework.validation.annotation.Validated;
16 17  
... ... @@ -101,4 +102,13 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
101 102 return clientFactory.getOrCreateStreamingChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
102 103 }
103 104  
  105 + @Override
  106 + public ImageClient getImageClient(AiPlatformEnum platform) {
  107 + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getName(), CommonStatusEnum.ENABLE.getStatus());
  108 + if (apiKey == null) {
  109 + return null;
  110 + }
  111 + return clientFactory.getOrCreateImageClient(platform, apiKey.getApiKey(), apiKey.getUrl());
  112 + }
  113 +
104 114 }
105 115 \ No newline at end of file
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java
... ... @@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.ai.core.factory;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4 4 import org.springframework.ai.chat.StreamingChatClient;
5   -import org.springframework.ai.chat.prompt.ChatOptions;
6 5 import org.springframework.ai.image.ImageClient;
7 6  
8 7 /**
... ... @@ -45,14 +44,15 @@ public interface AiClientFactory {
45 44 ImageClient getDefaultImageClient(AiPlatformEnum platform);
46 45  
47 46 /**
48   - * 创建 Chat 参数
  47 + * 基于指定配置,获得 ImageClient 对象
  48 + *
  49 + * 如果不存在,则进行创建
49 50 *
50 51 * @param platform 平台
51   - * @param model 模型
52   - * @param temperature 温度
53   - * @param maxTokens 生成的最大 Token
54   - * @return Chat 参数
  52 + * @param apiKey API KEY
  53 + * @param url API URL
  54 + * @return ImageClient 对象
55 55 */
56   - ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens);
  56 + ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url);
57 57  
58 58 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java
... ... @@ -11,29 +11,25 @@ import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties;
11 11 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
12 12 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
13 13 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
14   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
15 14 import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi;
16 15 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
17   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
18   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
19 16 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
20 17 import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
21   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
22 18 import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
23 19 import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
24 20 import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
25 21 import org.springframework.ai.chat.StreamingChatClient;
26   -import org.springframework.ai.chat.prompt.ChatOptions;
27 22 import org.springframework.ai.image.ImageClient;
28 23 import org.springframework.ai.ollama.OllamaChatClient;
29 24 import org.springframework.ai.ollama.api.OllamaApi;
30   -import org.springframework.ai.ollama.api.OllamaOptions;
31 25 import org.springframework.ai.openai.OpenAiChatClient;
32   -import org.springframework.ai.openai.OpenAiChatOptions;
33 26 import org.springframework.ai.openai.OpenAiImageClient;
34 27 import org.springframework.ai.openai.api.ApiUtils;
35 28 import org.springframework.ai.openai.api.OpenAiApi;
  29 +import org.springframework.ai.openai.api.OpenAiImageApi;
36 30 import org.springframework.ai.stabilityai.StabilityAiImageClient;
  31 +import org.springframework.ai.stabilityai.api.StabilityAiApi;
  32 +import org.springframework.web.client.RestClient;
37 33  
38 34 import java.util.List;
39 35  
... ... @@ -100,36 +96,26 @@ public class AiClientFactoryImpl implements AiClientFactory {
100 96 }
101 97 }
102 98  
103   - private static String buildClientCacheKey(Class<?> clazz, Object... params) {
104   - if (ArrayUtil.isEmpty(params)) {
105   - return clazz.getName();
106   - }
107   - return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
108   - }
109   -
110 99 @Override
111   - public ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
112   - Float temperatureF = temperature != null ? temperature.floatValue() : null;
  100 + public ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url) {
113 101 //noinspection EnhancedSwitchMigration
114 102 switch (platform) {
115 103 case OPENAI:
116   - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
117   - case OLLAMA:
118   - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
119   - case YI_YAN:
120   - // TODO @fan:增加一个 model
121   - return new YiYanChatOptions().setTemperature(temperatureF).setMaxOutputTokens(maxTokens);
122   - case XING_HUO:
123   - return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
124   - .setMaxTokens(maxTokens);
125   - case QIAN_WEN:
126   - // TODO @fan:增加 model、temperature 参数
127   - return new QianWenOptions().setMaxTokens(maxTokens);
  104 + return buildOpenAiImageClient(apiKey, url);
  105 + case STABLE_DIFFUSION:
  106 + return buildStabilityAiImageClient(apiKey, url);
128 107 default:
129 108 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
130 109 }
131 110 }
132 111  
  112 + private static String buildClientCacheKey(Class<?> clazz, Object... params) {
  113 + if (ArrayUtil.isEmpty(params)) {
  114 + return clazz.getName();
  115 + }
  116 + return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
  117 + }
  118 +
133 119 // ========== 各种创建 spring-ai 客户端的方法 ==========
134 120  
135 121 /**
... ... @@ -182,7 +168,6 @@ public class AiClientFactoryImpl implements AiClientFactory {
182 168 return new QianWenChatClient(qianWenApi);
183 169 }
184 170  
185   -
186 171 // private static VertexAiGeminiChatClient buildGoogleGemir(String key) {
187 172 // List<String> keys = StrUtil.split(key, '|');
188 173 // Assert.equals(keys.size(), 2, "VertexAiGeminiChatClient 的密钥需要 (projectId|location) 格式");
... ... @@ -190,4 +175,16 @@ public class AiClientFactoryImpl implements AiClientFactory {
190 175 // return new VertexAiGeminiChatClient(vertexApi);
191 176 // }
192 177  
  178 + private ImageClient buildOpenAiImageClient(String openAiToken, String url) {
  179 + url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL);
  180 + OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder());
  181 + return new OpenAiImageClient(openAiApi);
  182 + }
  183 +
  184 + private ImageClient buildStabilityAiImageClient(String apiKey, String url) {
  185 + url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL);
  186 + StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url);
  187 + return new StabilityAiImageClient(stabilityAiApi);
  188 + }
  189 +
193 190 }
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -161,7 +161,6 @@ spring:
161 161 project-id: 1 # TODO 芋艿:缺配置
162 162 location: 2
163 163  
164   -
165 164 yudao.ai:
166 165 yiyan:
167 166 enable: true
... ... @@ -193,11 +192,6 @@ yudao.ai:
193 192 topP: 0.8
194 193 topK: 0
195 194 api-key: sk-Zsd81gZYg7
196   - openAiImage:
197   - enable: true
198   - api-key: ${OPEN_AI_KEY}
199   - model: dall_e_2
200   - style: vivid
201 195 midjourney:
202 196 enable: true
203 197 token: MTE4MjE3MjY2MjkxNTY3ODIzOA.GEV1SG.c49F8lZoGCUHwsj8O0UdodmM6nyQHvuD2fXflw
... ... @@ -206,6 +200,7 @@ yudao.ai:
206 200 suno:
207 201 enable: true
208 202 token: 16b4356581984d538652354b60d69ff0
  203 +
209 204 --- #################### 芋道相关配置 ####################
210 205  
211 206 yudao:
... ...