diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java index 09de7263c5..e979056d4e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java @@ -1,4 +1,4 @@ /** - * crm 模块的 web 拓展封装 + * ai 模块的 web 拓展封装 */ -package cn.iocoder.yudao.module.crm.framework.web; +package cn.iocoder.yudao.module.ai.framework.web; 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 e8532a5762..3e8c09f079 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 @@ -20,7 +20,6 @@ import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; import cn.iocoder.yudao.module.infra.api.file.FileApi; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.image.ImageModel; @@ -144,7 +143,7 @@ public class AiImageServiceImpl implements AiImageService { .withClipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) .build(); } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { - return TongYiImagesOptions.builder() + return DashScopeImageOptions.builder() .withModel(draw.getModel()).withN(1) .withHeight(draw.getHeight()).withWidth(draw.getWidth()) .build(); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index a270f76558..156d3f0769 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -14,8 +14,8 @@ ${project.artifactId} AI 大模型拓展,接入国内外大模型 - group.springframework.ai - 1.1.0 + org.springframework.ai + 1.0.0-M6 @@ -90,6 +90,11 @@ spring-ai-qianfan-spring-boot-starter ${spring-ai.version} + + com.alibaba.cloud.ai + spring-ai-alibaba-starter + 1.0.0-M5.1 + 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 0d2620b0cb..3aed3ee1b9 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,15 +1,16 @@ package cn.iocoder.yudao.framework.ai.config; +import cn.hutool.core.util.StrUtil; 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.DeepSeekChatModel; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; 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.XingHuoChatModel; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; -import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.transformer.splitter.TokenTextSplitter; @@ -17,7 +18,6 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; /** @@ -28,7 +28,6 @@ import org.springframework.context.annotation.Lazy; @AutoConfiguration @EnableConfigurationProperties(YudaoAiProperties.class) @Slf4j -@Import(TongYiAutoConfiguration.class) public class YudaoAiAutoConfiguration { @Bean @@ -43,26 +42,52 @@ public class YudaoAiAutoConfiguration { @ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true") public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepSeek(); - DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model(properties.getModel()) - .temperature(properties.getTemperature()) - .maxTokens(properties.getMaxTokens()) - .topP(properties.getTopP()) + return buildDeepSeekChatModel(properties); + } + + public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(DeepSeekChatModel.MODEL_DEFAULT); + } + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(DeepSeekChatModel.BASE_URL) + .apiKey(properties.getApiKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) .build(); - return new DeepSeekChatModel(properties.getApiKey(), options); + return new DeepSeekChatModel(openAiChatModel); } @Bean @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); - XingHuoChatOptions options = XingHuoChatOptions.builder() - .model(properties.getModel()) - .temperature(properties.getTemperature()) - .maxTokens(properties.getMaxTokens()) - .topK(properties.getTopK()) + return buildXingHuoChatClient(properties); + } + + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) { + if (StrUtil.isEmpty(properties.getModel())) { + properties.setModel(XingHuoChatModel.MODEL_DEFAULT); + } + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(XingHuoChatModel.BASE_URL) + .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()) + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build()) .build(); - return new XingHuoChatModel(properties.getAppKey(), properties.getSecretKey(), options); + return new XingHuoChatModel(openAiChatModel); } @Bean diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index 82c74b0c64..7a98c4376a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -42,9 +42,9 @@ public class YudaoAiProperties { private String secretKey; private String model; - private Float temperature; + private Double temperature; private Integer maxTokens; - private Integer topK; + private Double topP; } @@ -55,9 +55,9 @@ public class YudaoAiProperties { private String apiKey; private String model; - private Float temperature; + private Double temperature; private Integer maxTokens; - private Float topP; + private Double topP; } 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 index 7acd247691..985d8fd041 100644 --- 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 @@ -13,60 +13,45 @@ import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; 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.XingHuoChatModel; -import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; -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.cloud.ai.tongyi.image.TongYiImagesModel; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.embeddings.TextEmbedding; -import com.azure.ai.openai.OpenAIClient; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.azure.ai.openai.OpenAIClientBuilder; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties; 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.autoconfigure.qianfan.QianFanImageProperties; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; -import org.springframework.ai.model.function.FunctionCallbackContext; 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.openai.api.common.OpenAiApiConstants; import org.springframework.ai.qianfan.QianFanChatModel; import org.springframework.ai.qianfan.QianFanImageModel; import org.springframework.ai.qianfan.api.QianFanApi; import org.springframework.ai.qianfan.api.QianFanImageApi; import org.springframework.ai.stabilityai.StabilityAiImageModel; import org.springframework.ai.stabilityai.api.StabilityAiApi; -import org.springframework.ai.vectorstore.RedisVectorStore; +import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; -import org.springframework.boot.autoconfigure.data.redis.RedisProperties; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; -import redis.clients.jedis.JedisPooled; -import redis.clients.jedis.search.Schema; import java.util.List; @@ -110,7 +95,7 @@ public class AiModelFactoryImpl implements AiModelFactory { //noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return SpringUtil.getBean(TongYiChatModel.class); + return SpringUtil.getBean(DashScopeChatModel.class); case YI_YAN: return SpringUtil.getBean(QianFanChatModel.class); case DEEP_SEEK: @@ -135,7 +120,7 @@ public class AiModelFactoryImpl implements AiModelFactory { //noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return SpringUtil.getBean(TongYiImagesModel.class); + return SpringUtil.getBean(DashScopeImageModel.class); case YI_YAN: return SpringUtil.getBean(QianFanImageModel.class); case ZHI_PU: @@ -202,17 +187,20 @@ public class AiModelFactoryImpl implements AiModelFactory { String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); - var config = RedisVectorStore.RedisVectorStoreConfig.builder() - .withIndexName(cacheKey) - .withPrefix(prefix) - .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) - .build(); - RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); - RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, - new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), - true); - redisVectorStore.afterPropertiesSet(); - return redisVectorStore; + // TODO @芋艿:先临时使用 store + return SimpleVectorStore.builder(embeddingModel).build(); + // TODO @芋艿:@xin:后续看看,是不是切到阿里云之类的 +// var config = RedisVectorStore.RedisVectorStoreConfig.builder() +// .withIndexName(cacheKey) +// .withPrefix(prefix) +// .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) +// .build(); +// RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); +// RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, +// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), +// true); +// redisVectorStore.afterPropertiesSet(); +// return redisVectorStore; }); } @@ -226,29 +214,23 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 spring-ai 客户端的方法 ========== /** - * 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)} + * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法 */ - private static TongYiChatModel buildTongYiChatModel(String key) { - com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class); - TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class); - // TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下 - // TODO @芋艿:貌似阿里云不是增量返回的 - // 该 issue 进行跟进中 https://github.com/alibaba/spring-cloud-alibaba/issues/3790 - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(key); - return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties); - } - - private static TongYiImagesModel buildTongYiImagesModel(String key) { - ImageSynthesis imageSynthesis = SpringUtil.getBean(ImageSynthesis.class); - TongYiImagesProperties imagesOptions = SpringUtil.getBean(TongYiImagesProperties.class); - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(key); - return new TongYiAutoConfiguration().tongYiImagesClient(imageSynthesis, imagesOptions, connectionProperties); + private static DashScopeChatModel buildTongYiChatModel(String key) { + DashScopeApi dashScopeApi = new DashScopeApi(key); + return new DashScopeChatModel(dashScopeApi); } /** - * 可参考 {@link QianFanAutoConfiguration#qianFanChatModel(QianFanConnectionProperties, QianFanChatProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法 + */ + private static DashScopeImageModel buildTongYiImagesModel(String key) { + DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); + return new DashScopeImageModel(dashScopeImageApi); + } + + /** + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法 */ private static QianFanChatModel buildYiYanChatModel(String key) { List keys = StrUtil.split(key, '|'); @@ -260,7 +242,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration#qianFanImageModel(QianFanConnectionProperties, QianFanImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法 */ private QianFanImageModel buildQianFanImageModel(String key) { List keys = StrUtil.split(key, '|'); @@ -275,11 +257,13 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link YudaoAiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} */ private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { - return new DeepSeekChatModel(apiKey); + YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties() + .setApiKey(apiKey); + return new YudaoAiAutoConfiguration().buildDeepSeekChatModel(properties); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); @@ -288,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); @@ -301,21 +285,22 @@ public class AiModelFactoryImpl implements AiModelFactory { */ private static XingHuoChatModel buildXingHuoChatModel(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 XingHuoChatModel(appKey, secretKey); + Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); + YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties() + .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); + return new YudaoAiAutoConfiguration().buildXingHuoChatClient(properties); } /** - * 可参考 {@link OpenAiAutoConfiguration} + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 */ 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); + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); + OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); + return OpenAiChatModel.builder().openAiApi(openAiApi).build(); } + // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** * 可参考 {@link AzureOpenAiAutoConfiguration} */ @@ -325,27 +310,28 @@ public class AiModelFactoryImpl implements AiModelFactory { AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); connectionProperties.setApiKey(apiKey); connectionProperties.setEndpoint(url); - OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties); + OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); // 获取 AzureOpenAiChatProperties 对象 AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null); + return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, + null, null, null); } /** - * 可参考 {@link OpenAiAutoConfiguration} + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法 */ private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); - OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder()); + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); + OpenAiImageApi openAiApi = OpenAiImageApi.builder().baseUrl(url).apiKey(openAiToken).build(); return new OpenAiImageModel(openAiApi); } /** - * 可参考 {@link OllamaAutoConfiguration} + * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 */ private static OllamaChatModel buildOllamaChatModel(String url) { OllamaApi ollamaApi = new OllamaApi(url); - return new OllamaChatModel(ollamaApi); + return OllamaChatModel.builder().ollamaApi(ollamaApi).build(); } private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { @@ -356,13 +342,13 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 EmbeddingModel 的方法 ========== + // TODO @芋艿:需要测试下 /** - * 可参考 {@link TongYiAutoConfiguration#tongYiTextEmbeddingClient(TextEmbedding, TongYiConnectionProperties)} + * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 */ private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) { - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(apiKey); - return new TongYiAutoConfiguration().tongYiTextEmbeddingClient(SpringUtil.getBean(TextEmbedding.class), connectionProperties); + DashScopeApi dashScopeApi = new DashScopeApi(apiKey); + return new DashScopeEmbeddingModel(dashScopeApi); } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java index 55091c78d4..a03a0c844f 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java @@ -8,9 +8,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.openai.api.ApiUtils; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @@ -18,6 +19,7 @@ import reactor.core.publisher.Mono; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -50,11 +52,19 @@ public class MidjourneyApi { public MidjourneyApi(String baseUrl, String apiKey, String notifyUrl) { this.webClient = WebClient.builder() .baseUrl(baseUrl) - .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)) + .defaultHeaders(getJsonContentHeaders(apiKey)) .build(); this.notifyUrl = notifyUrl; } + // TODO @芋艿:这里,看看怎么调整下???https://github.com/spring-projects/spring-ai/issues/741 + public static Consumer getJsonContentHeaders(String apiKey) { + return (headers) -> { + headers.setBearerAuth(apiKey); + headers.setContentType(MediaType.APPLICATION_JSON); + }; + }; + /** * imagine - 根据提示词提交绘画任务 * diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index e18f100156..7203db4593 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -2,9 +2,7 @@ package cn.iocoder.yudao.framework.ai.core.util; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.prompt.ChatOptions; @@ -21,26 +19,24 @@ import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; public class AiUtils { public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { - Float temperatureF = temperature != null ? temperature.floatValue() : null; //noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build(); + // TODO @芋艿:tongyi 暂时没 maxTokens 选项 + return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).build(); case YI_YAN: - return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); - case DEEP_SEEK: - return DeepSeekChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); + return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: - return ZhiPuAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); - case XING_HUO: - return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); + return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case OPENAI: - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + case DEEP_SEEK: // 复用 OpenAI 客户端 + case XING_HUO: // 复用 OpenAI 客户端 + return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case AZURE_OPENAI: // TODO 芋艿:貌似没 model 字段???! - return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); case OLLAMA: - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens); + return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens).build(); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -56,8 +52,8 @@ public class AiUtils { if (MessageType.SYSTEM.getValue().equals(type)) { return new SystemMessage(content); } - if (MessageType.FUNCTION.getValue().equals(type)) { - return new FunctionMessage(content); + if (MessageType.TOOL.getValue().equals(type)) { + throw new UnsupportedOperationException("暂不支持 tool 消息:" + content); } throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type)); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java deleted file mode 100644 index a72d50c4a8..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 - 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ai.autoconfigure.vectorstore.redis; - -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.vectorstore.RedisVectorStore; -import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import redis.clients.jedis.JedisPooled; - -/** - * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突 - * - * TODO 这个官方,有说啥时候 fix 哇? - * TODO 看着是列在1.0.0-M2版本 - * - * @author Christian Tzolov - * @author Eddú Meléndez - */ -@AutoConfiguration(after = RedisAutoConfiguration.class) -@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class}) -@ConditionalOnBean(JedisConnectionFactory.class) -@EnableConfigurationProperties(RedisVectorStoreProperties.class) -public class RedisVectorStoreAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties, - JedisConnectionFactory jedisConnectionFactory) { - - var config = RedisVectorStoreConfig.builder() - .withIndexName(properties.getIndex()) - .withPrefix(properties.getPrefix()) - .build(); - - return new RedisVectorStore(config, embeddingModel, - new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()), - properties.isInitializeSchema()); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java deleted file mode 100644 index de80401ed1..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java +++ /dev/null @@ -1,456 +0,0 @@ -/* - * Copyright 2023 - 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.ai.vectorstore; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import redis.clients.jedis.JedisPooled; -import redis.clients.jedis.Pipeline; -import redis.clients.jedis.json.Path2; -import redis.clients.jedis.search.*; -import redis.clients.jedis.search.Schema.FieldType; -import redis.clients.jedis.search.schemafields.*; -import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm; - -import java.text.MessageFormat; -import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * The RedisVectorStore is for managing and querying vector data in a Redis database. It - * offers functionalities like adding, deleting, and performing similarity searches on - * documents. - * - * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and - * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for - * efficient similarity searches. Additionally, it allows for custom metadata fields in - * the documents to be stored alongside the vector and content data. - * - * This class requires a RedisVectorStoreConfig configuration object for initialization, - * which includes settings like Redis URI, index name, field names, and vector algorithms. - * It also requires an EmbeddingModel to convert documents into embeddings before storing - * them. - * - * @author Julien Ruaux - * @author Christian Tzolov - * @author Eddú Meléndez - * @see VectorStore - * @see RedisVectorStoreConfig - * @see EmbeddingModel - */ -public class RedisVectorStore implements VectorStore, InitializingBean { - - public enum Algorithm { - - FLAT, HSNW - - } - - public record MetadataField(String name, FieldType fieldType) { - - public static MetadataField text(String name) { - return new MetadataField(name, FieldType.TEXT); - } - - public static MetadataField numeric(String name) { - return new MetadataField(name, FieldType.NUMERIC); - } - - public static MetadataField tag(String name) { - return new MetadataField(name, FieldType.TAG); - } - - } - - /** - * Configuration for the Redis vector store. - */ - public static final class RedisVectorStoreConfig { - - private final String indexName; - - private final String prefix; - - private final String contentFieldName; - - private final String embeddingFieldName; - - private final Algorithm vectorAlgorithm; - - private final List metadataFields; - - private RedisVectorStoreConfig() { - this(builder()); - } - - private RedisVectorStoreConfig(Builder builder) { - this.indexName = builder.indexName; - this.prefix = builder.prefix; - this.contentFieldName = builder.contentFieldName; - this.embeddingFieldName = builder.embeddingFieldName; - this.vectorAlgorithm = builder.vectorAlgorithm; - this.metadataFields = builder.metadataFields; - } - - /** - * Start building a new configuration. - * @return The entry point for creating a new configuration. - */ - public static Builder builder() { - - return new Builder(); - } - - /** - * {@return the default config} - */ - public static RedisVectorStoreConfig defaultConfig() { - - return builder().build(); - } - - public static class Builder { - - private String indexName = DEFAULT_INDEX_NAME; - - private String prefix = DEFAULT_PREFIX; - - private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME; - - private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME; - - private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM; - - private List metadataFields = new ArrayList<>(); - - private Builder() { - } - - /** - * Configures the Redis index name to use. - * @param name the index name to use - * @return this builder - */ - public Builder withIndexName(String name) { - this.indexName = name; - return this; - } - - /** - * Configures the Redis key prefix to use (default: "embedding:"). - * @param prefix the prefix to use - * @return this builder - */ - public Builder withPrefix(String prefix) { - this.prefix = prefix; - return this; - } - - /** - * Configures the Redis content field name to use. - * @param name the content field name to use - * @return this builder - */ - public Builder withContentFieldName(String name) { - this.contentFieldName = name; - return this; - } - - /** - * Configures the Redis embedding field name to use. - * @param name the embedding field name to use - * @return this builder - */ - public Builder withEmbeddingFieldName(String name) { - this.embeddingFieldName = name; - return this; - } - - /** - * Configures the Redis vector algorithmto use. - * @param algorithm the vector algorithm to use - * @return this builder - */ - public Builder withVectorAlgorithm(Algorithm algorithm) { - this.vectorAlgorithm = algorithm; - return this; - } - - public Builder withMetadataFields(MetadataField... fields) { - return withMetadataFields(Arrays.asList(fields)); - } - - public Builder withMetadataFields(List fields) { - this.metadataFields = fields; - return this; - } - - /** - * {@return the immutable configuration} - */ - public RedisVectorStoreConfig build() { - - return new RedisVectorStoreConfig(this); - } - - } - - } - - private final boolean initializeSchema; - - public static final String DEFAULT_INDEX_NAME = "spring-ai-index"; - - public static final String DEFAULT_CONTENT_FIELD_NAME = "content"; - - public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding"; - - public static final String DEFAULT_PREFIX = "embedding:"; - - public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW; - - private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]"; - - private static final Path2 JSON_SET_PATH = Path2.of("$"); - - private static final String JSON_PATH_PREFIX = "$."; - - private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class); - - private static final Predicate RESPONSE_OK = Predicate.isEqual("OK"); - - private static final Predicate RESPONSE_DEL_OK = Predicate.isEqual(1l); - - private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32"; - - private static final String EMBEDDING_PARAM_NAME = "BLOB"; - - public static final String DISTANCE_FIELD_NAME = "vector_score"; - - private static final String DEFAULT_DISTANCE_METRIC = "COSINE"; - - private final JedisPooled jedis; - - private final EmbeddingModel embeddingModel; - - private final RedisVectorStoreConfig config; - - private FilterExpressionConverter filterExpressionConverter; - - public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis, - boolean initializeSchema) { - - Assert.notNull(config, "Config must not be null"); - Assert.notNull(embeddingModel, "Embedding model must not be null"); - this.initializeSchema = initializeSchema; - - this.jedis = jedis; - this.embeddingModel = embeddingModel; - this.config = config; - this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields); - } - - public JedisPooled getJedis() { - return this.jedis; - } - - @Override - public void add(List documents) { - try (Pipeline pipeline = this.jedis.pipelined()) { - for (Document document : documents) { - var embedding = this.embeddingModel.embed(document); - document.setEmbedding(embedding); - - var fields = new HashMap(); - fields.put(this.config.embeddingFieldName, embedding); - fields.put(this.config.contentFieldName, document.getContent()); - fields.putAll(document.getMetadata()); - pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields); - } - List responses = pipeline.syncAndReturnAll(); - Optional errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny(); - if (errResponse.isPresent()) { - String message = MessageFormat.format("Could not add document: {0}", errResponse.get()); - if (logger.isErrorEnabled()) { - logger.error(message); - } - throw new RuntimeException(message); - } - } - } - - private String key(String id) { - return this.config.prefix + id; - } - - @Override - public Optional delete(List idList) { - try (Pipeline pipeline = this.jedis.pipelined()) { - for (String id : idList) { - pipeline.jsonDel(key(id)); - } - List responses = pipeline.syncAndReturnAll(); - Optional errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny(); - if (errResponse.isPresent()) { - if (logger.isErrorEnabled()) { - logger.error("Could not delete document: {}", errResponse.get()); - } - return Optional.of(false); - } - return Optional.of(true); - } - } - - @Override - public List similaritySearch(SearchRequest request) { - - Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero"); - Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1, - "The similarity score is bounded between 0 and 1; least to most similar respectively."); - - String filter = nativeExpressionFilter(request); - - String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName, - EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME); - - List returnFields = new ArrayList<>(); - this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add); - returnFields.add(this.config.embeddingFieldName); - returnFields.add(this.config.contentFieldName); - returnFields.add(DISTANCE_FIELD_NAME); - var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery())); - Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding)) - .returnFields(returnFields.toArray(new String[0])) - .setSortBy(DISTANCE_FIELD_NAME, true) - .dialect(2); - - SearchResult result = this.jedis.ftSearch(this.config.indexName, query); - return result.getDocuments() - .stream() - .filter(d -> similarityScore(d) >= request.getSimilarityThreshold()) - .map(this::toDocument) - .toList(); - } - - private Document toDocument(redis.clients.jedis.search.Document doc) { - var id = doc.getId().substring(this.config.prefix.length()); - var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName) - : null; - Map metadata = this.config.metadataFields.stream() - .map(MetadataField::name) - .filter(doc::hasProperty) - .collect(Collectors.toMap(Function.identity(), doc::getString)); - metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc)); - return new Document(id, content, metadata); - } - - private float similarityScore(redis.clients.jedis.search.Document doc) { - return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2; - } - - private String nativeExpressionFilter(SearchRequest request) { - if (request.getFilterExpression() == null) { - return "*"; - } - return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")"; - } - - @Override - public void afterPropertiesSet() { - - if (!this.initializeSchema) { - return; - } - - // If index already exists don't do anything - if (this.jedis.ftList().contains(this.config.indexName)) { - return; - } - - String response = this.jedis.ftCreate(this.config.indexName, - FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields()); - if (!RESPONSE_OK.test(response)) { - String message = MessageFormat.format("Could not create index: {0}", response); - throw new RuntimeException(message); - } - } - - private Iterable schemaFields() { - Map vectorAttrs = new HashMap<>(); - vectorAttrs.put("DIM", this.embeddingModel.dimensions()); - vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC); - vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32); - List fields = new ArrayList<>(); - fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0)); - fields.add(VectorField.builder() - .fieldName(jsonPath(this.config.embeddingFieldName)) - .algorithm(vectorAlgorithm()) - .attributes(vectorAttrs) - .as(this.config.embeddingFieldName) - .build()); - - if (!CollectionUtils.isEmpty(this.config.metadataFields)) { - for (MetadataField field : this.config.metadataFields) { - fields.add(schemaField(field)); - } - } - return fields; - } - - private SchemaField schemaField(MetadataField field) { - String fieldName = jsonPath(field.name); - switch (field.fieldType) { - case NUMERIC: - return NumericField.of(fieldName).as(field.name); - case TAG: - return TagField.of(fieldName).as(field.name); - case TEXT: - return TextField.of(fieldName).as(field.name); - default: - throw new IllegalArgumentException( - MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType)); - } - } - - private VectorAlgorithm vectorAlgorithm() { - if (config.vectorAlgorithm == Algorithm.HSNW) { - return VectorAlgorithm.HNSW; - } - return VectorAlgorithm.FLAT; - } - - private String jsonPath(String field) { - return JSON_PATH_PREFIX + field; - } - - private static float[] toFloatArray(List embeddingDouble) { - float[] embeddingFloat = new float[embeddingDouble.size()]; - int i = 0; - for (Double d : embeddingDouble) { - embeddingFloat[i++] = d.floatValue(); - } - return embeddingFloat; - } - -} \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java index c859587799..7713ec4688 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.framework.ai.chat; -import com.azure.ai.openai.OpenAIClient; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.util.ClientOptions; @@ -27,13 +26,13 @@ import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatP */ public class AzureOpenAIChatModelTests { - private final OpenAIClient openAiApi = (new OpenAIClientBuilder()) + // TODO @芋艿:晚点在调整 + private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() .endpoint("https://eastusprejade.openai.azure.com") .credential(new AzureKeyCredential("xxx")) - .clientOptions((new ClientOptions()).setApplicationId("spring-ai")) - .buildClient(); + .clientOptions((new ClientOptions()).setApplicationId("spring-ai")); private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi, - AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build()); + AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java index c6b99f287b..a72b556fcd 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java @@ -1,20 +1,6 @@ package cn.iocoder.yudao.framework.ai.chat; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.api.OllamaModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import reactor.core.publisher.Flux; - -import java.util.ArrayList; -import java.util.List; /** * {@link OllamaChatModel} 集成测试 @@ -23,41 +9,41 @@ import java.util.List; */ public class LlamaChatModelTests { - private final OllamaApi ollamaApi = new OllamaApi( - "http://127.0.0.1:11434"); - private final OllamaChatModel chatModel = new OllamaChatModel(ollamaApi, - OllamaOptions.create().withModel(OllamaModel.LLAMA3.getModelName())); - - @Test - @Disabled - public void testCall() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - ChatResponse response = chatModel.call(new Prompt(messages)); - // 打印结果 - System.out.println(response); - System.out.println(response.getResult().getOutput()); - } - - @Test - @Disabled - public void testStream() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - Flux flux = chatModel.stream(new Prompt(messages)); - // 打印结果 - flux.doOnNext(response -> { -// System.out.println(response); - System.out.println(response.getResult().getOutput()); - }).then().block(); - } +// private final OllamaApi ollamaApi = new OllamaApi( +// "http://127.0.0.1:11434"); +// private final OllamaChatModel chatModel = new OllamaChatModel(ollamaApi, +// OllamaOptions.create().withModel(OllamaModel.LLAMA3.getModelName())); +// +// @Test +// @Disabled +// public void testCall() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// ChatResponse response = chatModel.call(new Prompt(messages)); +// // 打印结果 +// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// } +// +// @Test +// @Disabled +// public void testStream() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// Flux flux = chatModel.stream(new Prompt(messages)); +// // 打印结果 +// flux.doOnNext(response -> { +//// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// }).then().block(); +// } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java index 6768325462..b6653b1b15 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java @@ -26,7 +26,7 @@ public class OpenAIChatModelTests { "https://api.holdai.top", "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf"); private final OpenAiChatModel chatModel = new OpenAiChatModel(openAiApi, - OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O).build()); + OpenAiChatOptions.builder().model(OpenAiApi.ChatModel.GPT_4_O).build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java index d10a04677f..e271decbc5 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java @@ -25,7 +25,7 @@ public class YiYanChatModelTests { "qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"); private final QianFanChatModel chatModel = new QianFanChatModel(qianFanApi, - QianFanChatOptions.builder().withModel(QianFanApi.ChatModel.ERNIE_Tiny_8K.getValue()).build() + QianFanChatOptions.builder().model(QianFanApi.ChatModel.ERNIE_Tiny_8K.getValue()).build() ); @Test diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java index 20b73bd8e7..101be34b7b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java @@ -24,7 +24,7 @@ public class ZhiPuAiChatModelTests { private final ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"); private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, - ZhiPuAiChatOptions.builder().withModel(ZhiPuAiApi.ChatModel.GLM_4.getModelName()).build()); + ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.ChatModel.GLM_4.getName()).build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java index 22bf6614eb..4c0a9eb36d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java @@ -25,9 +25,9 @@ public class QianFanImageTests { // 准备参数 // 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576 QianFanImageOptions imageOptions = QianFanImageOptions.builder() - .withModel(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) - .withWidth(1024).withHeight(1024) - .withN(1) + .model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) + .width(1024).height(1024) + .N(1) .build(); ImagePrompt prompt = new ImagePrompt("good", imageOptions); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java index 41d7859c4d..cc62f02fb6 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java @@ -1,34 +1,30 @@ package cn.iocoder.yudao.framework.ai.image; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.utils.Constants; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.openai.OpenAiImageOptions; /** - * {@link com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel} 集成测试类 + * {@link DashScopeImageModel} 集成测试类 * * @author fansili */ public class TongYiImagesModelTest { - private final ImageSynthesis imageApi = new ImageSynthesis(); - private final TongYiImagesModel imageModel = new TongYiImagesModel(imageApi); - - static { - Constants.apiKey = "sk-Zsd81gZYg7"; - } + private final DashScopeImageModel imageModel = new DashScopeImageModel( + new DashScopeImageApi("sk-7d903764249848cfa912733146da12d1")); @Test @Disabled public void imageCallTest() { // 准备参数 - ImageOptions options = OpenAiImageOptions.builder() + ImageOptions options = DashScopeImageOptions.builder() .withModel(ImageSynthesis.Models.WANX_V1) .withHeight(256).withWidth(256) .build(); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java index f9338995f3..f4802d4d21 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java @@ -22,7 +22,7 @@ public class ZhiPuAiImageModelTests { public void testCall() { // 准备参数 ZhiPuAiImageOptions imageOptions = ZhiPuAiImageOptions.builder() - .withModel(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) + .model(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) .build(); ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions);