Commit 2fefcf8834ede9c73fbd64a16d04fe2ffd25a425
1 parent
cad1ce48
【新增】AI:通过 AiClientFactory 提供 chatclient
Showing
12 changed files
with
289 additions
and
108 deletions
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
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: |