From e0f08a0f02b5bb53b7f3e6201469eb3a5767bf40 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 6 Jul 2024 12:54:23 +0800 Subject: [PATCH] 【代码优化】AI:将 ChatClient 替换成 ChatModel,和 Spring AI 对齐 --- yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java | 8 ++++---- yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java | 4 ++-- yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java | 8 ++++---- yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java | 16 ++++++++-------- yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java | 4 ++-- yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java | 8 ++++---- yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java | 82 ---------------------------------------------------------------------------------- yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java | 215 ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 324 insertions(+), 321 deletions(-) delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index 44b48a6..6c8cdea 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -70,7 +70,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); // 1.2 校验模型 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); - ChatModel chatClient = apiKeyService.getChatClient(model.getKeyId()); + ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); // 2. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, @@ -82,7 +82,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 3.2 创建 chat 需要的 Prompt Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO); - ChatResponse chatResponse = chatClient.call(prompt); + ChatResponse chatResponse = chatModel.call(prompt); // 3.3 段式返回 String newContent = chatResponse.getResult().getOutput().getContent(); @@ -101,7 +101,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); // 1.2 校验模型 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); - StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId()); + StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); // 2. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, @@ -113,7 +113,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 3.2 创建 chat 需要的 Prompt Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO); - Flux streamResponse = chatClient.stream(prompt); + Flux streamResponse = chatModel.stream(prompt); // 3.3 流式返回 // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index d3054d8..27c0978 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -98,8 +98,8 @@ public class AiImageServiceImpl implements AiImageService { // 1.1 构建请求 ImageOptions request = buildImageOptions(req); // 1.2 执行请求 - ImageModel imageClient = apiKeyService.getImageClient(AiPlatformEnum.validatePlatform(req.getPlatform())); - ImageResponse response = imageClient.call(new ImagePrompt(req.getPrompt(), request)); + ImageModel imageModel = apiKeyService.getImageModel(AiPlatformEnum.validatePlatform(req.getPlatform())); + ImageResponse response = imageModel.call(new ImagePrompt(req.getPrompt(), request)); // 2. 上传到文件服务 byte[] fileContent = Base64.decode(response.getResult().getOutput().getB64Json()); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java index a5ba608..fe8fdd1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java @@ -81,17 +81,17 @@ public interface AiApiKeyService { * @param id 编号 * @return ChatModel 对象 */ - ChatModel getChatClient(Long id); + ChatModel getChatModel(Long id); /** - * 获得 ImageClient 对象 + * 获得 ImageModel 对象 * * TODO 可优化点:目前默认获取 platform 对应的第一个开启的配置用于绘画;后续可以支持配置选择 * * @param platform 平台 - * @return ImageClient 对象 + * @return ImageModel 对象 */ - ImageModel getImageClient(AiPlatformEnum platform); + ImageModel getImageModel(AiPlatformEnum platform); /** * 获得 MidjourneyApi 对象 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java index 8db777f..6f8f060 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.ai.service.model; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -35,7 +35,7 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { private AiApiKeyMapper apiKeyMapper; @Resource - private AiClientFactory clientFactory; + private AiModelFactory modelFactory; @Override public Long createApiKey(AiApiKeySaveReqVO createReqVO) { @@ -98,19 +98,19 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { // ========== 与 spring-ai 集成 ========== @Override - public ChatModel getChatClient(Long id) { + public ChatModel getChatModel(Long id) { AiApiKeyDO apiKey = validateApiKey(id); AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); - return clientFactory.getOrCreateChatClient(platform, apiKey.getApiKey(), apiKey.getUrl()); + return modelFactory.getOrCreateChatClient(platform, apiKey.getApiKey(), apiKey.getUrl()); } @Override - public ImageModel getImageClient(AiPlatformEnum platform) { + public ImageModel getImageModel(AiPlatformEnum platform) { AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getName(), CommonStatusEnum.ENABLE.getStatus()); if (apiKey == null) { throw exception(API_KEY_IMAGE_NODE_FOUND, platform.getName()); } - return clientFactory.getOrCreateImageClient(platform, apiKey.getApiKey(), apiKey.getUrl()); + return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl()); } @Override @@ -120,7 +120,7 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { if (apiKey == null) { throw exception(API_KEY_MIDJOURNEY_NOT_FOUND); } - return clientFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl()); + return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl()); } @Override @@ -130,7 +130,7 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { if (apiKey == null) { throw exception(API_KEY_SUNO_NOT_FOUND); } - return clientFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); + return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java index 0051e4a..d43c11d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -54,7 +54,7 @@ public class AiWriteServiceImpl implements AiWriteService { public Flux> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) { // 1.1 校验模型 TODO 芋艿 是不是取默认的模型也ok?;那可以,有限拿 chatRole 的角色;如果没有,则获取默认的; AiChatModelDO model = chatModalService.getRequiredDefaultChatModel(); - StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId()); + StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); // 1.2 插入写作信息 @@ -65,7 +65,7 @@ public class AiWriteServiceImpl implements AiWriteService { // 2.1 构建提示词 ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens()); Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions); - Flux streamResponse = chatClient.stream(prompt); + Flux streamResponse = chatModel.stream(prompt); // 2.2 流式返回 StringBuffer contentBuffer = new StringBuffer(); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index b8419c8..128d9d9 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.framework.ai.config; -import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory; -import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl; +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatClient; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; @@ -28,8 +28,8 @@ import org.springframework.context.annotation.Import; public class YudaoAiAutoConfiguration { @Bean - public AiClientFactory aiClientFactory() { - return new AiClientFactoryImpl(); + public AiModelFactory aiModelFactory() { + return new AiModelFactoryImpl(); } // ========== 各种 AI Client 创建 ========== diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java deleted file mode 100644 index e37afc4..0000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java +++ /dev/null @@ -1,82 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.factory; - -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.image.ImageModel; - -/** - * AI 客户端工厂的接口类 - * - * @author fansili - */ -public interface AiClientFactory { - - /** - * 基于指定配置,获得 ChatModel 对象 - * - * 如果不存在,则进行创建 - * - * @param platform 平台 - * @param apiKey API KEY - * @param url API URL - * @return ChatModel 对象 - */ - ChatModel getOrCreateChatClient(AiPlatformEnum platform, String apiKey, String url); - - /** - * 基于默认配置,获得 ChatModel 对象 - * - * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置 - * - * @param platform 平台 - * @return ChatModel 对象 - */ - ChatModel getDefaultChatClient(AiPlatformEnum platform); - - /** - * 基于默认配置,获得 ImageClient 对象 - * - * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置 - * - * @param platform 平台 - * @return ImageClient 对象 - */ - ImageModel getDefaultImageClient(AiPlatformEnum platform); - - /** - * 基于指定配置,获得 ImageClient 对象 - * - * 如果不存在,则进行创建 - * - * @param platform 平台 - * @param apiKey API KEY - * @param url API URL - * @return ImageClient 对象 - */ - ImageModel getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url); - - /** - * 基于指定配置,获得 MidjourneyApi 对象 - * - * 如果不存在,则进行创建 - * - * @param apiKey API KEY - * @param url API URL - * @return MidjourneyApi 对象 - */ - MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url); - - /** - * 基于指定配置,获得 SunoApi 对象 - * - * 如果不存在,则进行创建 - * - * @param apiKey API KEY - * @param url API URL - * @return SunoApi 对象 - */ - SunoApi getOrCreateSunoApi(String apiKey, String url); - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java deleted file mode 100644 index 7eae7a8..0000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java +++ /dev/null @@ -1,215 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.factory; - -import cn.hutool.core.lang.Assert; -import cn.hutool.core.lang.Singleton; -import cn.hutool.core.lang.func.Func0; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration; -import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties; -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatClient; -import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; -import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; -import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties; -import com.alibaba.dashscope.aigc.generation.Generation; -import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; -import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; -import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; -import org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties; -import org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiImageModel; -import org.springframework.ai.openai.api.ApiUtils; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.api.OpenAiImageApi; -import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.api.QianFanApi; -import org.springframework.ai.stabilityai.StabilityAiImageModel; -import org.springframework.ai.stabilityai.api.StabilityAiApi; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.web.client.ResponseErrorHandler; -import org.springframework.web.client.RestClient; - -import java.util.List; - -/** - * AI 客户端工厂的实现类 - * - * @author 芋道源码 - */ -public class AiClientFactoryImpl implements AiClientFactory { - - @Override - public ChatModel getOrCreateChatClient(AiPlatformEnum platform, String apiKey, String url) { - String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url); - return Singleton.get(cacheKey, (Func0) () -> { - //noinspection EnhancedSwitchMigration - switch (platform) { - case OPENAI: - return buildOpenAiChatClient(apiKey, url); - case OLLAMA: - return buildOllamaChatClient(url); - case YI_YAN: - return buildYiYanChatClient(apiKey); - case XING_HUO: - return buildXingHuoChatClient(apiKey); - case QIAN_WEN: - return buildQianWenChatClient(apiKey); - case DEEP_SEEK: - return buildDeepSeekChatClient(apiKey); - default: - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); - } - }); - } - - @Override - public ChatModel getDefaultChatClient(AiPlatformEnum platform) { - //noinspection EnhancedSwitchMigration - switch (platform) { - case OPENAI: - return SpringUtil.getBean(OpenAiChatModel.class); - case OLLAMA: - return SpringUtil.getBean(OllamaChatModel.class); - case YI_YAN: - return SpringUtil.getBean(QianFanChatModel.class); - case XING_HUO: - return SpringUtil.getBean(XingHuoChatClient.class); - case QIAN_WEN: - return SpringUtil.getBean(TongYiChatModel.class); - default: - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); - } - } - - @Override - public ImageModel getDefaultImageClient(AiPlatformEnum platform) { - //noinspection EnhancedSwitchMigration - switch (platform) { - case OPENAI: - return SpringUtil.getBean(OpenAiImageModel.class); - case STABLE_DIFFUSION: - return SpringUtil.getBean(StabilityAiImageModel.class); - default: - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); - } - } - - @Override - public ImageModel getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url) { - //noinspection EnhancedSwitchMigration - switch (platform) { - case OPENAI: - return buildOpenAiImageClient(apiKey, url); - case STABLE_DIFFUSION: - return buildStabilityAiImageClient(apiKey, url); - default: - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); - } - } - - @Override - public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) { - String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url); - return Singleton.get(cacheKey, (Func0) () -> { - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class).getMidjourney(); - return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); - }); - } - - @Override - public SunoApi getOrCreateSunoApi(String apiKey, String url) { - String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url); - return Singleton.get(cacheKey, (Func0) () -> new SunoApi(url)); - } - - private static String buildClientCacheKey(Class clazz, Object... params) { - if (ArrayUtil.isEmpty(params)) { - return clazz.getName(); - } - return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_")); - } - - // ========== 各种创建 spring-ai 客户端的方法 ========== - - /** - * 可参考 {@link OpenAiAutoConfiguration} - */ - private static OpenAiChatModel buildOpenAiChatClient(String openAiToken, String url) { - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); - OpenAiApi openAiApi = new OpenAiApi(url, openAiToken); - return new OpenAiChatModel(openAiApi); - } - - /** - * 可参考 {@link OllamaAutoConfiguration} - */ - private static OllamaChatModel buildOllamaChatClient(String url) { - OllamaApi ollamaApi = new OllamaApi(url); - return new OllamaChatModel(ollamaApi); - } - - /** - * 可参考 {@link QianFanAutoConfiguration#qianFanChatModel(QianFanConnectionProperties, QianFanChatProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} - */ - private static QianFanChatModel buildYiYanChatClient(String key) { - // TODO @xin:貌似目前设置,request 势必会报错;看看能不能有办法,参考 buildQianWenChatClient,调用 QianFanAutoConfiguration#qianFanChatModel初始化,当然 key 要用自己的哈 - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); - String appKey = keys.get(0); - String secretKey = keys.get(1); - QianFanApi qianFanApi = new QianFanApi(appKey, secretKey); - return new QianFanChatModel(qianFanApi); - } - - /** - * 可参考 {@link YudaoAiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)} - */ - private static XingHuoChatClient buildXingHuoChatClient(String key) { - List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); - String appKey = keys.get(1); - String secretKey = keys.get(2); - return new XingHuoChatClient(appKey, secretKey); - } - - private static DeepSeekChatClient buildDeepSeekChatClient(String apiKey) { - return new DeepSeekChatClient(apiKey); - } - - /** - * 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)} - */ - private static TongYiChatModel buildQianWenChatClient(String key) { - com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class); - TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class); - // TODO @xin:貌似 apiKey 是全局唯一的???得测试下 - // TODO @xin:貌似阿里云不是增量返回的 - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(key); - return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties); - } - - private OpenAiImageModel buildOpenAiImageClient(String openAiToken, String url) { - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); - OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder()); - return new OpenAiImageModel(openAiApi); - } - - private StabilityAiImageModel buildStabilityAiImageClient(String apiKey, String url) { - url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL); - StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url); - return new StabilityAiImageModel(stabilityAiApi); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java new file mode 100644 index 0000000..e1d3aba --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.framework.ai.core.factory; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.image.ImageModel; + +/** + * AI Model 模型工厂的接口类 + * + * @author fansili + */ +public interface AiModelFactory { + + /** + * 基于指定配置,获得 ChatModel 对象 + * + * 如果不存在,则进行创建 + * + * @param platform 平台 + * @param apiKey API KEY + * @param url API URL + * @return ChatModel 对象 + */ + ChatModel getOrCreateChatClient(AiPlatformEnum platform, String apiKey, String url); + + /** + * 基于默认配置,获得 ChatModel 对象 + * + * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置 + * + * @param platform 平台 + * @return ChatModel 对象 + */ + ChatModel getDefaultChatModel(AiPlatformEnum platform); + + /** + * 基于默认配置,获得 ImageModel 对象 + * + * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置 + * + * @param platform 平台 + * @return ImageModel 对象 + */ + ImageModel getDefaultImageModel(AiPlatformEnum platform); + + /** + * 基于指定配置,获得 ImageModel 对象 + * + * 如果不存在,则进行创建 + * + * @param platform 平台 + * @param apiKey API KEY + * @param url API URL + * @return ImageModel 对象 + */ + ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url); + + /** + * 基于指定配置,获得 MidjourneyApi 对象 + * + * 如果不存在,则进行创建 + * + * @param apiKey API KEY + * @param url API URL + * @return MidjourneyApi 对象 + */ + MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url); + + /** + * 基于指定配置,获得 SunoApi 对象 + * + * 如果不存在,则进行创建 + * + * @param apiKey API KEY + * @param url API URL + * @return SunoApi 对象 + */ + SunoApi getOrCreateSunoApi(String apiKey, String url); + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java new file mode 100644 index 0000000..ca01a66 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -0,0 +1,218 @@ +package cn.iocoder.yudao.framework.ai.core.factory; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Singleton; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration; +import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatClient; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; +import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; +import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties; +import com.alibaba.dashscope.aigc.generation.Generation; +import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; +import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; +import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; +import org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties; +import org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiImageModel; +import org.springframework.ai.openai.api.ApiUtils; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.ai.qianfan.QianFanChatModel; +import org.springframework.ai.qianfan.api.QianFanApi; +import org.springframework.ai.stabilityai.StabilityAiImageModel; +import org.springframework.ai.stabilityai.api.StabilityAiApi; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; + +import java.util.List; + +/** + * AI Model 模型工厂的实现类 + * + * @author 芋道源码 + */ +public class AiModelFactoryImpl implements AiModelFactory { + + @Override + public ChatModel getOrCreateChatClient(AiPlatformEnum platform, String apiKey, String url) { + String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> { + //noinspection EnhancedSwitchMigration + switch (platform) { + case OLLAMA: + return buildOllamaChatClient(url); + case YI_YAN: + return buildYiYanChatClient(apiKey); + case XING_HUO: + return buildXingHuoChatClient(apiKey); + case QIAN_WEN: + return buildQianWenChatClient(apiKey); + case DEEP_SEEK: + return buildDeepSeekChatClient(apiKey); + case OPENAI: + return buildOpenAiChatModel(apiKey, url); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + }); + } + + @Override + public ChatModel getDefaultChatModel(AiPlatformEnum platform) { + //noinspection EnhancedSwitchMigration + switch (platform) { + case OLLAMA: + return SpringUtil.getBean(OllamaChatModel.class); + case YI_YAN: + return SpringUtil.getBean(QianFanChatModel.class); + case XING_HUO: + return SpringUtil.getBean(XingHuoChatClient.class); + case QIAN_WEN: + return SpringUtil.getBean(TongYiChatModel.class); + case OPENAI: + return SpringUtil.getBean(OpenAiChatModel.class); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + } + + @Override + public ImageModel getDefaultImageModel(AiPlatformEnum platform) { + //noinspection EnhancedSwitchMigration + switch (platform) { + case OPENAI: + return SpringUtil.getBean(OpenAiImageModel.class); + case STABLE_DIFFUSION: + return SpringUtil.getBean(StabilityAiImageModel.class); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + } + + @Override + public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) { + //noinspection EnhancedSwitchMigration + switch (platform) { + case OPENAI: + return buildOpenAiImageModel(apiKey, url); + case STABLE_DIFFUSION: + return buildStabilityAiImageClient(apiKey, url); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + } + + @Override + public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) { + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> { + YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class).getMidjourney(); + return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); + }); + } + + @Override + public SunoApi getOrCreateSunoApi(String apiKey, String url) { + String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> new SunoApi(url)); + } + + private static String buildClientCacheKey(Class clazz, Object... params) { + if (ArrayUtil.isEmpty(params)) { + return clazz.getName(); + } + return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_")); + } + + // ========== 各种创建 spring-ai 客户端的方法 ========== + + /** + * 可参考 {@link OpenAiAutoConfiguration} + */ + private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { + url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); + OpenAiApi openAiApi = new OpenAiApi(url, openAiToken); + return new OpenAiChatModel(openAiApi); + } + + /** + * 可参考 {@link OpenAiAutoConfiguration} + */ + private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { + url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); + OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder()); + return new OpenAiImageModel(openAiApi); + } + + /** + * 可参考 {@link OllamaAutoConfiguration} + */ + private static OllamaChatModel buildOllamaChatClient(String url) { + OllamaApi ollamaApi = new OllamaApi(url); + return new OllamaChatModel(ollamaApi); + } + + /** + * 可参考 {@link QianFanAutoConfiguration#qianFanChatModel(QianFanConnectionProperties, QianFanChatProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + */ + private static QianFanChatModel buildYiYanChatClient(String key) { + // TODO @xin:貌似目前设置,request 势必会报错;看看能不能有办法,参考 buildQianWenChatClient,调用 QianFanAutoConfiguration#qianFanChatModel初始化,当然 key 要用自己的哈 + List keys = StrUtil.split(key, '|'); + Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); + String appKey = keys.get(0); + String secretKey = keys.get(1); + QianFanApi qianFanApi = new QianFanApi(appKey, secretKey); + return new QianFanChatModel(qianFanApi); + } + + /** + * 可参考 {@link YudaoAiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)} + */ + private static XingHuoChatClient buildXingHuoChatClient(String key) { + List keys = StrUtil.split(key, '|'); + Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); + String appKey = keys.get(1); + String secretKey = keys.get(2); + return new XingHuoChatClient(appKey, secretKey); + } + + private static DeepSeekChatClient buildDeepSeekChatClient(String apiKey) { + return new DeepSeekChatClient(apiKey); + } + + /** + * 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)} + */ + private static TongYiChatModel buildQianWenChatClient(String key) { + com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class); + TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class); + // TODO @xin:貌似 apiKey 是全局唯一的???得测试下 + // TODO @xin:貌似阿里云不是增量返回的 + TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); + connectionProperties.setApiKey(key); + return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties); + } + + private StabilityAiImageModel buildStabilityAiImageClient(String apiKey, String url) { + url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL); + StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url); + return new StabilityAiImageModel(stabilityAiApi); + } + +} -- libgit2 0.21.4