Commit 7ef73b7d0992486c380c49e7f214f48ebf0f29d4
1 parent
3b7b8182
【代码重构】AI:spring ai 依赖,升级到 1.0.0-M6
Showing
18 changed files
with
197 additions
and
712 deletions
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java
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; |
| 20 | 20 | import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; |
| 21 | 21 | import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; |
| 22 | 22 | import cn.iocoder.yudao.module.infra.api.file.FileApi; |
| 23 | -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesOptions; | |
| 24 | 23 | import jakarta.annotation.Resource; |
| 25 | 24 | import lombok.extern.slf4j.Slf4j; |
| 26 | 25 | import org.springframework.ai.image.ImageModel; |
| ... | ... | @@ -144,7 +143,7 @@ public class AiImageServiceImpl implements AiImageService { |
| 144 | 143 | .withClipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) |
| 145 | 144 | .build(); |
| 146 | 145 | } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { |
| 147 | - return TongYiImagesOptions.builder() | |
| 146 | + return DashScopeImageOptions.builder() | |
| 148 | 147 | .withModel(draw.getModel()).withN(1) |
| 149 | 148 | .withHeight(draw.getHeight()).withWidth(draw.getWidth()) |
| 150 | 149 | .build(); | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
| ... | ... | @@ -14,8 +14,8 @@ |
| 14 | 14 | <name>${project.artifactId}</name> |
| 15 | 15 | <description>AI 大模型拓展,接入国内外大模型</description> |
| 16 | 16 | <properties> |
| 17 | - <spring-ai.groupId>group.springframework.ai</spring-ai.groupId> | |
| 18 | - <spring-ai.version>1.1.0</spring-ai.version> | |
| 17 | + <spring-ai.groupId>org.springframework.ai</spring-ai.groupId> | |
| 18 | + <spring-ai.version>1.0.0-M6</spring-ai.version> | |
| 19 | 19 | </properties> |
| 20 | 20 | |
| 21 | 21 | <dependencies> |
| ... | ... | @@ -90,6 +90,11 @@ |
| 90 | 90 | <artifactId>spring-ai-qianfan-spring-boot-starter</artifactId> |
| 91 | 91 | <version>${spring-ai.version}</version> |
| 92 | 92 | </dependency> |
| 93 | + <dependency> | |
| 94 | + <groupId>com.alibaba.cloud.ai</groupId> | |
| 95 | + <artifactId>spring-ai-alibaba-starter</artifactId> | |
| 96 | + <version>1.0.0-M5.1</version> | |
| 97 | + </dependency> | |
| 93 | 98 | |
| 94 | 99 | <!-- 阿里云 通义千问 --> |
| 95 | 100 | <!-- TODO 芋艿:等 spring cloud alibaba ai 发布最新的时候,可以替换掉这个依赖,并且删除我们直接 cv 的代码 --> | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
| 1 | 1 | package cn.iocoder.yudao.framework.ai.config; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; |
| 4 | 5 | import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; |
| 5 | 6 | import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; |
| 6 | -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; | |
| 7 | 7 | import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| 8 | 8 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 9 | 9 | import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; |
| 10 | -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; | |
| 11 | -import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; | |
| 12 | 10 | import lombok.extern.slf4j.Slf4j; |
| 11 | +import org.springframework.ai.openai.OpenAiChatModel; | |
| 12 | +import org.springframework.ai.openai.OpenAiChatOptions; | |
| 13 | +import org.springframework.ai.openai.api.OpenAiApi; | |
| 13 | 14 | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; |
| 14 | 15 | import org.springframework.ai.tokenizer.TokenCountEstimator; |
| 15 | 16 | import org.springframework.ai.transformer.splitter.TokenTextSplitter; |
| ... | ... | @@ -17,7 +18,6 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 17 | 18 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 18 | 19 | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| 19 | 20 | import org.springframework.context.annotation.Bean; |
| 20 | -import org.springframework.context.annotation.Import; | |
| 21 | 21 | import org.springframework.context.annotation.Lazy; |
| 22 | 22 | |
| 23 | 23 | /** |
| ... | ... | @@ -28,7 +28,6 @@ import org.springframework.context.annotation.Lazy; |
| 28 | 28 | @AutoConfiguration |
| 29 | 29 | @EnableConfigurationProperties(YudaoAiProperties.class) |
| 30 | 30 | @Slf4j |
| 31 | -@Import(TongYiAutoConfiguration.class) | |
| 32 | 31 | public class YudaoAiAutoConfiguration { |
| 33 | 32 | |
| 34 | 33 | @Bean |
| ... | ... | @@ -43,26 +42,52 @@ public class YudaoAiAutoConfiguration { |
| 43 | 42 | @ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true") |
| 44 | 43 | public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) { |
| 45 | 44 | YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepSeek(); |
| 46 | - DeepSeekChatOptions options = DeepSeekChatOptions.builder() | |
| 47 | - .model(properties.getModel()) | |
| 48 | - .temperature(properties.getTemperature()) | |
| 49 | - .maxTokens(properties.getMaxTokens()) | |
| 50 | - .topP(properties.getTopP()) | |
| 45 | + return buildDeepSeekChatModel(properties); | |
| 46 | + } | |
| 47 | + | |
| 48 | + public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) { | |
| 49 | + if (StrUtil.isEmpty(properties.getModel())) { | |
| 50 | + properties.setModel(DeepSeekChatModel.MODEL_DEFAULT); | |
| 51 | + } | |
| 52 | + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | |
| 53 | + .openAiApi(OpenAiApi.builder() | |
| 54 | + .baseUrl(DeepSeekChatModel.BASE_URL) | |
| 55 | + .apiKey(properties.getApiKey()) | |
| 56 | + .build()) | |
| 57 | + .defaultOptions(OpenAiChatOptions.builder() | |
| 58 | + .model(properties.getModel()) | |
| 59 | + .temperature(properties.getTemperature()) | |
| 60 | + .maxTokens(properties.getMaxTokens()) | |
| 61 | + .topP(properties.getTopP()) | |
| 62 | + .build()) | |
| 51 | 63 | .build(); |
| 52 | - return new DeepSeekChatModel(properties.getApiKey(), options); | |
| 64 | + return new DeepSeekChatModel(openAiChatModel); | |
| 53 | 65 | } |
| 54 | 66 | |
| 55 | 67 | @Bean |
| 56 | 68 | @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") |
| 57 | 69 | public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { |
| 58 | 70 | YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); |
| 59 | - XingHuoChatOptions options = XingHuoChatOptions.builder() | |
| 60 | - .model(properties.getModel()) | |
| 61 | - .temperature(properties.getTemperature()) | |
| 62 | - .maxTokens(properties.getMaxTokens()) | |
| 63 | - .topK(properties.getTopK()) | |
| 71 | + return buildXingHuoChatClient(properties); | |
| 72 | + } | |
| 73 | + | |
| 74 | + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) { | |
| 75 | + if (StrUtil.isEmpty(properties.getModel())) { | |
| 76 | + properties.setModel(XingHuoChatModel.MODEL_DEFAULT); | |
| 77 | + } | |
| 78 | + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | |
| 79 | + .openAiApi(OpenAiApi.builder() | |
| 80 | + .baseUrl(XingHuoChatModel.BASE_URL) | |
| 81 | + .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()) | |
| 82 | + .build()) | |
| 83 | + .defaultOptions(OpenAiChatOptions.builder() | |
| 84 | + .model(properties.getModel()) | |
| 85 | + .temperature(properties.getTemperature()) | |
| 86 | + .maxTokens(properties.getMaxTokens()) | |
| 87 | + .topP(properties.getTopP()) | |
| 88 | + .build()) | |
| 64 | 89 | .build(); |
| 65 | - return new XingHuoChatModel(properties.getAppKey(), properties.getSecretKey(), options); | |
| 90 | + return new XingHuoChatModel(openAiChatModel); | |
| 66 | 91 | } |
| 67 | 92 | |
| 68 | 93 | @Bean | ... | ... |
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 { |
| 42 | 42 | private String secretKey; |
| 43 | 43 | |
| 44 | 44 | private String model; |
| 45 | - private Float temperature; | |
| 45 | + private Double temperature; | |
| 46 | 46 | private Integer maxTokens; |
| 47 | - private Integer topK; | |
| 47 | + private Double topP; | |
| 48 | 48 | |
| 49 | 49 | } |
| 50 | 50 | |
| ... | ... | @@ -55,9 +55,9 @@ public class YudaoAiProperties { |
| 55 | 55 | private String apiKey; |
| 56 | 56 | |
| 57 | 57 | private String model; |
| 58 | - private Float temperature; | |
| 58 | + private Double temperature; | |
| 59 | 59 | private Integer maxTokens; |
| 60 | - private Float topP; | |
| 60 | + private Double topP; | |
| 61 | 61 | |
| 62 | 62 | } |
| 63 | 63 | ... | ... |
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; |
| 13 | 13 | import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| 14 | 14 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 15 | 15 | import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; |
| 16 | -import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; | |
| 17 | -import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; | |
| 18 | -import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; | |
| 19 | -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; | |
| 20 | -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties; | |
| 21 | -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; | |
| 22 | -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties; | |
| 23 | -import com.alibaba.dashscope.aigc.generation.Generation; | |
| 24 | -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; | |
| 25 | -import com.alibaba.dashscope.embeddings.TextEmbedding; | |
| 26 | -import com.azure.ai.openai.OpenAIClient; | |
| 16 | +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration; | |
| 17 | +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; | |
| 18 | +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; | |
| 19 | +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; | |
| 20 | +import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; | |
| 21 | +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; | |
| 22 | +import com.azure.ai.openai.OpenAIClientBuilder; | |
| 27 | 23 | import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; |
| 28 | 24 | import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; |
| 29 | 25 | import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties; |
| 30 | 26 | import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; |
| 31 | 27 | import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; |
| 32 | 28 | import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; |
| 33 | -import org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties; | |
| 34 | -import org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties; | |
| 35 | -import org.springframework.ai.autoconfigure.qianfan.QianFanImageProperties; | |
| 36 | 29 | import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; |
| 37 | -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties; | |
| 38 | 30 | import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; |
| 39 | -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties; | |
| 40 | 31 | import org.springframework.ai.azure.openai.AzureOpenAiChatModel; |
| 41 | 32 | import org.springframework.ai.chat.model.ChatModel; |
| 42 | 33 | import org.springframework.ai.embedding.EmbeddingModel; |
| 43 | 34 | import org.springframework.ai.image.ImageModel; |
| 44 | -import org.springframework.ai.model.function.FunctionCallbackContext; | |
| 45 | 35 | import org.springframework.ai.ollama.OllamaChatModel; |
| 46 | 36 | import org.springframework.ai.ollama.api.OllamaApi; |
| 47 | 37 | import org.springframework.ai.openai.OpenAiChatModel; |
| 48 | 38 | import org.springframework.ai.openai.OpenAiImageModel; |
| 49 | -import org.springframework.ai.openai.api.ApiUtils; | |
| 50 | 39 | import org.springframework.ai.openai.api.OpenAiApi; |
| 51 | 40 | import org.springframework.ai.openai.api.OpenAiImageApi; |
| 41 | +import org.springframework.ai.openai.api.common.OpenAiApiConstants; | |
| 52 | 42 | import org.springframework.ai.qianfan.QianFanChatModel; |
| 53 | 43 | import org.springframework.ai.qianfan.QianFanImageModel; |
| 54 | 44 | import org.springframework.ai.qianfan.api.QianFanApi; |
| 55 | 45 | import org.springframework.ai.qianfan.api.QianFanImageApi; |
| 56 | 46 | import org.springframework.ai.stabilityai.StabilityAiImageModel; |
| 57 | 47 | import org.springframework.ai.stabilityai.api.StabilityAiApi; |
| 58 | -import org.springframework.ai.vectorstore.RedisVectorStore; | |
| 48 | +import org.springframework.ai.vectorstore.SimpleVectorStore; | |
| 59 | 49 | import org.springframework.ai.vectorstore.VectorStore; |
| 60 | 50 | import org.springframework.ai.zhipuai.ZhiPuAiChatModel; |
| 61 | 51 | import org.springframework.ai.zhipuai.ZhiPuAiImageModel; |
| 62 | 52 | import org.springframework.ai.zhipuai.api.ZhiPuAiApi; |
| 63 | 53 | import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; |
| 64 | -import org.springframework.boot.autoconfigure.data.redis.RedisProperties; | |
| 65 | -import org.springframework.retry.support.RetryTemplate; | |
| 66 | -import org.springframework.web.client.ResponseErrorHandler; | |
| 67 | 54 | import org.springframework.web.client.RestClient; |
| 68 | -import redis.clients.jedis.JedisPooled; | |
| 69 | -import redis.clients.jedis.search.Schema; | |
| 70 | 55 | |
| 71 | 56 | import java.util.List; |
| 72 | 57 | |
| ... | ... | @@ -110,7 +95,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 110 | 95 | //noinspection EnhancedSwitchMigration |
| 111 | 96 | switch (platform) { |
| 112 | 97 | case TONG_YI: |
| 113 | - return SpringUtil.getBean(TongYiChatModel.class); | |
| 98 | + return SpringUtil.getBean(DashScopeChatModel.class); | |
| 114 | 99 | case YI_YAN: |
| 115 | 100 | return SpringUtil.getBean(QianFanChatModel.class); |
| 116 | 101 | case DEEP_SEEK: |
| ... | ... | @@ -135,7 +120,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 135 | 120 | //noinspection EnhancedSwitchMigration |
| 136 | 121 | switch (platform) { |
| 137 | 122 | case TONG_YI: |
| 138 | - return SpringUtil.getBean(TongYiImagesModel.class); | |
| 123 | + return SpringUtil.getBean(DashScopeImageModel.class); | |
| 139 | 124 | case YI_YAN: |
| 140 | 125 | return SpringUtil.getBean(QianFanImageModel.class); |
| 141 | 126 | case ZHI_PU: |
| ... | ... | @@ -202,17 +187,20 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 202 | 187 | String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url); |
| 203 | 188 | return Singleton.get(cacheKey, (Func0<VectorStore>) () -> { |
| 204 | 189 | String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); |
| 205 | - var config = RedisVectorStore.RedisVectorStoreConfig.builder() | |
| 206 | - .withIndexName(cacheKey) | |
| 207 | - .withPrefix(prefix) | |
| 208 | - .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) | |
| 209 | - .build(); | |
| 210 | - RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); | |
| 211 | - RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, | |
| 212 | - new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), | |
| 213 | - true); | |
| 214 | - redisVectorStore.afterPropertiesSet(); | |
| 215 | - return redisVectorStore; | |
| 190 | + // TODO @芋艿:先临时使用 store | |
| 191 | + return SimpleVectorStore.builder(embeddingModel).build(); | |
| 192 | + // TODO @芋艿:@xin:后续看看,是不是切到阿里云之类的 | |
| 193 | +// var config = RedisVectorStore.RedisVectorStoreConfig.builder() | |
| 194 | +// .withIndexName(cacheKey) | |
| 195 | +// .withPrefix(prefix) | |
| 196 | +// .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) | |
| 197 | +// .build(); | |
| 198 | +// RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); | |
| 199 | +// RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, | |
| 200 | +// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), | |
| 201 | +// true); | |
| 202 | +// redisVectorStore.afterPropertiesSet(); | |
| 203 | +// return redisVectorStore; | |
| 216 | 204 | }); |
| 217 | 205 | } |
| 218 | 206 | |
| ... | ... | @@ -226,29 +214,23 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 226 | 214 | // ========== 各种创建 spring-ai 客户端的方法 ========== |
| 227 | 215 | |
| 228 | 216 | /** |
| 229 | - * 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)} | |
| 217 | + * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法 | |
| 230 | 218 | */ |
| 231 | - private static TongYiChatModel buildTongYiChatModel(String key) { | |
| 232 | - com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class); | |
| 233 | - TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class); | |
| 234 | - // TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下 | |
| 235 | - // TODO @芋艿:貌似阿里云不是增量返回的 | |
| 236 | - // 该 issue 进行跟进中 https://github.com/alibaba/spring-cloud-alibaba/issues/3790 | |
| 237 | - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); | |
| 238 | - connectionProperties.setApiKey(key); | |
| 239 | - return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties); | |
| 219 | + private static DashScopeChatModel buildTongYiChatModel(String key) { | |
| 220 | + DashScopeApi dashScopeApi = new DashScopeApi(key); | |
| 221 | + return new DashScopeChatModel(dashScopeApi); | |
| 240 | 222 | } |
| 241 | 223 | |
| 242 | - private static TongYiImagesModel buildTongYiImagesModel(String key) { | |
| 243 | - ImageSynthesis imageSynthesis = SpringUtil.getBean(ImageSynthesis.class); | |
| 244 | - TongYiImagesProperties imagesOptions = SpringUtil.getBean(TongYiImagesProperties.class); | |
| 245 | - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); | |
| 246 | - connectionProperties.setApiKey(key); | |
| 247 | - return new TongYiAutoConfiguration().tongYiImagesClient(imageSynthesis, imagesOptions, connectionProperties); | |
| 224 | + /** | |
| 225 | + * 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法 | |
| 226 | + */ | |
| 227 | + private static DashScopeImageModel buildTongYiImagesModel(String key) { | |
| 228 | + DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); | |
| 229 | + return new DashScopeImageModel(dashScopeImageApi); | |
| 248 | 230 | } |
| 249 | 231 | |
| 250 | 232 | /** |
| 251 | - * 可参考 {@link QianFanAutoConfiguration#qianFanChatModel(QianFanConnectionProperties, QianFanChatProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} | |
| 233 | + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法 | |
| 252 | 234 | */ |
| 253 | 235 | private static QianFanChatModel buildYiYanChatModel(String key) { |
| 254 | 236 | List<String> keys = StrUtil.split(key, '|'); |
| ... | ... | @@ -260,7 +242,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 260 | 242 | } |
| 261 | 243 | |
| 262 | 244 | /** |
| 263 | - * 可参考 {@link QianFanAutoConfiguration#qianFanImageModel(QianFanConnectionProperties, QianFanImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} | |
| 245 | + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法 | |
| 264 | 246 | */ |
| 265 | 247 | private QianFanImageModel buildQianFanImageModel(String key) { |
| 266 | 248 | List<String> keys = StrUtil.split(key, '|'); |
| ... | ... | @@ -275,11 +257,13 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 275 | 257 | * 可参考 {@link YudaoAiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} |
| 276 | 258 | */ |
| 277 | 259 | private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { |
| 278 | - return new DeepSeekChatModel(apiKey); | |
| 260 | + YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties() | |
| 261 | + .setApiKey(apiKey); | |
| 262 | + return new YudaoAiAutoConfiguration().buildDeepSeekChatModel(properties); | |
| 279 | 263 | } |
| 280 | 264 | |
| 281 | 265 | /** |
| 282 | - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} | |
| 266 | + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 | |
| 283 | 267 | */ |
| 284 | 268 | private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { |
| 285 | 269 | url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); |
| ... | ... | @@ -288,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 288 | 272 | } |
| 289 | 273 | |
| 290 | 274 | /** |
| 291 | - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} | |
| 275 | + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 | |
| 292 | 276 | */ |
| 293 | 277 | private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { |
| 294 | 278 | url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); |
| ... | ... | @@ -301,21 +285,22 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 301 | 285 | */ |
| 302 | 286 | private static XingHuoChatModel buildXingHuoChatModel(String key) { |
| 303 | 287 | List<String> keys = StrUtil.split(key, '|'); |
| 304 | - Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); | |
| 305 | - String appKey = keys.get(1); | |
| 306 | - String secretKey = keys.get(2); | |
| 307 | - return new XingHuoChatModel(appKey, secretKey); | |
| 288 | + Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); | |
| 289 | + YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties() | |
| 290 | + .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); | |
| 291 | + return new YudaoAiAutoConfiguration().buildXingHuoChatClient(properties); | |
| 308 | 292 | } |
| 309 | 293 | |
| 310 | 294 | /** |
| 311 | - * 可参考 {@link OpenAiAutoConfiguration} | |
| 295 | + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 | |
| 312 | 296 | */ |
| 313 | 297 | private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { |
| 314 | - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); | |
| 315 | - OpenAiApi openAiApi = new OpenAiApi(url, openAiToken); | |
| 316 | - return new OpenAiChatModel(openAiApi); | |
| 298 | + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); | |
| 299 | + OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); | |
| 300 | + return OpenAiChatModel.builder().openAiApi(openAiApi).build(); | |
| 317 | 301 | } |
| 318 | 302 | |
| 303 | + // TODO @芋艿:手头暂时没密钥,使用建议再测试下 | |
| 319 | 304 | /** |
| 320 | 305 | * 可参考 {@link AzureOpenAiAutoConfiguration} |
| 321 | 306 | */ |
| ... | ... | @@ -325,27 +310,28 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 325 | 310 | AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); |
| 326 | 311 | connectionProperties.setApiKey(apiKey); |
| 327 | 312 | connectionProperties.setEndpoint(url); |
| 328 | - OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties); | |
| 313 | + OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); | |
| 329 | 314 | // 获取 AzureOpenAiChatProperties 对象 |
| 330 | 315 | AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); |
| 331 | - return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null); | |
| 316 | + return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, | |
| 317 | + null, null, null); | |
| 332 | 318 | } |
| 333 | 319 | |
| 334 | 320 | /** |
| 335 | - * 可参考 {@link OpenAiAutoConfiguration} | |
| 321 | + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法 | |
| 336 | 322 | */ |
| 337 | 323 | private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { |
| 338 | - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); | |
| 339 | - OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder()); | |
| 324 | + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); | |
| 325 | + OpenAiImageApi openAiApi = OpenAiImageApi.builder().baseUrl(url).apiKey(openAiToken).build(); | |
| 340 | 326 | return new OpenAiImageModel(openAiApi); |
| 341 | 327 | } |
| 342 | 328 | |
| 343 | 329 | /** |
| 344 | - * 可参考 {@link OllamaAutoConfiguration} | |
| 330 | + * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 | |
| 345 | 331 | */ |
| 346 | 332 | private static OllamaChatModel buildOllamaChatModel(String url) { |
| 347 | 333 | OllamaApi ollamaApi = new OllamaApi(url); |
| 348 | - return new OllamaChatModel(ollamaApi); | |
| 334 | + return OllamaChatModel.builder().ollamaApi(ollamaApi).build(); | |
| 349 | 335 | } |
| 350 | 336 | |
| 351 | 337 | private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { |
| ... | ... | @@ -356,13 +342,13 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 356 | 342 | |
| 357 | 343 | // ========== 各种创建 EmbeddingModel 的方法 ========== |
| 358 | 344 | |
| 345 | + // TODO @芋艿:需要测试下 | |
| 359 | 346 | /** |
| 360 | - * 可参考 {@link TongYiAutoConfiguration#tongYiTextEmbeddingClient(TextEmbedding, TongYiConnectionProperties)} | |
| 347 | + * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 | |
| 361 | 348 | */ |
| 362 | 349 | private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) { |
| 363 | - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); | |
| 364 | - connectionProperties.setApiKey(apiKey); | |
| 365 | - return new TongYiAutoConfiguration().tongYiTextEmbeddingClient(SpringUtil.getBean(TextEmbedding.class), connectionProperties); | |
| 350 | + DashScopeApi dashScopeApi = new DashScopeApi(apiKey); | |
| 351 | + return new DashScopeEmbeddingModel(dashScopeApi); | |
| 366 | 352 | } |
| 367 | 353 | |
| 368 | 354 | } | ... | ... |
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; |
| 8 | 8 | import lombok.Data; |
| 9 | 9 | import lombok.Getter; |
| 10 | 10 | import lombok.extern.slf4j.Slf4j; |
| 11 | -import org.springframework.ai.openai.api.ApiUtils; | |
| 11 | +import org.springframework.http.HttpHeaders; | |
| 12 | 12 | import org.springframework.http.HttpRequest; |
| 13 | 13 | import org.springframework.http.HttpStatusCode; |
| 14 | +import org.springframework.http.MediaType; | |
| 14 | 15 | import org.springframework.web.reactive.function.client.ClientResponse; |
| 15 | 16 | import org.springframework.web.reactive.function.client.WebClient; |
| 16 | 17 | import reactor.core.publisher.Mono; |
| ... | ... | @@ -18,6 +19,7 @@ import reactor.core.publisher.Mono; |
| 18 | 19 | import java.util.Collection; |
| 19 | 20 | import java.util.List; |
| 20 | 21 | import java.util.Map; |
| 22 | +import java.util.function.Consumer; | |
| 21 | 23 | import java.util.function.Function; |
| 22 | 24 | import java.util.function.Predicate; |
| 23 | 25 | |
| ... | ... | @@ -50,11 +52,19 @@ public class MidjourneyApi { |
| 50 | 52 | public MidjourneyApi(String baseUrl, String apiKey, String notifyUrl) { |
| 51 | 53 | this.webClient = WebClient.builder() |
| 52 | 54 | .baseUrl(baseUrl) |
| 53 | - .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)) | |
| 55 | + .defaultHeaders(getJsonContentHeaders(apiKey)) | |
| 54 | 56 | .build(); |
| 55 | 57 | this.notifyUrl = notifyUrl; |
| 56 | 58 | } |
| 57 | 59 | |
| 60 | + // TODO @芋艿:这里,看看怎么调整下???https://github.com/spring-projects/spring-ai/issues/741 | |
| 61 | + public static Consumer<HttpHeaders> getJsonContentHeaders(String apiKey) { | |
| 62 | + return (headers) -> { | |
| 63 | + headers.setBearerAuth(apiKey); | |
| 64 | + headers.setContentType(MediaType.APPLICATION_JSON); | |
| 65 | + }; | |
| 66 | + }; | |
| 67 | + | |
| 58 | 68 | /** |
| 59 | 69 | * imagine - 根据提示词提交绘画任务 |
| 60 | 70 | * | ... | ... |
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; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; |
| 5 | -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; | |
| 6 | -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; | |
| 7 | -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; | |
| 5 | +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; | |
| 8 | 6 | import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; |
| 9 | 7 | import org.springframework.ai.chat.messages.*; |
| 10 | 8 | import org.springframework.ai.chat.prompt.ChatOptions; |
| ... | ... | @@ -21,26 +19,24 @@ import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; |
| 21 | 19 | public class AiUtils { |
| 22 | 20 | |
| 23 | 21 | public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { |
| 24 | - Float temperatureF = temperature != null ? temperature.floatValue() : null; | |
| 25 | 22 | //noinspection EnhancedSwitchMigration |
| 26 | 23 | switch (platform) { |
| 27 | 24 | case TONG_YI: |
| 28 | - return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build(); | |
| 25 | + // TODO @芋艿:tongyi 暂时没 maxTokens 选项 | |
| 26 | + return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).build(); | |
| 29 | 27 | case YI_YAN: |
| 30 | - return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); | |
| 31 | - case DEEP_SEEK: | |
| 32 | - return DeepSeekChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); | |
| 28 | + return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | |
| 33 | 29 | case ZHI_PU: |
| 34 | - return ZhiPuAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); | |
| 35 | - case XING_HUO: | |
| 36 | - return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); | |
| 30 | + return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | |
| 37 | 31 | case OPENAI: |
| 38 | - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); | |
| 32 | + case DEEP_SEEK: // 复用 OpenAI 客户端 | |
| 33 | + case XING_HUO: // 复用 OpenAI 客户端 | |
| 34 | + return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | |
| 39 | 35 | case AZURE_OPENAI: |
| 40 | 36 | // TODO 芋艿:貌似没 model 字段???! |
| 41 | - return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); | |
| 37 | + return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); | |
| 42 | 38 | case OLLAMA: |
| 43 | - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens); | |
| 39 | + return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens).build(); | |
| 44 | 40 | default: |
| 45 | 41 | throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); |
| 46 | 42 | } |
| ... | ... | @@ -56,8 +52,8 @@ public class AiUtils { |
| 56 | 52 | if (MessageType.SYSTEM.getValue().equals(type)) { |
| 57 | 53 | return new SystemMessage(content); |
| 58 | 54 | } |
| 59 | - if (MessageType.FUNCTION.getValue().equals(type)) { | |
| 60 | - return new FunctionMessage(content); | |
| 55 | + if (MessageType.TOOL.getValue().equals(type)) { | |
| 56 | + throw new UnsupportedOperationException("暂不支持 tool 消息:" + content); | |
| 61 | 57 | } |
| 62 | 58 | throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type)); |
| 63 | 59 | } | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java deleted
| 1 | -/* | |
| 2 | - * Copyright 2023 - 2024 the original author or authors. | |
| 3 | - * | |
| 4 | - * Licensed under the Apache License, Version 2.0 (the "License"); | |
| 5 | - * you may not use this file except in compliance with the License. | |
| 6 | - * You may obtain a copy of the License at | |
| 7 | - * | |
| 8 | - * https://www.apache.org/licenses/LICENSE-2.0 | |
| 9 | - * | |
| 10 | - * Unless required by applicable law or agreed to in writing, software | |
| 11 | - * distributed under the License is distributed on an "AS IS" BASIS, | |
| 12 | - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 13 | - * See the License for the specific language governing permissions and | |
| 14 | - * limitations under the License. | |
| 15 | - */ | |
| 16 | -package org.springframework.ai.autoconfigure.vectorstore.redis; | |
| 17 | - | |
| 18 | -import org.springframework.ai.embedding.EmbeddingModel; | |
| 19 | -import org.springframework.ai.vectorstore.RedisVectorStore; | |
| 20 | -import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig; | |
| 21 | -import org.springframework.boot.autoconfigure.AutoConfiguration; | |
| 22 | -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; | |
| 23 | -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; | |
| 24 | -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | |
| 25 | -import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; | |
| 26 | -import org.springframework.boot.context.properties.EnableConfigurationProperties; | |
| 27 | -import org.springframework.context.annotation.Bean; | |
| 28 | -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; | |
| 29 | -import redis.clients.jedis.JedisPooled; | |
| 30 | - | |
| 31 | -/** | |
| 32 | - * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突 | |
| 33 | - * | |
| 34 | - * TODO 这个官方,有说啥时候 fix 哇? | |
| 35 | - * TODO 看着是列在1.0.0-M2版本 | |
| 36 | - * | |
| 37 | - * @author Christian Tzolov | |
| 38 | - * @author Eddú Meléndez | |
| 39 | - */ | |
| 40 | -@AutoConfiguration(after = RedisAutoConfiguration.class) | |
| 41 | -@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class}) | |
| 42 | -@ConditionalOnBean(JedisConnectionFactory.class) | |
| 43 | -@EnableConfigurationProperties(RedisVectorStoreProperties.class) | |
| 44 | -public class RedisVectorStoreAutoConfiguration { | |
| 45 | - | |
| 46 | - @Bean | |
| 47 | - @ConditionalOnMissingBean | |
| 48 | - public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties, | |
| 49 | - JedisConnectionFactory jedisConnectionFactory) { | |
| 50 | - | |
| 51 | - var config = RedisVectorStoreConfig.builder() | |
| 52 | - .withIndexName(properties.getIndex()) | |
| 53 | - .withPrefix(properties.getPrefix()) | |
| 54 | - .build(); | |
| 55 | - | |
| 56 | - return new RedisVectorStore(config, embeddingModel, | |
| 57 | - new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()), | |
| 58 | - properties.isInitializeSchema()); | |
| 59 | - } | |
| 60 | - | |
| 61 | -} |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java deleted
| 1 | -/* | |
| 2 | - * Copyright 2023 - 2024 the original author or authors. | |
| 3 | - * | |
| 4 | - * Licensed under the Apache License, Version 2.0 (the "License"); | |
| 5 | - * you may not use this file except in compliance with the License. | |
| 6 | - * You may obtain a copy of the License at | |
| 7 | - * | |
| 8 | - * https://www.apache.org/licenses/LICENSE-2.0 | |
| 9 | - * | |
| 10 | - * Unless required by applicable law or agreed to in writing, software | |
| 11 | - * distributed under the License is distributed on an "AS IS" BASIS, | |
| 12 | - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 13 | - * See the License for the specific language governing permissions and | |
| 14 | - * limitations under the License. | |
| 15 | - */ | |
| 16 | -package org.springframework.ai.vectorstore; | |
| 17 | - | |
| 18 | -import org.slf4j.Logger; | |
| 19 | -import org.slf4j.LoggerFactory; | |
| 20 | -import org.springframework.ai.document.Document; | |
| 21 | -import org.springframework.ai.embedding.EmbeddingModel; | |
| 22 | -import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; | |
| 23 | -import org.springframework.beans.factory.InitializingBean; | |
| 24 | -import org.springframework.util.Assert; | |
| 25 | -import org.springframework.util.CollectionUtils; | |
| 26 | -import redis.clients.jedis.JedisPooled; | |
| 27 | -import redis.clients.jedis.Pipeline; | |
| 28 | -import redis.clients.jedis.json.Path2; | |
| 29 | -import redis.clients.jedis.search.*; | |
| 30 | -import redis.clients.jedis.search.Schema.FieldType; | |
| 31 | -import redis.clients.jedis.search.schemafields.*; | |
| 32 | -import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm; | |
| 33 | - | |
| 34 | -import java.text.MessageFormat; | |
| 35 | -import java.util.*; | |
| 36 | -import java.util.function.Function; | |
| 37 | -import java.util.function.Predicate; | |
| 38 | -import java.util.stream.Collectors; | |
| 39 | - | |
| 40 | -/** | |
| 41 | - * The RedisVectorStore is for managing and querying vector data in a Redis database. It | |
| 42 | - * offers functionalities like adding, deleting, and performing similarity searches on | |
| 43 | - * documents. | |
| 44 | - * | |
| 45 | - * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and | |
| 46 | - * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for | |
| 47 | - * efficient similarity searches. Additionally, it allows for custom metadata fields in | |
| 48 | - * the documents to be stored alongside the vector and content data. | |
| 49 | - * | |
| 50 | - * This class requires a RedisVectorStoreConfig configuration object for initialization, | |
| 51 | - * which includes settings like Redis URI, index name, field names, and vector algorithms. | |
| 52 | - * It also requires an EmbeddingModel to convert documents into embeddings before storing | |
| 53 | - * them. | |
| 54 | - * | |
| 55 | - * @author Julien Ruaux | |
| 56 | - * @author Christian Tzolov | |
| 57 | - * @author Eddú Meléndez | |
| 58 | - * @see VectorStore | |
| 59 | - * @see RedisVectorStoreConfig | |
| 60 | - * @see EmbeddingModel | |
| 61 | - */ | |
| 62 | -public class RedisVectorStore implements VectorStore, InitializingBean { | |
| 63 | - | |
| 64 | - public enum Algorithm { | |
| 65 | - | |
| 66 | - FLAT, HSNW | |
| 67 | - | |
| 68 | - } | |
| 69 | - | |
| 70 | - public record MetadataField(String name, FieldType fieldType) { | |
| 71 | - | |
| 72 | - public static MetadataField text(String name) { | |
| 73 | - return new MetadataField(name, FieldType.TEXT); | |
| 74 | - } | |
| 75 | - | |
| 76 | - public static MetadataField numeric(String name) { | |
| 77 | - return new MetadataField(name, FieldType.NUMERIC); | |
| 78 | - } | |
| 79 | - | |
| 80 | - public static MetadataField tag(String name) { | |
| 81 | - return new MetadataField(name, FieldType.TAG); | |
| 82 | - } | |
| 83 | - | |
| 84 | - } | |
| 85 | - | |
| 86 | - /** | |
| 87 | - * Configuration for the Redis vector store. | |
| 88 | - */ | |
| 89 | - public static final class RedisVectorStoreConfig { | |
| 90 | - | |
| 91 | - private final String indexName; | |
| 92 | - | |
| 93 | - private final String prefix; | |
| 94 | - | |
| 95 | - private final String contentFieldName; | |
| 96 | - | |
| 97 | - private final String embeddingFieldName; | |
| 98 | - | |
| 99 | - private final Algorithm vectorAlgorithm; | |
| 100 | - | |
| 101 | - private final List<MetadataField> metadataFields; | |
| 102 | - | |
| 103 | - private RedisVectorStoreConfig() { | |
| 104 | - this(builder()); | |
| 105 | - } | |
| 106 | - | |
| 107 | - private RedisVectorStoreConfig(Builder builder) { | |
| 108 | - this.indexName = builder.indexName; | |
| 109 | - this.prefix = builder.prefix; | |
| 110 | - this.contentFieldName = builder.contentFieldName; | |
| 111 | - this.embeddingFieldName = builder.embeddingFieldName; | |
| 112 | - this.vectorAlgorithm = builder.vectorAlgorithm; | |
| 113 | - this.metadataFields = builder.metadataFields; | |
| 114 | - } | |
| 115 | - | |
| 116 | - /** | |
| 117 | - * Start building a new configuration. | |
| 118 | - * @return The entry point for creating a new configuration. | |
| 119 | - */ | |
| 120 | - public static Builder builder() { | |
| 121 | - | |
| 122 | - return new Builder(); | |
| 123 | - } | |
| 124 | - | |
| 125 | - /** | |
| 126 | - * {@return the default config} | |
| 127 | - */ | |
| 128 | - public static RedisVectorStoreConfig defaultConfig() { | |
| 129 | - | |
| 130 | - return builder().build(); | |
| 131 | - } | |
| 132 | - | |
| 133 | - public static class Builder { | |
| 134 | - | |
| 135 | - private String indexName = DEFAULT_INDEX_NAME; | |
| 136 | - | |
| 137 | - private String prefix = DEFAULT_PREFIX; | |
| 138 | - | |
| 139 | - private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME; | |
| 140 | - | |
| 141 | - private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME; | |
| 142 | - | |
| 143 | - private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM; | |
| 144 | - | |
| 145 | - private List<MetadataField> metadataFields = new ArrayList<>(); | |
| 146 | - | |
| 147 | - private Builder() { | |
| 148 | - } | |
| 149 | - | |
| 150 | - /** | |
| 151 | - * Configures the Redis index name to use. | |
| 152 | - * @param name the index name to use | |
| 153 | - * @return this builder | |
| 154 | - */ | |
| 155 | - public Builder withIndexName(String name) { | |
| 156 | - this.indexName = name; | |
| 157 | - return this; | |
| 158 | - } | |
| 159 | - | |
| 160 | - /** | |
| 161 | - * Configures the Redis key prefix to use (default: "embedding:"). | |
| 162 | - * @param prefix the prefix to use | |
| 163 | - * @return this builder | |
| 164 | - */ | |
| 165 | - public Builder withPrefix(String prefix) { | |
| 166 | - this.prefix = prefix; | |
| 167 | - return this; | |
| 168 | - } | |
| 169 | - | |
| 170 | - /** | |
| 171 | - * Configures the Redis content field name to use. | |
| 172 | - * @param name the content field name to use | |
| 173 | - * @return this builder | |
| 174 | - */ | |
| 175 | - public Builder withContentFieldName(String name) { | |
| 176 | - this.contentFieldName = name; | |
| 177 | - return this; | |
| 178 | - } | |
| 179 | - | |
| 180 | - /** | |
| 181 | - * Configures the Redis embedding field name to use. | |
| 182 | - * @param name the embedding field name to use | |
| 183 | - * @return this builder | |
| 184 | - */ | |
| 185 | - public Builder withEmbeddingFieldName(String name) { | |
| 186 | - this.embeddingFieldName = name; | |
| 187 | - return this; | |
| 188 | - } | |
| 189 | - | |
| 190 | - /** | |
| 191 | - * Configures the Redis vector algorithmto use. | |
| 192 | - * @param algorithm the vector algorithm to use | |
| 193 | - * @return this builder | |
| 194 | - */ | |
| 195 | - public Builder withVectorAlgorithm(Algorithm algorithm) { | |
| 196 | - this.vectorAlgorithm = algorithm; | |
| 197 | - return this; | |
| 198 | - } | |
| 199 | - | |
| 200 | - public Builder withMetadataFields(MetadataField... fields) { | |
| 201 | - return withMetadataFields(Arrays.asList(fields)); | |
| 202 | - } | |
| 203 | - | |
| 204 | - public Builder withMetadataFields(List<MetadataField> fields) { | |
| 205 | - this.metadataFields = fields; | |
| 206 | - return this; | |
| 207 | - } | |
| 208 | - | |
| 209 | - /** | |
| 210 | - * {@return the immutable configuration} | |
| 211 | - */ | |
| 212 | - public RedisVectorStoreConfig build() { | |
| 213 | - | |
| 214 | - return new RedisVectorStoreConfig(this); | |
| 215 | - } | |
| 216 | - | |
| 217 | - } | |
| 218 | - | |
| 219 | - } | |
| 220 | - | |
| 221 | - private final boolean initializeSchema; | |
| 222 | - | |
| 223 | - public static final String DEFAULT_INDEX_NAME = "spring-ai-index"; | |
| 224 | - | |
| 225 | - public static final String DEFAULT_CONTENT_FIELD_NAME = "content"; | |
| 226 | - | |
| 227 | - public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding"; | |
| 228 | - | |
| 229 | - public static final String DEFAULT_PREFIX = "embedding:"; | |
| 230 | - | |
| 231 | - public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW; | |
| 232 | - | |
| 233 | - private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]"; | |
| 234 | - | |
| 235 | - private static final Path2 JSON_SET_PATH = Path2.of("$"); | |
| 236 | - | |
| 237 | - private static final String JSON_PATH_PREFIX = "$."; | |
| 238 | - | |
| 239 | - private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class); | |
| 240 | - | |
| 241 | - private static final Predicate<Object> RESPONSE_OK = Predicate.isEqual("OK"); | |
| 242 | - | |
| 243 | - private static final Predicate<Object> RESPONSE_DEL_OK = Predicate.isEqual(1l); | |
| 244 | - | |
| 245 | - private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32"; | |
| 246 | - | |
| 247 | - private static final String EMBEDDING_PARAM_NAME = "BLOB"; | |
| 248 | - | |
| 249 | - public static final String DISTANCE_FIELD_NAME = "vector_score"; | |
| 250 | - | |
| 251 | - private static final String DEFAULT_DISTANCE_METRIC = "COSINE"; | |
| 252 | - | |
| 253 | - private final JedisPooled jedis; | |
| 254 | - | |
| 255 | - private final EmbeddingModel embeddingModel; | |
| 256 | - | |
| 257 | - private final RedisVectorStoreConfig config; | |
| 258 | - | |
| 259 | - private FilterExpressionConverter filterExpressionConverter; | |
| 260 | - | |
| 261 | - public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis, | |
| 262 | - boolean initializeSchema) { | |
| 263 | - | |
| 264 | - Assert.notNull(config, "Config must not be null"); | |
| 265 | - Assert.notNull(embeddingModel, "Embedding model must not be null"); | |
| 266 | - this.initializeSchema = initializeSchema; | |
| 267 | - | |
| 268 | - this.jedis = jedis; | |
| 269 | - this.embeddingModel = embeddingModel; | |
| 270 | - this.config = config; | |
| 271 | - this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields); | |
| 272 | - } | |
| 273 | - | |
| 274 | - public JedisPooled getJedis() { | |
| 275 | - return this.jedis; | |
| 276 | - } | |
| 277 | - | |
| 278 | - @Override | |
| 279 | - public void add(List<Document> documents) { | |
| 280 | - try (Pipeline pipeline = this.jedis.pipelined()) { | |
| 281 | - for (Document document : documents) { | |
| 282 | - var embedding = this.embeddingModel.embed(document); | |
| 283 | - document.setEmbedding(embedding); | |
| 284 | - | |
| 285 | - var fields = new HashMap<String, Object>(); | |
| 286 | - fields.put(this.config.embeddingFieldName, embedding); | |
| 287 | - fields.put(this.config.contentFieldName, document.getContent()); | |
| 288 | - fields.putAll(document.getMetadata()); | |
| 289 | - pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields); | |
| 290 | - } | |
| 291 | - List<Object> responses = pipeline.syncAndReturnAll(); | |
| 292 | - Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny(); | |
| 293 | - if (errResponse.isPresent()) { | |
| 294 | - String message = MessageFormat.format("Could not add document: {0}", errResponse.get()); | |
| 295 | - if (logger.isErrorEnabled()) { | |
| 296 | - logger.error(message); | |
| 297 | - } | |
| 298 | - throw new RuntimeException(message); | |
| 299 | - } | |
| 300 | - } | |
| 301 | - } | |
| 302 | - | |
| 303 | - private String key(String id) { | |
| 304 | - return this.config.prefix + id; | |
| 305 | - } | |
| 306 | - | |
| 307 | - @Override | |
| 308 | - public Optional<Boolean> delete(List<String> idList) { | |
| 309 | - try (Pipeline pipeline = this.jedis.pipelined()) { | |
| 310 | - for (String id : idList) { | |
| 311 | - pipeline.jsonDel(key(id)); | |
| 312 | - } | |
| 313 | - List<Object> responses = pipeline.syncAndReturnAll(); | |
| 314 | - Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny(); | |
| 315 | - if (errResponse.isPresent()) { | |
| 316 | - if (logger.isErrorEnabled()) { | |
| 317 | - logger.error("Could not delete document: {}", errResponse.get()); | |
| 318 | - } | |
| 319 | - return Optional.of(false); | |
| 320 | - } | |
| 321 | - return Optional.of(true); | |
| 322 | - } | |
| 323 | - } | |
| 324 | - | |
| 325 | - @Override | |
| 326 | - public List<Document> similaritySearch(SearchRequest request) { | |
| 327 | - | |
| 328 | - Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero"); | |
| 329 | - Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1, | |
| 330 | - "The similarity score is bounded between 0 and 1; least to most similar respectively."); | |
| 331 | - | |
| 332 | - String filter = nativeExpressionFilter(request); | |
| 333 | - | |
| 334 | - String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName, | |
| 335 | - EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME); | |
| 336 | - | |
| 337 | - List<String> returnFields = new ArrayList<>(); | |
| 338 | - this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add); | |
| 339 | - returnFields.add(this.config.embeddingFieldName); | |
| 340 | - returnFields.add(this.config.contentFieldName); | |
| 341 | - returnFields.add(DISTANCE_FIELD_NAME); | |
| 342 | - var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery())); | |
| 343 | - Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding)) | |
| 344 | - .returnFields(returnFields.toArray(new String[0])) | |
| 345 | - .setSortBy(DISTANCE_FIELD_NAME, true) | |
| 346 | - .dialect(2); | |
| 347 | - | |
| 348 | - SearchResult result = this.jedis.ftSearch(this.config.indexName, query); | |
| 349 | - return result.getDocuments() | |
| 350 | - .stream() | |
| 351 | - .filter(d -> similarityScore(d) >= request.getSimilarityThreshold()) | |
| 352 | - .map(this::toDocument) | |
| 353 | - .toList(); | |
| 354 | - } | |
| 355 | - | |
| 356 | - private Document toDocument(redis.clients.jedis.search.Document doc) { | |
| 357 | - var id = doc.getId().substring(this.config.prefix.length()); | |
| 358 | - var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName) | |
| 359 | - : null; | |
| 360 | - Map<String, Object> metadata = this.config.metadataFields.stream() | |
| 361 | - .map(MetadataField::name) | |
| 362 | - .filter(doc::hasProperty) | |
| 363 | - .collect(Collectors.toMap(Function.identity(), doc::getString)); | |
| 364 | - metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc)); | |
| 365 | - return new Document(id, content, metadata); | |
| 366 | - } | |
| 367 | - | |
| 368 | - private float similarityScore(redis.clients.jedis.search.Document doc) { | |
| 369 | - return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2; | |
| 370 | - } | |
| 371 | - | |
| 372 | - private String nativeExpressionFilter(SearchRequest request) { | |
| 373 | - if (request.getFilterExpression() == null) { | |
| 374 | - return "*"; | |
| 375 | - } | |
| 376 | - return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")"; | |
| 377 | - } | |
| 378 | - | |
| 379 | - @Override | |
| 380 | - public void afterPropertiesSet() { | |
| 381 | - | |
| 382 | - if (!this.initializeSchema) { | |
| 383 | - return; | |
| 384 | - } | |
| 385 | - | |
| 386 | - // If index already exists don't do anything | |
| 387 | - if (this.jedis.ftList().contains(this.config.indexName)) { | |
| 388 | - return; | |
| 389 | - } | |
| 390 | - | |
| 391 | - String response = this.jedis.ftCreate(this.config.indexName, | |
| 392 | - FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields()); | |
| 393 | - if (!RESPONSE_OK.test(response)) { | |
| 394 | - String message = MessageFormat.format("Could not create index: {0}", response); | |
| 395 | - throw new RuntimeException(message); | |
| 396 | - } | |
| 397 | - } | |
| 398 | - | |
| 399 | - private Iterable<SchemaField> schemaFields() { | |
| 400 | - Map<String, Object> vectorAttrs = new HashMap<>(); | |
| 401 | - vectorAttrs.put("DIM", this.embeddingModel.dimensions()); | |
| 402 | - vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC); | |
| 403 | - vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32); | |
| 404 | - List<SchemaField> fields = new ArrayList<>(); | |
| 405 | - fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0)); | |
| 406 | - fields.add(VectorField.builder() | |
| 407 | - .fieldName(jsonPath(this.config.embeddingFieldName)) | |
| 408 | - .algorithm(vectorAlgorithm()) | |
| 409 | - .attributes(vectorAttrs) | |
| 410 | - .as(this.config.embeddingFieldName) | |
| 411 | - .build()); | |
| 412 | - | |
| 413 | - if (!CollectionUtils.isEmpty(this.config.metadataFields)) { | |
| 414 | - for (MetadataField field : this.config.metadataFields) { | |
| 415 | - fields.add(schemaField(field)); | |
| 416 | - } | |
| 417 | - } | |
| 418 | - return fields; | |
| 419 | - } | |
| 420 | - | |
| 421 | - private SchemaField schemaField(MetadataField field) { | |
| 422 | - String fieldName = jsonPath(field.name); | |
| 423 | - switch (field.fieldType) { | |
| 424 | - case NUMERIC: | |
| 425 | - return NumericField.of(fieldName).as(field.name); | |
| 426 | - case TAG: | |
| 427 | - return TagField.of(fieldName).as(field.name); | |
| 428 | - case TEXT: | |
| 429 | - return TextField.of(fieldName).as(field.name); | |
| 430 | - default: | |
| 431 | - throw new IllegalArgumentException( | |
| 432 | - MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType)); | |
| 433 | - } | |
| 434 | - } | |
| 435 | - | |
| 436 | - private VectorAlgorithm vectorAlgorithm() { | |
| 437 | - if (config.vectorAlgorithm == Algorithm.HSNW) { | |
| 438 | - return VectorAlgorithm.HNSW; | |
| 439 | - } | |
| 440 | - return VectorAlgorithm.FLAT; | |
| 441 | - } | |
| 442 | - | |
| 443 | - private String jsonPath(String field) { | |
| 444 | - return JSON_PATH_PREFIX + field; | |
| 445 | - } | |
| 446 | - | |
| 447 | - private static float[] toFloatArray(List<Double> embeddingDouble) { | |
| 448 | - float[] embeddingFloat = new float[embeddingDouble.size()]; | |
| 449 | - int i = 0; | |
| 450 | - for (Double d : embeddingDouble) { | |
| 451 | - embeddingFloat[i++] = d.floatValue(); | |
| 452 | - } | |
| 453 | - return embeddingFloat; | |
| 454 | - } | |
| 455 | - | |
| 456 | -} | |
| 457 | 0 | \ No newline at end of file |
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java
| 1 | 1 | package cn.iocoder.yudao.framework.ai.chat; |
| 2 | 2 | |
| 3 | -import com.azure.ai.openai.OpenAIClient; | |
| 4 | 3 | import com.azure.ai.openai.OpenAIClientBuilder; |
| 5 | 4 | import com.azure.core.credential.AzureKeyCredential; |
| 6 | 5 | import com.azure.core.util.ClientOptions; |
| ... | ... | @@ -27,13 +26,13 @@ import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatP |
| 27 | 26 | */ |
| 28 | 27 | public class AzureOpenAIChatModelTests { |
| 29 | 28 | |
| 30 | - private final OpenAIClient openAiApi = (new OpenAIClientBuilder()) | |
| 29 | + // TODO @芋艿:晚点在调整 | |
| 30 | + private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() | |
| 31 | 31 | .endpoint("https://eastusprejade.openai.azure.com") |
| 32 | 32 | .credential(new AzureKeyCredential("xxx")) |
| 33 | - .clientOptions((new ClientOptions()).setApplicationId("spring-ai")) | |
| 34 | - .buildClient(); | |
| 33 | + .clientOptions((new ClientOptions()).setApplicationId("spring-ai")); | |
| 35 | 34 | private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi, |
| 36 | - AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build()); | |
| 35 | + AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); | |
| 37 | 36 | |
| 38 | 37 | @Test |
| 39 | 38 | @Disabled | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java
| 1 | 1 | package cn.iocoder.yudao.framework.ai.chat; |
| 2 | 2 | |
| 3 | -import org.junit.jupiter.api.Disabled; | |
| 4 | -import org.junit.jupiter.api.Test; | |
| 5 | -import org.springframework.ai.chat.messages.Message; | |
| 6 | -import org.springframework.ai.chat.messages.SystemMessage; | |
| 7 | -import org.springframework.ai.chat.messages.UserMessage; | |
| 8 | -import org.springframework.ai.chat.model.ChatResponse; | |
| 9 | -import org.springframework.ai.chat.prompt.Prompt; | |
| 10 | 3 | import org.springframework.ai.ollama.OllamaChatModel; |
| 11 | -import org.springframework.ai.ollama.api.OllamaApi; | |
| 12 | -import org.springframework.ai.ollama.api.OllamaModel; | |
| 13 | -import org.springframework.ai.ollama.api.OllamaOptions; | |
| 14 | -import reactor.core.publisher.Flux; | |
| 15 | - | |
| 16 | -import java.util.ArrayList; | |
| 17 | -import java.util.List; | |
| 18 | 4 | |
| 19 | 5 | /** |
| 20 | 6 | * {@link OllamaChatModel} 集成测试 |
| ... | ... | @@ -23,41 +9,41 @@ import java.util.List; |
| 23 | 9 | */ |
| 24 | 10 | public class LlamaChatModelTests { |
| 25 | 11 | |
| 26 | - private final OllamaApi ollamaApi = new OllamaApi( | |
| 27 | - "http://127.0.0.1:11434"); | |
| 28 | - private final OllamaChatModel chatModel = new OllamaChatModel(ollamaApi, | |
| 29 | - OllamaOptions.create().withModel(OllamaModel.LLAMA3.getModelName())); | |
| 30 | - | |
| 31 | - @Test | |
| 32 | - @Disabled | |
| 33 | - public void testCall() { | |
| 34 | - // 准备参数 | |
| 35 | - List<Message> messages = new ArrayList<>(); | |
| 36 | - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | |
| 37 | - messages.add(new UserMessage("1 + 1 = ?")); | |
| 38 | - | |
| 39 | - // 调用 | |
| 40 | - ChatResponse response = chatModel.call(new Prompt(messages)); | |
| 41 | - // 打印结果 | |
| 42 | - System.out.println(response); | |
| 43 | - System.out.println(response.getResult().getOutput()); | |
| 44 | - } | |
| 45 | - | |
| 46 | - @Test | |
| 47 | - @Disabled | |
| 48 | - public void testStream() { | |
| 49 | - // 准备参数 | |
| 50 | - List<Message> messages = new ArrayList<>(); | |
| 51 | - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | |
| 52 | - messages.add(new UserMessage("1 + 1 = ?")); | |
| 53 | - | |
| 54 | - // 调用 | |
| 55 | - Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | |
| 56 | - // 打印结果 | |
| 57 | - flux.doOnNext(response -> { | |
| 58 | -// System.out.println(response); | |
| 59 | - System.out.println(response.getResult().getOutput()); | |
| 60 | - }).then().block(); | |
| 61 | - } | |
| 12 | +// private final OllamaApi ollamaApi = new OllamaApi( | |
| 13 | +// "http://127.0.0.1:11434"); | |
| 14 | +// private final OllamaChatModel chatModel = new OllamaChatModel(ollamaApi, | |
| 15 | +// OllamaOptions.create().withModel(OllamaModel.LLAMA3.getModelName())); | |
| 16 | +// | |
| 17 | +// @Test | |
| 18 | +// @Disabled | |
| 19 | +// public void testCall() { | |
| 20 | +// // 准备参数 | |
| 21 | +// List<Message> messages = new ArrayList<>(); | |
| 22 | +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | |
| 23 | +// messages.add(new UserMessage("1 + 1 = ?")); | |
| 24 | +// | |
| 25 | +// // 调用 | |
| 26 | +// ChatResponse response = chatModel.call(new Prompt(messages)); | |
| 27 | +// // 打印结果 | |
| 28 | +// System.out.println(response); | |
| 29 | +// System.out.println(response.getResult().getOutput()); | |
| 30 | +// } | |
| 31 | +// | |
| 32 | +// @Test | |
| 33 | +// @Disabled | |
| 34 | +// public void testStream() { | |
| 35 | +// // 准备参数 | |
| 36 | +// List<Message> messages = new ArrayList<>(); | |
| 37 | +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | |
| 38 | +// messages.add(new UserMessage("1 + 1 = ?")); | |
| 39 | +// | |
| 40 | +// // 调用 | |
| 41 | +// Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | |
| 42 | +// // 打印结果 | |
| 43 | +// flux.doOnNext(response -> { | |
| 44 | +//// System.out.println(response); | |
| 45 | +// System.out.println(response.getResult().getOutput()); | |
| 46 | +// }).then().block(); | |
| 47 | +// } | |
| 62 | 48 | |
| 63 | 49 | } | ... | ... |
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 { |
| 26 | 26 | "https://api.holdai.top", |
| 27 | 27 | "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf"); |
| 28 | 28 | private final OpenAiChatModel chatModel = new OpenAiChatModel(openAiApi, |
| 29 | - OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O).build()); | |
| 29 | + OpenAiChatOptions.builder().model(OpenAiApi.ChatModel.GPT_4_O).build()); | |
| 30 | 30 | |
| 31 | 31 | @Test |
| 32 | 32 | @Disabled | ... | ... |
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 { |
| 25 | 25 | "qS8k8dYr2nXunagK4SSU8Xjj", |
| 26 | 26 | "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"); |
| 27 | 27 | private final QianFanChatModel chatModel = new QianFanChatModel(qianFanApi, |
| 28 | - QianFanChatOptions.builder().withModel(QianFanApi.ChatModel.ERNIE_Tiny_8K.getValue()).build() | |
| 28 | + QianFanChatOptions.builder().model(QianFanApi.ChatModel.ERNIE_Tiny_8K.getValue()).build() | |
| 29 | 29 | ); |
| 30 | 30 | |
| 31 | 31 | @Test | ... | ... |
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 { |
| 24 | 24 | |
| 25 | 25 | private final ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"); |
| 26 | 26 | private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, |
| 27 | - ZhiPuAiChatOptions.builder().withModel(ZhiPuAiApi.ChatModel.GLM_4.getModelName()).build()); | |
| 27 | + ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.ChatModel.GLM_4.getName()).build()); | |
| 28 | 28 | |
| 29 | 29 | @Test |
| 30 | 30 | @Disabled | ... | ... |
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 { |
| 25 | 25 | // 准备参数 |
| 26 | 26 | // 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576 |
| 27 | 27 | QianFanImageOptions imageOptions = QianFanImageOptions.builder() |
| 28 | - .withModel(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) | |
| 29 | - .withWidth(1024).withHeight(1024) | |
| 30 | - .withN(1) | |
| 28 | + .model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) | |
| 29 | + .width(1024).height(1024) | |
| 30 | + .N(1) | |
| 31 | 31 | .build(); |
| 32 | 32 | ImagePrompt prompt = new ImagePrompt("good", imageOptions); |
| 33 | 33 | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java
| 1 | 1 | package cn.iocoder.yudao.framework.ai.image; |
| 2 | 2 | |
| 3 | -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; | |
| 3 | +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; | |
| 4 | +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; | |
| 5 | +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; | |
| 4 | 6 | import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; |
| 5 | -import com.alibaba.dashscope.utils.Constants; | |
| 6 | 7 | import org.junit.jupiter.api.Disabled; |
| 7 | 8 | import org.junit.jupiter.api.Test; |
| 8 | 9 | import org.springframework.ai.image.ImageOptions; |
| 9 | 10 | import org.springframework.ai.image.ImagePrompt; |
| 10 | 11 | import org.springframework.ai.image.ImageResponse; |
| 11 | -import org.springframework.ai.openai.OpenAiImageOptions; | |
| 12 | 12 | |
| 13 | 13 | /** |
| 14 | - * {@link com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel} 集成测试类 | |
| 14 | + * {@link DashScopeImageModel} 集成测试类 | |
| 15 | 15 | * |
| 16 | 16 | * @author fansili |
| 17 | 17 | */ |
| 18 | 18 | public class TongYiImagesModelTest { |
| 19 | 19 | |
| 20 | - private final ImageSynthesis imageApi = new ImageSynthesis(); | |
| 21 | - private final TongYiImagesModel imageModel = new TongYiImagesModel(imageApi); | |
| 22 | - | |
| 23 | - static { | |
| 24 | - Constants.apiKey = "sk-Zsd81gZYg7"; | |
| 25 | - } | |
| 20 | + private final DashScopeImageModel imageModel = new DashScopeImageModel( | |
| 21 | + new DashScopeImageApi("sk-7d903764249848cfa912733146da12d1")); | |
| 26 | 22 | |
| 27 | 23 | @Test |
| 28 | 24 | @Disabled |
| 29 | 25 | public void imageCallTest() { |
| 30 | 26 | // 准备参数 |
| 31 | - ImageOptions options = OpenAiImageOptions.builder() | |
| 27 | + ImageOptions options = DashScopeImageOptions.builder() | |
| 32 | 28 | .withModel(ImageSynthesis.Models.WANX_V1) |
| 33 | 29 | .withHeight(256).withWidth(256) |
| 34 | 30 | .build(); | ... | ... |
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 { |
| 22 | 22 | public void testCall() { |
| 23 | 23 | // 准备参数 |
| 24 | 24 | ZhiPuAiImageOptions imageOptions = ZhiPuAiImageOptions.builder() |
| 25 | - .withModel(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) | |
| 25 | + .model(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) | |
| 26 | 26 | .build(); |
| 27 | 27 | ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions); |
| 28 | 28 | ... | ... |