Commit 2fefcf8834ede9c73fbd64a16d04fe2ffd25a425

Authored by YunaiV
1 parent cad1ce48

【新增】AI:通过 AiClientFactory 提供 chatclient

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/config/AiChatClientFactory.java deleted
1 -package cn.iocoder.yudao.module.ai.config;  
2 -  
3 -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;  
4 -import org.springframework.ai.chat.ChatClient;  
5 -import org.springframework.ai.chat.StreamingChatClient;  
6 -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;  
7 -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;  
8 -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;  
9 -import org.springframework.ai.ollama.OllamaChatClient;  
10 -import org.springframework.ai.openai.OpenAiChatClient;  
11 -import org.springframework.beans.factory.annotation.Autowired;  
12 -import org.springframework.context.ApplicationContext;  
13 -import org.springframework.stereotype.Component;  
14 -  
15 -/**  
16 - * factory  
17 - *  
18 - * @author fansili  
19 - * @time 2024/4/25 17:36  
20 - * @since 1.0  
21 - */  
22 -@Component  
23 -public class AiChatClientFactory {  
24 -  
25 - @Autowired  
26 - private ApplicationContext applicationContext;  
27 -  
28 - public ChatClient getChatClient(AiPlatformEnum platformEnum) {  
29 - if (AiPlatformEnum.QIAN_WEN == platformEnum) {  
30 - return applicationContext.getBean(QianWenChatClient.class);  
31 - } else if (AiPlatformEnum.YI_YAN == platformEnum) {  
32 - return applicationContext.getBean(YiYanChatClient.class);  
33 - } else if (AiPlatformEnum.XING_HUO == platformEnum) {  
34 - return applicationContext.getBean(XingHuoChatClient.class);  
35 - }  
36 - throw new IllegalArgumentException("不支持的 chat client!");  
37 - }  
38 -  
39 - // TODO yunai 要不再加一个接口,让他们拥有 ChatClient、StreamingChatClient 功能  
40 - public StreamingChatClient getStreamingChatClient(AiPlatformEnum platformEnum) {  
41 -// if (true) {  
42 -// return applicationContext.getBean(OllamaChatClient.class);  
43 -// }  
44 - if (AiPlatformEnum.QIAN_WEN == platformEnum) {  
45 - return applicationContext.getBean(QianWenChatClient.class);  
46 - } else if (AiPlatformEnum.YI_YAN == platformEnum) {  
47 - return applicationContext.getBean(YiYanChatClient.class);  
48 - } else if (AiPlatformEnum.XING_HUO == platformEnum) {  
49 - return applicationContext.getBean(XingHuoChatClient.class);  
50 - } else if (AiPlatformEnum.OLLAMA == platformEnum) {  
51 - return applicationContext.getBean(OllamaChatClient.class);  
52 - } else if (AiPlatformEnum.OPENAI == platformEnum) {  
53 - return applicationContext.getBean(OpenAiChatClient.class);  
54 - }  
55 - throw new IllegalArgumentException("不支持的 chat client!");  
56 - }  
57 -}  
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/impl/AiChatServiceImpl.java
@@ -4,13 +4,20 @@ import cn.hutool.core.collection.CollUtil; @@ -4,13 +4,20 @@ import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.ObjUtil; 4 import cn.hutool.core.util.ObjUtil;
5 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
6 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; 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.common.util.object.BeanUtils; 8 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8 -import cn.iocoder.yudao.module.ai.config.AiChatClientFactory; 9 +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
  10 +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
  11 +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
  12 +import jakarta.annotation.Resource;
  13 +import org.springframework.ai.chat.ChatResponse;
  14 +import org.springframework.ai.chat.StreamingChatClient;
  15 +import org.springframework.ai.chat.messages.*;
  16 +import org.springframework.ai.chat.prompt.ChatOptions;
  17 +import org.springframework.ai.chat.prompt.Prompt;
