Commit 7ef73b7d0992486c380c49e7f214f48ebf0f29d4

Authored by YunaiV
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
1 1 /**
2   - * crm 模块的 web 拓展封装
  2 + * ai 模块的 web 拓展封装
3 3 */
4   -package cn.iocoder.yudao.module.crm.framework.web;
  4 +package cn.iocoder.yudao.module.ai.framework.web;
... ...
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  
... ...