9 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO; 18 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO;
10 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; 19 import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
11 -import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;  
12 import cn.iocoder.yudao.module.ai.convert.AiChatMessageConvert; 20 import cn.iocoder.yudao.module.ai.convert.AiChatMessageConvert;
13 -import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;  
14 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; 21 import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
15 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; 22 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
16 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; 23 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
@@ -19,12 +26,7 @@ import cn.iocoder.yudao.module.ai.service.AiChatService; @@ -19,12 +26,7 @@ import cn.iocoder.yudao.module.ai.service.AiChatService;
19 import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; 26 import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
20 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; 27 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
21 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; 28 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
22 -import lombok.AllArgsConstructor;  
23 import lombok.extern.slf4j.Slf4j; 29 import lombok.extern.slf4j.Slf4j;
24 -import org.springframework.ai.chat.ChatResponse;  
25 -import org.springframework.ai.chat.StreamingChatClient;  
26 -import org.springframework.ai.chat.messages.*;  
27 -import org.springframework.ai.chat.prompt.Prompt;  
28 import org.springframework.stereotype.Service; 30 import org.springframework.stereotype.Service;
29 import org.springframework.transaction.annotation.Transactional; 31 import org.springframework.transaction.annotation.Transactional;
30 import reactor.core.publisher.Flux; 32 import reactor.core.publisher.Flux;
@@ -46,16 +48,22 @@ import static cn.iocoder.yudao.module.ai.ErrorCodeConstants.CHAT_CONVERSATION_NO @@ -46,16 +48,22 @@ import static cn.iocoder.yudao.module.ai.ErrorCodeConstants.CHAT_CONVERSATION_NO
46 */ 48 */
47 @Slf4j 49 @Slf4j
48 @Service 50 @Service
49 -@AllArgsConstructor  
50 public class AiChatServiceImpl implements AiChatService { 51 public class AiChatServiceImpl implements AiChatService {
51 52
52 - private final AiChatClientFactory chatClientFactory; 53 + @Resource
  54 + private AiChatMessageMapper chatMessageMapper;
53 55
54 - private final AiChatMessageMapper chatMessageMapper; 56 + @Resource
  57 + private AiClientFactory clientFactory;
55 58
56 - private final AiChatConversationService chatConversationService;  
57 - private final AiChatModelService chatModalService;  
58 - private final AiChatRoleService chatRoleService; 59 + @Resource
  60 + private AiChatConversationService chatConversationService;
  61 + @Resource
  62 + private AiChatModelService chatModalService;
  63 + @Resource
  64 + private AiChatRoleService chatRoleService;
  65 + @Resource
  66 + private AiApiKeyService apiKeyService;
59 67
60 @Transactional(rollbackFor = Exception.class) 68 @Transactional(rollbackFor = Exception.class)
61 public AiChatMessageRespVO chat(AiChatMessageSendReqVO req) { 69 public AiChatMessageRespVO chat(AiChatMessageSendReqVO req) {
@@ -106,8 +114,7 @@ public class AiChatServiceImpl implements AiChatService { @@ -106,8 +114,7 @@ public class AiChatServiceImpl implements AiChatService {
106 List<AiChatMessageDO> historyMessages = chatMessageMapper.selectByConversationId(conversation.getId()); 114 List<AiChatMessageDO> historyMessages = chatMessageMapper.selectByConversationId(conversation.getId());
107 // 1.2 校验模型 115 // 1.2 校验模型
108 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); 116 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
109 - AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());  
110 - StreamingChatClient chatClient = chatClientFactory.getStreamingChatClient(platform); 117 + StreamingChatClient chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
111 118
112 // 2. 插入 user 发送消息 119 // 2. 插入 user 发送消息
113 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, 120 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
@@ -118,13 +125,13 @@ public class AiChatServiceImpl implements AiChatService { @@ -118,13 +125,13 @@ public class AiChatServiceImpl implements AiChatService {
118 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); 125 userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
119 126
120 // 3.2 创建 chat 需要的 Prompt 127 // 3.2 创建 chat 需要的 Prompt
121 - Prompt prompt = buildPrompt(conversation, historyMessages, sendReqVO); 128 + Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO);
122 Flux<ChatResponse> streamResponse = chatClient.stream(prompt); 129 Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
123 130
124 // 3.3 流式返回 131 // 3.3 流式返回
125 // 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 132 // 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
126 StringBuffer contentBuffer = new StringBuffer(); 133 StringBuffer contentBuffer = new StringBuffer();
127 - return streamResponse.publishOn(Schedulers.immediate()).map(chunk -> { 134 + return streamResponse.publishOn(Schedulers.single()).map(chunk -> {
128 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; 135 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null;
129 newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 136 newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
130 contentBuffer.append(newContent); 137 contentBuffer.append(newContent);
@@ -144,7 +151,8 @@ public class AiChatServiceImpl implements AiChatService { @@ -144,7 +151,8 @@ public class AiChatServiceImpl implements AiChatService {
144 return chatMessageMapper.deleteByConversationId(conversationId) > 0; 151 return chatMessageMapper.deleteByConversationId(conversationId) > 0;
145 } 152 }
146 153
147 - private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, AiChatMessageSendReqVO sendReqVO) { 154 + private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
  155 + AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) {
148 // 1. 构建 Prompt Message 列表 156 // 1. 构建 Prompt Message 列表
149 List<Message> chatMessages = new ArrayList<>(); 157 List<Message> chatMessages = new ArrayList<>();
150 // 1.1 system context 角色设定 158 // 1.1 system context 角色设定
@@ -156,10 +164,11 @@ public class AiChatServiceImpl implements AiChatService { @@ -156,10 +164,11 @@ public class AiChatServiceImpl implements AiChatService {
156 chatMessages.add(new UserMessage(sendReqVO.getContent())); 164 chatMessages.add(new UserMessage(sendReqVO.getContent()));
157 165
158 // 2. 构建 ChatOptions 对象 TODO 芋艿:临时注释掉;等文心一言兼容了; 166 // 2. 构建 ChatOptions 对象 TODO 芋艿:临时注释掉;等文心一言兼容了;
159 - // TODO 每一轮 token 数量  
160 -// ChatOptions chatOptions = ChatOptionsBuilder.builder().withTemperature(conversation.getTemperature().floatValue()).build();  
161 -// return new Prompt(chatMessages, null);  
162 - return new Prompt(chatMessages); 167 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
  168 + ChatOptions chatOptions = clientFactory.buildChatOptions(platform, model.getModel(),
  169 + conversation.getTemperature(), conversation.getMaxTokens());
  170 + return new Prompt(chatMessages, chatOptions);
  171 +// return new Prompt(chatMessages);
163 } 172 }
164 173
165 /** 174 /**
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageR @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageR
5 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; 5 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
6 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; 6 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
7 import jakarta.validation.Valid; 7 import jakarta.validation.Valid;
  8 +import org.springframework.ai.chat.StreamingChatClient;
8 9
9 import java.util.List; 10 import java.util.List;
10 11
@@ -68,4 +69,14 @@ public interface AiApiKeyService { @@ -68,4 +69,14 @@ public interface AiApiKeyService {
68 */ 69 */
69 List<AiApiKeyDO> getApiKeyList(); 70 List<AiApiKeyDO> getApiKeyList();
70 71
  72 + // ========== 与 spring-ai 集成 ==========
  73 +
  74 + /**
  75 + * 获得 StreamingChatClient 对象
  76 + *
  77 + * @param id 编号
  78 + * @return StreamingChatClient 对象
  79 + */
  80 + StreamingChatClient getStreamingChatClient(Long id);
  81 +
71 } 82 }
72 \ No newline at end of file 83 \ 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
1 package cn.iocoder.yudao.module.ai.service.model; 1 package cn.iocoder.yudao.module.ai.service.model;
2 2
  3 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  4 +import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -8,6 +10,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR @@ -8,6 +10,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR
8 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; 10 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
9 import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper; 11 import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
10 import jakarta.annotation.Resource; 12 import jakarta.annotation.Resource;
  13 +import org.springframework.ai.chat.StreamingChatClient;
11 import org.springframework.stereotype.Service; 14 import org.springframework.stereotype.Service;
12 import org.springframework.validation.annotation.Validated; 15 import org.springframework.validation.annotation.Validated;
13 16
@@ -28,6 +31,9 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { @@ -28,6 +31,9 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
28 @Resource 31 @Resource
29 private AiApiKeyMapper apiKeyMapper; 32 private AiApiKeyMapper apiKeyMapper;
30 33
  34 + @Resource
  35 + private AiClientFactory clientFactory;
  36 +
31 @Override 37 @Override
32 public Long createApiKey(AiApiKeySaveReqVO createReqVO) { 38 public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
33 // 插入 39 // 插入
@@ -86,4 +92,13 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { @@ -86,4 +92,13 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
86 return apiKeyMapper.selectList(); 92 return apiKeyMapper.selectList();
87 } 93 }
88 94
  95 + // ========== 与 spring-ai 集成 ==========
  96 +
  97 + @Override
  98 + public StreamingChatClient getStreamingChatClient(Long id) {
  99 + AiApiKeyDO apiKey = validateApiKey(id);
  100 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
  101 + return clientFactory.getOrCreateStreamingChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
  102 + }
  103 +
89 } 104 }
90 \ No newline at end of file 105 \ No newline at end of file
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
1 package cn.iocoder.yudao.framework.ai.config; 1 package cn.iocoder.yudao.framework.ai.config;
2 2
3 import cn.hutool.core.io.IoUtil; 3 import cn.hutool.core.io.IoUtil;
  4 +import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
  5 +import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl;
4 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient; 6 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
5 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal; 7 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
6 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions; 8 import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
@@ -36,18 +38,23 @@ import java.util.HashMap; @@ -36,18 +38,23 @@ import java.util.HashMap;
36 import java.util.Map; 38 import java.util.Map;
37 39
38 /** 40 /**
39 - * ai 自动配置 41 + * 芋道 AI 自动配置
40 * 42 *
41 * @author fansili 43 * @author fansili
42 - * @time 2024/4/12 16:29  
43 - * @since 1.0  
44 */ 44 */
45 -@Slf4j  
46 @AutoConfiguration 45 @AutoConfiguration
47 @EnableConfigurationProperties(YudaoAiProperties.class) 46 @EnableConfigurationProperties(YudaoAiProperties.class)
  47 +@Slf4j
48 public class YudaoAiAutoConfiguration { 48 public class YudaoAiAutoConfiguration {
49 49
50 @Bean 50 @Bean
  51 + public AiClientFactory aiClientFactory() {
  52 + return new AiClientFactoryImpl();
  53 + }
  54 +
  55 + // ========== 各种 AI Client 创建 ==========
  56 +
  57 + @Bean
51 @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") 58 @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
52 public XingHuoChatClient xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { 59 public XingHuoChatClient xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
53 YudaoAiProperties.XingHuoProperties xingHuoProperties = yudaoAiProperties.getXinghuo(); 60 YudaoAiProperties.XingHuoProperties xingHuoProperties = yudaoAiProperties.getXinghuo();
@@ -108,21 +115,6 @@ public class YudaoAiAutoConfiguration { @@ -108,21 +115,6 @@ public class YudaoAiAutoConfiguration {
108 } 115 }
109 116
110 @Bean 117 @Bean
111 - @ConditionalOnProperty(value = "yudao.ai.openAiImage.enable", havingValue = "true")  
112 - public OpenAiImageClient openAiImageClient(YudaoAiProperties yudaoAiProperties) {  
113 - YudaoAiProperties.OpenAiImageProperties openAiImageProperties = yudaoAiProperties.getOpenAiImage();  
114 - OpenAiImageOptions openAiImageOptions = new OpenAiImageOptions();  
115 - openAiImageOptions.setModel(openAiImageProperties.getModel().getModel());  
116 - openAiImageOptions.setStyle(openAiImageProperties.getStyle().getStyle());  
117 - openAiImageOptions.setResponseFormat("url"); // TODO 芋艿:OpenAiImageOptions.ResponseFormatEnum.URL.getValue()  
118 - // 创建 client  
119 - return new OpenAiImageClient(  
120 - new OpenAiImageApi(openAiImageProperties.getApiKey()),  
121 - openAiImageOptions,  
122 - RetryUtils.DEFAULT_RETRY_TEMPLATE);  
123 - }  
124 -  
125 - @Bean  
126 @ConditionalOnMissingBean(value = MidjourneyMessageHandler.class) 118 @ConditionalOnMissingBean(value = MidjourneyMessageHandler.class)
127 public MidjourneyMessageHandler defaultMidjourneyMessageHandler() { 119 public MidjourneyMessageHandler defaultMidjourneyMessageHandler() {
128 // 如果没有实现 MidjourneyMessageHandler 默认注入一个 120 // 如果没有实现 MidjourneyMessageHandler 默认注入一个
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.core.factory;
  2 +
  3 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  4 +import org.springframework.ai.chat.StreamingChatClient;
  5 +import org.springframework.ai.chat.prompt.ChatOptions;
  6 +
  7 +/**
  8 + * AI 客户端工厂的接口类
  9 + *
  10 + * @author fansili
  11 + */
  12 +public interface AiClientFactory {
  13 +
  14 + /**
  15 + * 基于指定配置,获得 StreamingChatClient 对象
  16 + *
  17 + * 如果不存在,则进行创建
  18 + *
  19 + * @param platform 平台
  20 + * @param apiKey API KEY
  21 + * @param url API URL
  22 + * @return StreamingChatClient 对象
  23 + */
  24 + StreamingChatClient getOrCreateStreamingChatClient(AiPlatformEnum platform, String apiKey, String url);
  25 +
  26 + /**
  27 + * 基于默认配置,获得 StreamingChatClient 对象
  28 + *
  29 + * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置
  30 + *
  31 + * @param platform 平台
  32 + * @return StreamingChatClient 对象
  33 + */
  34 + StreamingChatClient getDefaultStreamingChatClient(AiPlatformEnum platform);
  35 +
  36 + /**
  37 + * 创建 Chat 参数
  38 + *
  39 + * @param platform 平台
  40 + * @param model 模型
  41 + * @param temperature 温度
  42 + * @param maxTokens 生成的最大 Token
  43 + * @return Chat 参数
  44 + */
  45 + ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens);
  46 +
  47 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.core.factory;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.lang.Singleton;
  5 +import cn.hutool.core.lang.func.Func0;
  6 +import cn.hutool.core.util.ArrayUtil;
  7 +import cn.hutool.core.util.StrUtil;
  8 +import cn.hutool.extra.spring.SpringUtil;
  9 +import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration;
  10 +import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties;
  11 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  12 +import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
  13 +import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
  14 +import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
  15 +import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi;
  16 +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 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
  20 +import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
  21 +import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
  22 +import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
  23 +import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
  24 +import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
  25 +import org.springframework.ai.chat.StreamingChatClient;
  26 +import org.springframework.ai.chat.prompt.ChatOptions;
  27 +import org.springframework.ai.ollama.OllamaChatClient;
  28 +import org.springframework.ai.ollama.api.OllamaApi;
  29 +import org.springframework.ai.ollama.api.OllamaOptions;
  30 +import org.springframework.ai.openai.OpenAiChatClient;
  31 +import org.springframework.ai.openai.OpenAiChatOptions;
  32 +import org.springframework.ai.openai.api.ApiUtils;
  33 +import org.springframework.ai.openai.api.OpenAiApi;
  34 +
  35 +import java.util.List;
  36 +
  37 +/**
  38 + * AI 客户端工厂的实现类
  39 + *
  40 + * @author 芋道源码
  41 + */
  42 +public class AiClientFactoryImpl implements AiClientFactory {
  43 +
  44 + @Override
  45 + public StreamingChatClient getOrCreateStreamingChatClient(AiPlatformEnum platform, String apiKey, String url) {
  46 + String cacheKey = buildClientCacheKey(StreamingChatClient.class, platform, apiKey, url);
  47 + return Singleton.get(cacheKey, (Func0<StreamingChatClient>) () -> {
  48 + //noinspection EnhancedSwitchMigration
  49 + switch (platform) {
  50 + case OPENAI:
  51 + return buildOpenAiChatClient(apiKey, url);
  52 + case OLLAMA:
  53 + return buildOllamaChatClient(url);
  54 + case YI_YAN:
  55 + return buildYiYanChatClient(apiKey);
  56 + case XING_HUO:
  57 + return buildXingHuoChatClient(apiKey);
  58 + case QIAN_WEN:
  59 + return buildQianWenChatClient(apiKey);
  60 + default:
  61 + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
  62 + }
  63 + });
  64 + }
  65 +
  66 + @Override
  67 + public StreamingChatClient getDefaultStreamingChatClient(AiPlatformEnum platform) {
  68 + //noinspection EnhancedSwitchMigration
  69 + switch (platform) {
  70 + case OPENAI:
  71 + return SpringUtil.getBean(OpenAiChatClient.class);
  72 + case OLLAMA:
  73 + return SpringUtil.getBean(OllamaChatClient.class);
  74 + case YI_YAN:
  75 + return SpringUtil.getBean(YiYanChatClient.class);
  76 + case XING_HUO:
  77 + return SpringUtil.getBean(XingHuoChatClient.class);
  78 + case QIAN_WEN:
  79 + return SpringUtil.getBean(QianWenChatClient.class);
  80 + default:
  81 + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
  82 + }
  83 + }
  84 +
  85 + private static String buildClientCacheKey(Class<?> clazz, Object... params) {
  86 + if (ArrayUtil.isEmpty(params)) {
  87 + return clazz.getName();
  88 + }
  89 + return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
  90 + }
  91 +
  92 + @Override
  93 + public ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
  94 + Float temperatureF = temperature != null ? temperature.floatValue() : null;
  95 + //noinspection EnhancedSwitchMigration
  96 + switch (platform) {
  97 + case OPENAI:
  98 + return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  99 + case OLLAMA:
  100 + return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
  101 + case YI_YAN:
  102 + // TODO @fan:增加一个 model
  103 + return new YiYanChatOptions().setTemperature(temperatureF).setMaxOutputTokens(maxTokens);
  104 + case XING_HUO:
  105 + return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
  106 + .setMaxTokens(maxTokens);
  107 + case QIAN_WEN:
  108 + // TODO @fan:增加 model、temperature 参数
  109 + return new QianWenOptions().setMaxTokens(maxTokens);
  110 + default:
  111 + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
  112 + }
  113 + }
  114 +
  115 + // ========== 各种创建 spring-ai 客户端的方法 ==========
  116 +
  117 + /**
  118 + * 可参考 {@link OpenAiAutoConfiguration}
  119 + */
  120 + private static OpenAiChatClient buildOpenAiChatClient(String openAiToken, String url) {
  121 + url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL);
  122 + OpenAiApi openAiApi = new OpenAiApi(url, openAiToken);
  123 + return new OpenAiChatClient(openAiApi);
  124 + }
  125 +
  126 + /**
  127 + * 可参考 {@link OllamaAutoConfiguration}
  128 + */
  129 + private static OllamaChatClient buildOllamaChatClient(String url) {
  130 + OllamaApi ollamaApi = new OllamaApi(url);
  131 + return new OllamaChatClient(ollamaApi);
  132 + }
  133 +
  134 + /**
  135 + * 可参考 {@link YudaoAiAutoConfiguration#yiYanChatClient(YudaoAiProperties)}
  136 + */
  137 + private static YiYanChatClient buildYiYanChatClient(String key) {
  138 + List<String> keys = StrUtil.split(key, '|');
  139 + Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
  140 + String appKey = keys.get(0);
  141 + String secretKey = keys.get(1);
  142 + YiYanApi yiYanApi = new YiYanApi(appKey, secretKey, YiYanApi.DEFAULT_CHAT_MODEL, 0);
  143 + return new YiYanChatClient(yiYanApi);
  144 + }
  145 +
  146 + /**
  147 + * 可参考 {@link YudaoAiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)}
  148 + */
  149 + private static XingHuoChatClient buildXingHuoChatClient(String key) {
  150 + List<String> keys = StrUtil.split(key, '|');
  151 + Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
  152 + String appId = keys.get(0);
  153 + String appKey = keys.get(1);
  154 + String secretKey = keys.get(2);
  155 + XingHuoApi xingHuoApi = new XingHuoApi(appId, appKey, secretKey);
  156 + return new XingHuoChatClient(xingHuoApi);
  157 + }
  158 +
  159 + /**
  160 + * 可参考 {@link YudaoAiAutoConfiguration#qianWenChatClient(YudaoAiProperties)}
  161 + */
  162 + private static QianWenChatClient buildQianWenChatClient(String key) {
  163 + QianWenApi qianWenApi = new QianWenApi(key, QianWenChatModal.QWEN_72B_CHAT);
  164 + return new QianWenChatClient(qianWenApi);
  165 + }
  166 +
  167 +}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/QianWenOptions.java
@@ -6,6 +6,8 @@ import lombok.experimental.Accessors; @@ -6,6 +6,8 @@ import lombok.experimental.Accessors;
6 6
7 import java.util.List; 7 import java.util.List;
8 8
  9 +// TODO @fan:增加一个 model 参数
  10 +// TODO @fan:增加一个 Temperature 参数
9 /** 11 /**
10 * 阿里云 千问 属性 12 * 阿里云 千问 属性
11 * 13 *
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoOptions.java
@@ -14,6 +14,7 @@ import lombok.experimental.Accessors; @@ -14,6 +14,7 @@ import lombok.experimental.Accessors;
14 @Accessors(chain = true) 14 @Accessors(chain = true)
15 public class XingHuoOptions implements ChatOptions { 15 public class XingHuoOptions implements ChatOptions {
16 16
  17 + // TODO @fan:这里 model 参数,然后使用 string
17 /** 18 /**
18 * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E 19 * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
19 * <p> 20 * <p>
@@ -43,7 +44,6 @@ public class XingHuoOptions implements ChatOptions { @@ -43,7 +44,6 @@ public class XingHuoOptions implements ChatOptions {
43 */ 44 */
44 private String chatId; 45 private String chatId;
45 46
46 -  
47 @Override 47 @Override
48 public Float getTemperature() { 48 public Float getTemperature() {
49 return this.temperature; 49 return this.temperature;
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/YiYanChatOptions.java
@@ -6,6 +6,7 @@ import org.springframework.ai.chat.prompt.ChatOptions; @@ -6,6 +6,7 @@ import org.springframework.ai.chat.prompt.ChatOptions;
6 6
7 import java.util.List; 7 import java.util.List;
8 8
  9 +// TODO @fan:增加一个 model
9 // TODO @fan:字段命名,penalty_score 类似的,建议改成驼峰原则 10 // TODO @fan:字段命名,penalty_score 类似的,建议改成驼峰原则
10 // TODO @fan:字段的注释,可以都删除掉,让用户 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t 即可 11 // TODO @fan:字段的注释,可以都删除掉,让用户 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t 即可
11 /** 12 /**
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/api/YiYanApi.java
@@ -18,7 +18,7 @@ public class YiYanApi { @@ -18,7 +18,7 @@ public class YiYanApi {
18 18
19 private static final String AUTH_2_TOKEN_URI = "/oauth/2.0/token"; 19 private static final String AUTH_2_TOKEN_URI = "/oauth/2.0/token";
20 20
21 - public static final String DEFAULT_CHAT_MODEL = YiYanChatModel.ERNIE4_0.getModel(); 21 + public static final YiYanChatModel DEFAULT_CHAT_MODEL = YiYanChatModel.ERNIE4_0;
22 22
23 private final String appKey; 23 private final String appKey;
24 private final String secretKey; 24 private final String secretKey;
@@ -39,6 +39,7 @@ public class YiYanApi { @@ -39,6 +39,7 @@ public class YiYanApi {
39 */ 39 */
40 private final YiYanChatModel useChatModel; 40 private final YiYanChatModel useChatModel;
41 41
  42 + // TODO fan:看看是不是去掉 refreshTokenSecondTime 字段
42 public YiYanApi(String appKey, String secretKey, YiYanChatModel useChatModel, int refreshTokenSecondTime) { 43 public YiYanApi(String appKey, String secretKey, YiYanChatModel useChatModel, int refreshTokenSecondTime) {
43 this.appKey = appKey; 44 this.appKey = appKey;
44 this.secretKey = secretKey; 45 this.secretKey = secretKey;
yudao-server/src/main/resources/application.yaml
@@ -150,15 +150,8 @@ spring.ai: @@ -150,15 +150,8 @@ spring.ai:
150 chat: 150 chat:
151 model: llama3 151 model: llama3
152 openai: 152 openai:
153 -# api-key: sk-QmgIIPc5xiYd8lPb076b1b7774Ea49Af9eD2Ef172c8f7e43  
154 -# base-url: https://openkey.cloud  
155 -# api-key: sk-gkgfYxhX9FxyZJznwxRZSJwKeGQYNPDVWjhby2PRRf17GHeT  
156 -# base-url: https://api.chatanywhere.tech  
157 api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z 153 api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z
158 base-url: https://api.gptsapi.net 154 base-url: https://api.gptsapi.net
159 -# chat:  
160 -# options:  
161 -# model: gpt-4-0125-preview  
162 155
163 yudao.ai: 156 yudao.ai:
164 yiyan: 157 yiyan: