From e8c4c582a5db536f19192ce52c9085956d1d7cfa Mon Sep 17 00:00:00 2001 From: PolarishT Date: Mon, 30 Dec 2024 13:08:20 +0800 Subject: [PATCH 1/9] docs: update function-calling README Signed-off-by: PolarishT --- community/function-calling/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community/function-calling/README.md b/community/function-calling/README.md index 878a333df..198586cb6 100644 --- a/community/function-calling/README.md +++ b/community/function-calling/README.md @@ -15,4 +15,4 @@ public class BaidusearchProperties {} ``` 4. 请在根目录 pom.xml 中添加 module 配置,如 `community/function-calling/spring-ai-alibaba-starter-functioncalling-baidusearch` -5. 请在插件 pom.xml 文件中只保留必须的传递依赖,插件版本应与 Spring AI Alibaba 统一,插件依赖规范为:序列化与反序列化依赖统一使用 `com.fasterxml.jackson.core:jackson`,日志依赖统一使用 `org.slf4j`,HTTP 客户端依赖统一使用 `org.springframework.boot::webClient`,其余依赖尽可能少引用或不引用 +5. 请在插件 pom.xml 文件中只保留必须的传递依赖,插件版本应与 Spring AI Alibaba 统一,插件依赖规范为:序列化与反序列化依赖统一使用 `com.fasterxml.jackson.core:jackson`,日志依赖统一使用 `org.slf4j.LoggerFactory:logger`,HTTP 客户端依赖统一使用 `org.springframework.boot:webClient`,其余依赖尽可能少引用或不引用 From a06b2e0009e46e784cd2e660b9ac0ffa85fdedc1 Mon Sep 17 00:00:00 2001 From: PolarishT Date: Sun, 5 Jan 2025 23:25:23 +0800 Subject: [PATCH 2/9] [feat]: add ImageModel Observation Signed-off-by: PolarishT --- .../dashscope/DashScopeAutoConfiguration.java | 437 ++++++++---------- .../dashscope/image/DashScopeImageModel.java | 294 ++++++------ .../image/DashScopeImageOptions.java | 1 - ...hScopeImageModelObservationConvention.java | 48 ++ .../dashscope/DashScopeAutoConfiguration.java | 420 +++++++---------- .../DashscopeAiTestConfiguration.java | 213 +++++---- 6 files changed, 652 insertions(+), 761 deletions(-) create mode 100644 spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java diff --git a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java index 1fe244e61..b35bed63c 100644 --- a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java +++ b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java @@ -22,6 +22,7 @@ import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.alibaba.cloud.ai.dashscope.image.observation.DashScopeImageModelObservationConvention; import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel; import com.alibaba.dashscope.audio.asr.transcription.Transcription; import com.alibaba.dashscope.audio.tts.SpeechSynthesizer; @@ -69,259 +70,191 @@ * @since 2024/8/16 11:45 */ @ConditionalOnClass(DashScopeApi.class) -@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, - SpringAiRetryAutoConfiguration.class }) -@EnableConfigurationProperties({ DashScopeConnectionProperties.class, DashScopeChatProperties.class, - DashScopeImageProperties.class, DashScopeSpeechSynthesisProperties.class, - DashScopeAudioTranscriptionProperties.class, DashScopeEmbeddingProperties.class, - DashScopeRerankProperties.class }) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class }) +@AutoConfiguration(after = {RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class}) +@EnableConfigurationProperties({DashScopeConnectionProperties.class, DashScopeChatProperties.class, DashScopeImageProperties.class, DashScopeSpeechSynthesisProperties.class, DashScopeAudioTranscriptionProperties.class, DashScopeEmbeddingProperties.class, DashScopeRerankProperties.class}) +@ImportAutoConfiguration(classes = {SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class}) public class DashScopeAutoConfiguration { - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public SpeechSynthesizer speechSynthesizer() { - return new SpeechSynthesizer(); - } - - @Bean - @ConditionalOnMissingBean - public Transcription transcription() { - return new Transcription(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeChatModel dashscopeChatModel(DashScopeConnectionProperties commonProperties, - DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, List toolFunctionCallbacks, - FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, - responseErrorHandler); - - var dashscopeModel = new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackResolver, - retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(dashscopeModel::setObservationConvention); - - return dashscopeModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeApi dashscopeChatApi(DashScopeConnectionProperties commonProperties, - DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); - - return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, - webClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeEmbeddingModel dashscopeEmbeddingModel(DashScopeConnectionProperties commonProperties, - DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var dashScopeApi = dashscopeEmbeddingApi(commonProperties, embeddingProperties, restClientBuilder, - webClientBuilder, responseErrorHandler); - - var embeddingModel = new DashScopeEmbeddingModel(dashScopeApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - public DashScopeApi dashscopeEmbeddingApi(DashScopeConnectionProperties commonProperties, - DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, - "embedding"); - - return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, - webClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi(DashScopeConnectionProperties commonProperties, - DashScopeSpeechSynthesisProperties speechSynthesisProperties) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, - "speechsynthesis"); - - return new DashScopeSpeechSynthesisApi(resolved.apiKey()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi(DashScopeConnectionProperties commonProperties, - DashScopeAudioTranscriptionProperties audioTranscriptionProperties) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, - audioTranscriptionProperties, "audiotranscription"); - - return new DashScopeAudioTranscriptionApi(resolved.apiKey()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeAgentApi dashscopeAgentApi(DashScopeConnectionProperties commonProperties, - DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); - - return new DashScopeAgentApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, - webClientBuilder, responseErrorHandler); - } - - @Bean - public RestClientCustomizer restClientCustomizer(DashScopeConnectionProperties commonProperties) { - return restClientBuilder -> restClientBuilder - .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS - .withReadTimeout(Duration.ofSeconds(commonProperties.getReadTimeout())))); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeImageModel dashScopeImageModel(DashScopeConnectionProperties commonProperties, - DashScopeImageProperties imageProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); - - var dashScopeImageApi = new DashScopeImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), - restClientBuilder, webClientBuilder, responseErrorHandler); - - return new DashScopeImageModel(dashScopeImageApi, imageProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeRerankProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeRerankModel dashscopeRerankModel(DashScopeConnectionProperties commonProperties, - DashScopeRerankProperties rerankProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, rerankProperties, - "rerank"); - - var dashscopeApi = new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), - restClientBuilder, webClientBuilder, responseErrorHandler); - - return new DashScopeRerankModel(dashscopeApi, rerankProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel(DashScopeConnectionProperties commonProperties, - DashScopeSpeechSynthesisProperties speechSynthesisProperties, RetryTemplate retryTemplate) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, - "speechsynthesis"); - - var dashScopeSpeechSynthesisApi = dashScopeSpeechSynthesisApi(commonProperties, speechSynthesisProperties); - - return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, speechSynthesisProperties.getOptions(), - retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel( - DashScopeConnectionProperties commonProperties, - DashScopeAudioTranscriptionProperties audioTranscriptionProperties, RetryTemplate retryTemplate) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, - audioTranscriptionProperties, "audiotranscription"); - - var dashScopeSpeechSynthesisApi = dashScopeAudioTranscriptionApi(commonProperties, - audioTranscriptionProperties); - - return new DashScopeAudioTranscriptionModel(dashScopeSpeechSynthesisApi, - audioTranscriptionProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - private record ResolvedConnectionProperties(String baseUrl, String apiKey, String workspaceId, - MultiValueMap headers) { - } - - private static @NotNull ResolvedConnectionProperties resolveConnectionProperties( - DashScopeParentProperties commonProperties, DashScopeParentProperties modelProperties, String modelType) { - - String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() - : commonProperties.getApiKey(); - String workspaceId = StringUtils.hasText(modelProperties.getWorkspaceId()) ? modelProperties.getWorkspaceId() - : commonProperties.getWorkspaceId(); - - Map> connectionHeaders = new HashMap<>(); - if (StringUtils.hasText(workspaceId)) { - connectionHeaders.put("DashScope-Workspace", List.of(workspaceId)); - } - - // get apikey from system env. - if (Objects.isNull(apiKey)) { - if (Objects.nonNull(System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY))) { - apiKey = System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY); - } - } - - Assert.hasText(baseUrl, - "DashScope base URL must be set. Use the connection property: spring.ai.dashscope.base-url or spring.ai.dashscope." - + modelType + ".base-url property."); - Assert.hasText(apiKey, - "DashScope API key must be set. Use the connection property: spring.ai.dashscope.api-key or spring.ai.dashscope." - + modelType + ".api-key property."); - - return new ResolvedConnectionProperties(baseUrl, apiKey, workspaceId, - CollectionUtils.toMultiValueMap(connectionHeaders)); - } + private static @NotNull ResolvedConnectionProperties resolveConnectionProperties (DashScopeParentProperties commonProperties, DashScopeParentProperties modelProperties, String modelType) { + + String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() : commonProperties.getBaseUrl(); + String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() : commonProperties.getApiKey(); + String workspaceId = StringUtils.hasText(modelProperties.getWorkspaceId()) ? modelProperties.getWorkspaceId() : commonProperties.getWorkspaceId(); + + Map> connectionHeaders = new HashMap<>(); + if (StringUtils.hasText(workspaceId)) { + connectionHeaders.put("DashScope-Workspace", List.of(workspaceId)); + } + + // get apikey from system env. + if (Objects.isNull(apiKey)) { + if (Objects.nonNull(System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY))) { + apiKey = System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY); + } + } + + Assert.hasText(baseUrl, "DashScope base URL must be set. Use the connection property: spring.ai.dashscope.base-url or spring.ai.dashscope." + modelType + ".base-url property."); + Assert.hasText(apiKey, "DashScope API key must be set. Use the connection property: spring.ai.dashscope.api-key or spring.ai.dashscope." + modelType + ".api-key property."); + + return new ResolvedConnectionProperties(baseUrl, apiKey, workspaceId, CollectionUtils.toMultiValueMap(connectionHeaders)); + } + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public SpeechSynthesizer speechSynthesizer () { + return new SpeechSynthesizer(); + } + + @Bean + @ConditionalOnMissingBean + public Transcription transcription () { + return new Transcription(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeChatModel dashscopeChatModel (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { + + if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { + chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); + } + + var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, responseErrorHandler); + + var dashscopeModel = new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackResolver, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(dashscopeModel::setObservationConvention); + + return dashscopeModel; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeApi dashscopeChatApi (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); + + return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeEmbeddingModel dashscopeEmbeddingModel (DashScopeConnectionProperties commonProperties, DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { + + var dashScopeApi = dashscopeEmbeddingApi(commonProperties, embeddingProperties, restClientBuilder, webClientBuilder, responseErrorHandler); + + var embeddingModel = new DashScopeEmbeddingModel(dashScopeApi, embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(embeddingModel::setObservationConvention); + + return embeddingModel; + } + + public DashScopeApi dashscopeEmbeddingApi (DashScopeConnectionProperties commonProperties, DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, "embedding"); + + return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi (DashScopeConnectionProperties commonProperties, DashScopeSpeechSynthesisProperties speechSynthesisProperties) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, "speechsynthesis"); + + return new DashScopeSpeechSynthesisApi(resolved.apiKey()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi (DashScopeConnectionProperties commonProperties, DashScopeAudioTranscriptionProperties audioTranscriptionProperties) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, audioTranscriptionProperties, "audiotranscription"); + + return new DashScopeAudioTranscriptionApi(resolved.apiKey()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeAgentApi dashscopeAgentApi (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); + + return new DashScopeAgentApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + } + + @Bean + public RestClientCustomizer restClientCustomizer (DashScopeConnectionProperties commonProperties) { + return restClientBuilder -> restClientBuilder.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS.withReadTimeout(Duration.ofSeconds(commonProperties.getReadTimeout())))); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeImageModel dashScopeImageModel (DashScopeConnectionProperties commonProperties, DashScopeImageProperties imageProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); + + var dashScopeImageApi = new DashScopeImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + + var dashScopeImageModel = new DashScopeImageModel(dashScopeImageApi, imageProperties.getOptions(), retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(dashScopeImageModel::setObservationConvention); + + return dashScopeImageModel; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeRerankProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeRerankModel dashscopeRerankModel (DashScopeConnectionProperties commonProperties, DashScopeRerankProperties rerankProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, rerankProperties, "rerank"); + + var dashscopeApi = new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + + return new DashScopeRerankModel(dashscopeApi, rerankProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel (DashScopeConnectionProperties commonProperties, DashScopeSpeechSynthesisProperties speechSynthesisProperties, RetryTemplate retryTemplate) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, "speechsynthesis"); + + var dashScopeSpeechSynthesisApi = dashScopeSpeechSynthesisApi(commonProperties, speechSynthesisProperties); + + return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, speechSynthesisProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel (DashScopeConnectionProperties commonProperties, DashScopeAudioTranscriptionProperties audioTranscriptionProperties, RetryTemplate retryTemplate) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, audioTranscriptionProperties, "audiotranscription"); + + var dashScopeSpeechSynthesisApi = dashScopeAudioTranscriptionApi(commonProperties, audioTranscriptionProperties); + + return new DashScopeAudioTranscriptionModel(dashScopeSpeechSynthesisApi, audioTranscriptionProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + public FunctionCallbackResolver springAiFunctionManager (ApplicationContext context) { + DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); + manager.setApplicationContext(context); + return manager; + } + + private record ResolvedConnectionProperties(String baseUrl, String apiKey, String workspaceId, + MultiValueMap headers) {} } diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java index 0f9ee26cd..d0dc9cd20 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java @@ -15,178 +15,158 @@ */ package com.alibaba.cloud.ai.dashscope.image; -import java.util.List; -import java.util.Objects; - import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants; +import com.alibaba.cloud.ai.dashscope.image.observation.DashScopeImageModelObservationConvention; +import io.micrometer.observation.ObservationRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import org.springframework.ai.image.Image; -import org.springframework.ai.image.ImageGeneration; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.image.ImageOptions; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; +import org.springframework.ai.image.*; +import org.springframework.ai.image.observation.ImageModelObservationContext; +import org.springframework.ai.image.observation.ImageModelObservationConvention; +import org.springframework.ai.image.observation.ImageModelObservationDocumentation; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.retry.RetryUtils; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; +import java.util.List; +import java.util.Objects; + /** * @author nuocheng.lxm * @author yuluo + * @author 北极星 * @since 2024/8/16 11:29 */ public class DashScopeImageModel implements ImageModel { - private static final Logger logger = LoggerFactory.getLogger(DashScopeImageModel.class); - - /** - * The default model used for the image completion requests. - */ - private static final String DEFAULT_MODEL = "wanx-v1"; - - /** - * Low-level access to the DashScope Image API. - */ - private final DashScopeImageApi dashScopeImageApi; - - /** - * The default options used for the image completion requests. - */ - private final DashScopeImageOptions defaultOptions; - - /** - * The retry template used to retry the OpenAI Image API calls. - */ - private final RetryTemplate retryTemplate; - - private static final int MAX_RETRY_COUNT = 10; - - public DashScopeImageModel(DashScopeImageApi dashScopeImageApi) { - this(dashScopeImageApi, DashScopeImageOptions.builder().build(), RetryUtils.DEFAULT_RETRY_TEMPLATE); - } - - public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options, - RetryTemplate retryTemplate) { - - Assert.notNull(dashScopeImageApi, "DashScopeImageApi must not be null"); - Assert.notNull(options, "options must not be null"); - Assert.notNull(retryTemplate, "retryTemplate must not be null"); - - this.dashScopeImageApi = dashScopeImageApi; - this.defaultOptions = options; - this.retryTemplate = retryTemplate; - } - - @Override - public ImageResponse call(ImagePrompt request) { - - String taskId = submitImageGenTask(request); - if (taskId == null) { - return new ImageResponse(List.of()); - } - - int retryCount = 0; - while (retryCount < MAX_RETRY_COUNT) { - DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); - if (getResultResponse != null) { - DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse - .output(); - String taskStatus = output.taskStatus(); - switch (taskStatus) { - case "SUCCEEDED" -> { - return toImageResponse(output); - } - case "FAILED", "UNKNOWN" -> { - return new ImageResponse(List.of()); - } - } - } - try { - Thread.sleep(15000L); - retryCount++; - } - catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - return new ImageResponse(List.of()); - } - - public String submitImageGenTask(ImagePrompt request) { - - DashScopeImageOptions imageOptions = toImageOptions(request.getOptions()); - logger.debug("Image options: {}", imageOptions); - - DashScopeImageApi.DashScopeImageRequest dashScopeImageRequest = constructImageRequest(request, imageOptions); - - ResponseEntity submitResponse = dashScopeImageApi - .submitImageGenTask(dashScopeImageRequest); - - if (submitResponse == null || submitResponse.getBody() == null) { - logger.warn("Submit imageGen error,request: {}", request); - return null; - } - - return submitResponse.getBody().output().taskId(); - } - - /** - * Merge Image options. Notice: Programmatically set options parameters take - * precedence - */ - private DashScopeImageOptions toImageOptions(ImageOptions runtimeOptions) { - - // set default image model - var currentOptions = DashScopeImageOptions.builder().withModel(DEFAULT_MODEL).build(); - - if (Objects.nonNull(runtimeOptions)) { - currentOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, - DashScopeImageOptions.class); - } - - currentOptions = ModelOptionsUtils.merge(currentOptions, this.defaultOptions, DashScopeImageOptions.class); - - return currentOptions; - } - - public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask(String taskId) { - ResponseEntity getImageGenResponse = dashScopeImageApi - .getImageGenTaskResult(taskId); - if (getImageGenResponse == null || getImageGenResponse.getBody() == null) { - logger.warn("No image response returned for taskId: {}", taskId); - return null; - } - return getImageGenResponse.getBody(); - } - - private ImageResponse toImageResponse( - DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output) { - List genImageList = output - .results(); - if (genImageList == null || genImageList.isEmpty()) { - return new ImageResponse(List.of()); - } - List imageGenerationList = genImageList.stream() - .map(entry -> new ImageGeneration(new Image(entry.url(), null))) - .toList(); - - return new ImageResponse(imageGenerationList); - } - - private DashScopeImageApi.DashScopeImageRequest constructImageRequest(ImagePrompt imagePrompt, - DashScopeImageOptions options) { - - return new DashScopeImageApi.DashScopeImageRequest(options.getModel(), - new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestInput( - imagePrompt.getInstructions().get(0).getText(), options.getNegativePrompt(), - options.getRefImg()), - new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestParameter(options.getStyle(), - options.getSize(), options.getN(), options.getSeed(), options.getRefStrength(), - options.getRefMode())); - } + private static final Logger logger = LoggerFactory.getLogger(DashScopeImageModel.class); + + private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DashScopeImageModelObservationConvention(); + + /** + * The default model used for the image completion requests. + */ + private static final String DEFAULT_MODEL = DashScopeImageApi.DEFAULT_IMAGE_MODEL; + + /** + * Low-level access to the DashScope Image API. + */ + private final DashScopeImageApi dashScopeImageApi; + + /** + * Observation registry used for instrumentation. + */ + private final ObservationRegistry observationRegistry; + + /** + * The default options used for the image completion requests. + */ + private final DashScopeImageOptions defaultOptions; + + /** + * The retry template used to retry the OpenAI Image API calls. + */ + private final RetryTemplate retryTemplate; + + /** + * Conventions to use for generating observations. + */ + private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; + + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; + } + + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry, DashScopeImageOptions options, RetryTemplate retryTemplate) { + this.observationRegistry = observationRegistry; + this.defaultOptions = options; + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = retryTemplate; + } + + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options, RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { + + Assert.notNull(dashScopeImageApi, "DashScopeImageApi must not be null"); + Assert.notNull(options, "options must not be null"); + Assert.notNull(retryTemplate, "retryTemplate must not be null"); + + this.dashScopeImageApi = dashScopeImageApi; + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + this.observationRegistry = observationRegistry; + } + + @Override + public ImageResponse call (ImagePrompt prompt) { + + ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(prompt).provider(DashScopeApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions).build(); + + return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).observe(() -> { + + DashScopeImageApi.DashScopeImageRequest request = constructImageRequest(prompt, toImageOptions(prompt.getOptions())); + + ResponseEntity completionEntity = this.retryTemplate.execute(ctx -> this.dashScopeImageApi.submitImageGenTask(request), ctx -> new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)); + + var imageCompletion = completionEntity.getBody(); + + if (imageCompletion == null || imageCompletion.output() == null) { + logger.warn("No Image completion returned for prompt: {}", prompt); + return new ImageResponse(List.of()); + } + + ImageResponse response = toImageResponse(imageCompletion.output()); + observationContext.setResponse(response); + return response; + }); + } + + /** + * Merge Image options. Notice: Programmatically set options parameters take + * precedence + */ + private DashScopeImageOptions toImageOptions (ImageOptions runtimeOptions) { + + // set default image model + var currentOptions = DashScopeImageOptions.builder().withModel(DEFAULT_MODEL).build(); + + if (Objects.nonNull(runtimeOptions)) { + currentOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, DashScopeImageOptions.class); + } + + currentOptions = ModelOptionsUtils.merge(currentOptions, this.defaultOptions, DashScopeImageOptions.class); + return currentOptions; + } + + private ImageResponse toImageResponse (DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output) { + List genImageList = output.results(); + if (genImageList == null || genImageList.isEmpty()) { + return new ImageResponse(List.of()); + } + List imageGenerationList = genImageList.stream().map(entry -> new ImageGeneration(new Image(entry.url(), null))).toList(); + + return new ImageResponse(imageGenerationList); + } + + private DashScopeImageApi.DashScopeImageRequest constructImageRequest (ImagePrompt imagePrompt, DashScopeImageOptions options) { + + return new DashScopeImageApi.DashScopeImageRequest(options.getModel(), new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestInput(imagePrompt.getInstructions().get(0).getText(), options.getNegativePrompt(), options.getRefImg()), new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestParameter(options.getStyle(), options.getSize(), options.getN(), options.getSeed(), options.getRefStrength(), options.getRefMode())); + } + + /** + * Use the provided convention for reporting observation data + * + * @param observationConvention The provided convention + */ + public void setObservationConvention (ImageModelObservationConvention observationConvention) { + Assert.notNull(observationConvention, "observationConvention cannot be null"); + this.observationConvention = observationConvention; + } } diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageOptions.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageOptions.java index c15031e79..256e20b58 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageOptions.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageOptions.java @@ -274,5 +274,4 @@ public DashScopeImageOptions build() { } } - } diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java new file mode 100644 index 000000000..74bdb2981 --- /dev/null +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.cloud.ai.dashscope.image.observation; + +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; +import com.alibaba.fastjson.JSON; +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import org.springframework.ai.chat.observation.ChatModelObservationContext; +import org.springframework.ai.chat.observation.ChatModelObservationDocumentation; +import org.springframework.ai.image.observation.DefaultImageModelObservationConvention; +import org.springframework.ai.image.observation.ImageModelObservationContext; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Objects; + +/** + * Dashscope conventions to populate observations for Image model operations. + * + * @author Lumian + * @since 1.0.0 + */ +public class DashScopeImageModelObservationConvention extends DefaultImageModelObservationConvention { + + public static final String DEFAULT_NAME = "gen_ai.client.operation"; + + private static final String ILLEGAL_STOP_CONTENT = ""; + + @Override + public String getName () { + return DEFAULT_NAME; + } +} diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeAutoConfiguration.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeAutoConfiguration.java index c8f9f004c..097ccf59d 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeAutoConfiguration.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeAutoConfiguration.java @@ -22,14 +22,17 @@ import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.alibaba.cloud.ai.dashscope.image.observation.DashScopeImageModelObservationConvention; import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel; import com.alibaba.dashscope.audio.asr.transcription.Transcription; import com.alibaba.dashscope.audio.tts.SpeechSynthesizer; +import io.micrometer.observation.ObservationRegistry; import org.jetbrains.annotations.NotNull; import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; import org.springframework.ai.model.function.FunctionCallback; import org.springframework.ai.model.function.FunctionCallbackResolver; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -65,248 +68,183 @@ * @since 2024/8/16 11:45 */ @ConditionalOnClass(DashScopeApi.class) -@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, - SpringAiRetryAutoConfiguration.class }) -@EnableConfigurationProperties({ DashScopeConnectionProperties.class, DashScopeChatProperties.class, - DashScopeImageProperties.class, DashScopeSpeechSynthesisProperties.class, - DashScopeAudioTranscriptionProperties.class, DashScopeEmbeddingProperties.class, - DashScopeRerankProperties.class }) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class }) +@AutoConfiguration(after = {RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class}) +@EnableConfigurationProperties({DashScopeConnectionProperties.class, DashScopeChatProperties.class, DashScopeImageProperties.class, DashScopeSpeechSynthesisProperties.class, DashScopeAudioTranscriptionProperties.class, DashScopeEmbeddingProperties.class, DashScopeRerankProperties.class}) +@ImportAutoConfiguration(classes = {SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class}) public class DashScopeAutoConfiguration { - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public SpeechSynthesizer speechSynthesizer() { - return new SpeechSynthesizer(); - } - - @Bean - @ConditionalOnMissingBean - public Transcription transcription() { - return new Transcription(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeChatModel dashscopeChatModel(DashScopeConnectionProperties commonProperties, - DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, List toolFunctionCallbacks, - FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { - - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, - responseErrorHandler); - - return new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackResolver, - retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeApi dashscopeChatApi(DashScopeConnectionProperties commonProperties, - DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); - - return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, - webClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeEmbeddingModel dashscopeEmbeddingModel(DashScopeConnectionProperties commonProperties, - DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { - - var dashScopeApi = dashscopeEmbeddingApi(commonProperties, embeddingProperties, restClientBuilder, - webClientBuilder, responseErrorHandler); - - return new DashScopeEmbeddingModel(dashScopeApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate); - } - - public DashScopeApi dashscopeEmbeddingApi(DashScopeConnectionProperties commonProperties, - DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, - "embedding"); - - return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, - webClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi(DashScopeConnectionProperties commonProperties, - DashScopeSpeechSynthesisProperties speechSynthesisProperties) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, - "speechsynthesis"); - - return new DashScopeSpeechSynthesisApi(resolved.apiKey()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi(DashScopeConnectionProperties commonProperties, - DashScopeAudioTranscriptionProperties audioTranscriptionProperties) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, - audioTranscriptionProperties, "audiotranscription"); - - return new DashScopeAudioTranscriptionApi(resolved.apiKey()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeAgentApi dashscopeAgentApi(DashScopeConnectionProperties commonProperties, - DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); - - return new DashScopeAgentApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, - webClientBuilder, responseErrorHandler); - } - - @Bean - public RestClientCustomizer restClientCustomizer(DashScopeConnectionProperties commonProperties) { - return restClientBuilder -> restClientBuilder - .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS - .withReadTimeout(Duration.ofSeconds(commonProperties.getReadTimeout())))); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeImageModel dashScopeImageModel(DashScopeConnectionProperties commonProperties, - DashScopeImageProperties imageProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); - - var dashScopeImageApi = new DashScopeImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), - restClientBuilder, webClientBuilder, responseErrorHandler); - - return new DashScopeImageModel(dashScopeImageApi, imageProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeRerankProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public DashScopeRerankModel dashscopeRerankModel(DashScopeConnectionProperties commonProperties, - DashScopeRerankProperties rerankProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler) { - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, rerankProperties, - "rerank"); - - var dashscopeApi = new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), - restClientBuilder, webClientBuilder, responseErrorHandler); - - return new DashScopeRerankModel(dashscopeApi, rerankProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel(DashScopeConnectionProperties commonProperties, - DashScopeSpeechSynthesisProperties speechSynthesisProperties, RetryTemplate retryTemplate) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, - "speechsynthesis"); - - var dashScopeSpeechSynthesisApi = dashScopeSpeechSynthesisApi(commonProperties, speechSynthesisProperties); - - return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, speechSynthesisProperties.getOptions(), - retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel( - DashScopeConnectionProperties commonProperties, - DashScopeAudioTranscriptionProperties audioTranscriptionProperties, RetryTemplate retryTemplate) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, - audioTranscriptionProperties, "audiotranscription"); - - var dashScopeSpeechSynthesisApi = dashScopeAudioTranscriptionApi(commonProperties, - audioTranscriptionProperties); - - return new DashScopeAudioTranscriptionModel(dashScopeSpeechSynthesisApi, - audioTranscriptionProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - private record ResolvedConnectionProperties(String baseUrl, String apiKey, String workspaceId, - MultiValueMap headers) { - } - - private static @NotNull ResolvedConnectionProperties resolveConnectionProperties( - DashScopeParentProperties commonProperties, DashScopeParentProperties modelProperties, String modelType) { - - String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() - : commonProperties.getApiKey(); - String workspaceId = StringUtils.hasText(modelProperties.getWorkspaceId()) ? modelProperties.getWorkspaceId() - : commonProperties.getWorkspaceId(); - - Map> connectionHeaders = new HashMap<>(); - if (StringUtils.hasText(workspaceId)) { - connectionHeaders.put("DashScope-Workspace", List.of(workspaceId)); - } - - // get apikey from system env. - if (Objects.isNull(apiKey)) { - if (Objects.nonNull(System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY))) { - apiKey = System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY); - } - } - - Assert.hasText(baseUrl, - "DashScope base URL must be set. Use the connection property: spring.ai.dashscope.base-url or spring.ai.dashscope." - + modelType + ".base-url property."); - Assert.hasText(apiKey, - "DashScope API key must be set. Use the connection property: spring.ai.dashscope.api-key or spring.ai.dashscope." - + modelType + ".api-key property."); - - return new ResolvedConnectionProperties(baseUrl, apiKey, workspaceId, - CollectionUtils.toMultiValueMap(connectionHeaders)); - } + private static @NotNull ResolvedConnectionProperties resolveConnectionProperties (DashScopeParentProperties commonProperties, DashScopeParentProperties modelProperties, String modelType) { + + String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() : commonProperties.getBaseUrl(); + String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() : commonProperties.getApiKey(); + String workspaceId = StringUtils.hasText(modelProperties.getWorkspaceId()) ? modelProperties.getWorkspaceId() : commonProperties.getWorkspaceId(); + + Map> connectionHeaders = new HashMap<>(); + if (StringUtils.hasText(workspaceId)) { + connectionHeaders.put("DashScope-Workspace", List.of(workspaceId)); + } + + // get apikey from system env. + if (Objects.isNull(apiKey)) { + if (Objects.nonNull(System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY))) { + apiKey = System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY); + } + } + + Assert.hasText(baseUrl, "DashScope base URL must be set. Use the connection property: spring.ai.dashscope.base-url or spring.ai.dashscope." + modelType + ".base-url property."); + Assert.hasText(apiKey, "DashScope API key must be set. Use the connection property: spring.ai.dashscope.api-key or spring.ai.dashscope." + modelType + ".api-key property."); + + return new ResolvedConnectionProperties(baseUrl, apiKey, workspaceId, CollectionUtils.toMultiValueMap(connectionHeaders)); + } + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public SpeechSynthesizer speechSynthesizer () { + return new SpeechSynthesizer(); + } + + @Bean + @ConditionalOnMissingBean + public Transcription transcription () { + return new Transcription(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeChatModel dashscopeChatModel (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { + + if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { + chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); + } + + var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, responseErrorHandler); + + return new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackResolver, retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeApi dashscopeChatApi (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); + + return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeEmbeddingModel dashscopeEmbeddingModel (DashScopeConnectionProperties commonProperties, DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { + + var dashScopeApi = dashscopeEmbeddingApi(commonProperties, embeddingProperties, restClientBuilder, webClientBuilder, responseErrorHandler); + + return new DashScopeEmbeddingModel(dashScopeApi, embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), retryTemplate); + } + + public DashScopeApi dashscopeEmbeddingApi (DashScopeConnectionProperties commonProperties, DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, "embedding"); + + return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi (DashScopeConnectionProperties commonProperties, DashScopeSpeechSynthesisProperties speechSynthesisProperties) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, "speechsynthesis"); + + return new DashScopeSpeechSynthesisApi(resolved.apiKey()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi (DashScopeConnectionProperties commonProperties, DashScopeAudioTranscriptionProperties audioTranscriptionProperties) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, audioTranscriptionProperties, "audiotranscription"); + + return new DashScopeAudioTranscriptionApi(resolved.apiKey()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeAgentApi dashscopeAgentApi (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); + + return new DashScopeAgentApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + } + + @Bean + public RestClientCustomizer restClientCustomizer (DashScopeConnectionProperties commonProperties) { + return restClientBuilder -> restClientBuilder.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS.withReadTimeout(Duration.ofSeconds(commonProperties.getReadTimeout())))); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeImageModel dashScopeImageModel (DashScopeConnectionProperties commonProperties, DashScopeImageProperties imageProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); + + var dashScopeImageApi = new DashScopeImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + + var dashScopeImageModel = new DashScopeImageModel(dashScopeImageApi, imageProperties.getOptions(), retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(dashScopeImageModel::setObservationConvention); + + return dashScopeImageModel; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeRerankProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeRerankModel dashscopeRerankModel (DashScopeConnectionProperties commonProperties, DashScopeRerankProperties rerankProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, rerankProperties, "rerank"); + + var dashscopeApi = new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); + + return new DashScopeRerankModel(dashscopeApi, rerankProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel (DashScopeConnectionProperties commonProperties, DashScopeSpeechSynthesisProperties speechSynthesisProperties, RetryTemplate retryTemplate) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, "speechsynthesis"); + + var dashScopeSpeechSynthesisApi = dashScopeSpeechSynthesisApi(commonProperties, speechSynthesisProperties); + + return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, speechSynthesisProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) + public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel (DashScopeConnectionProperties commonProperties, DashScopeAudioTranscriptionProperties audioTranscriptionProperties, RetryTemplate retryTemplate) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, audioTranscriptionProperties, "audiotranscription"); + + var dashScopeSpeechSynthesisApi = dashScopeAudioTranscriptionApi(commonProperties, audioTranscriptionProperties); + + return new DashScopeAudioTranscriptionModel(dashScopeSpeechSynthesisApi, audioTranscriptionProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + public FunctionCallbackResolver springAiFunctionManager (ApplicationContext context) { + DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); + manager.setApplicationContext(context); + return manager; + } + + private record ResolvedConnectionProperties(String baseUrl, String apiKey, String workspaceId, + MultiValueMap headers) {} } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/DashscopeAiTestConfiguration.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/DashscopeAiTestConfiguration.java index 86feb992c..507a10195 100755 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/DashscopeAiTestConfiguration.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/DashscopeAiTestConfiguration.java @@ -16,12 +16,12 @@ package com.alibaba.cloud.ai.dashscope; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.api.DashScopeAudioTranscriptionApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeSpeechSynthesisApi; -import com.alibaba.cloud.ai.dashscope.api.DashScopeAudioTranscriptionApi; +import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionModel; import com.alibaba.cloud.ai.dashscope.audio.DashScopeSpeechSynthesisModel; import com.alibaba.cloud.ai.dashscope.audio.DashScopeSpeechSynthesisOptions; -import com.alibaba.cloud.ai.dashscope.audio.DashScopeAudioTranscriptionModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; @@ -30,7 +30,6 @@ import com.alibaba.cloud.ai.model.RerankModel; import com.alibaba.dashscope.audio.asr.transcription.Transcription; import com.alibaba.dashscope.audio.tts.SpeechSynthesizer; - import io.micrometer.observation.tck.TestObservationRegistry; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; @@ -46,112 +45,106 @@ @SpringBootConfiguration public class DashscopeAiTestConfiguration { - @Bean - public DashScopeImageApi dashscopeImageApi() { - return newDashScopeImageApi(getApiKey()); - } - - @Bean - public DashScopeApi dashscopeApi() { - return newDashScopeApi(getApiKey()); - } - - @Bean - public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi() { - return newDashScopeSpeechSynthesisApi(getApiKey()); - } - - @Bean - public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi() { - return newDashScopeAudioTranscriptionApi(getApiKey()); - } - - @Bean - public DashScopeApi dashscopeChatApi() { - return newDashScopeChatApi(getApiKey()); - } - - private DashScopeApi newDashScopeChatApi(String apiKey) { - return new DashScopeApi(DEFAULT_BASE_URL, apiKey, ""); - } - - private DashScopeApi newDashScopeApi(String apiKey) { - return new DashScopeApi(apiKey); - } - - private DashScopeSpeechSynthesisApi newDashScopeSpeechSynthesisApi(String apiKey) { - return new DashScopeSpeechSynthesisApi(apiKey); - } - - private DashScopeAudioTranscriptionApi newDashScopeAudioTranscriptionApi(String apiKey) { - return new DashScopeAudioTranscriptionApi(apiKey); - } - - private DashScopeImageApi newDashScopeImageApi(String apiKey) { - return new DashScopeImageApi(apiKey); - } - - private String getApiKey() { - String apiKey = System.getenv("DASHSCOPE_API_KEY"); - if (!StringUtils.hasText(apiKey)) { - throw new IllegalArgumentException( - "You must provide an API key. Put it in an environment variable under the name DASHSCOPE_API_KEY"); - } - return apiKey; - } - - @Bean - public ChatModel dashscopeChatModel(DashScopeApi dashscopeChatApi, TestObservationRegistry observationRegistry) { - return new DashScopeChatModel(dashscopeChatApi, - DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL).build(), null, - RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); - } - - @Bean - public EmbeddingModel dashscopeEmbeddingModel(DashScopeApi dashscopeApi) { - return new DashScopeEmbeddingModel(dashscopeApi); - } - - @Bean - public DashScopeImageModel dashscopeImageModel(DashScopeImageApi dashscopeImageApi) { - return new DashScopeImageModel(dashscopeImageApi); - } - - @Bean - public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel( - DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi) { - return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, - DashScopeSpeechSynthesisOptions.builder().withModel("cosyvoice-v1").withVoice("longhua").build()); - } - - @Bean - public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel( - DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi) { - return new DashScopeAudioTranscriptionModel(dashScopeAudioTranscriptionApi); - } - - @Bean - public TestObservationRegistry observationRegistry() { - return TestObservationRegistry.create(); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public SpeechSynthesizer speechSynthesizer() { - return new SpeechSynthesizer(); - } - - @Bean - @ConditionalOnMissingBean - public Transcription transcription() { - return new Transcription(); - } - - @Bean - @ConditionalOnMissingBean - public RerankModel dashscopeRerankModel(DashScopeApi dashscopeApi) { - return new DashScopeRerankModel(dashscopeApi); - } + @Bean + public DashScopeImageApi dashscopeImageApi () { + return newDashScopeImageApi(getApiKey()); + } + + @Bean + public DashScopeApi dashscopeApi () { + return newDashScopeApi(getApiKey()); + } + + @Bean + public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi () { + return newDashScopeSpeechSynthesisApi(getApiKey()); + } + + @Bean + public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi () { + return newDashScopeAudioTranscriptionApi(getApiKey()); + } + + @Bean + public DashScopeApi dashscopeChatApi () { + return newDashScopeChatApi(getApiKey()); + } + + private DashScopeApi newDashScopeChatApi (String apiKey) { + return new DashScopeApi(DEFAULT_BASE_URL, apiKey, ""); + } + + private DashScopeApi newDashScopeApi (String apiKey) { + return new DashScopeApi(apiKey); + } + + private DashScopeSpeechSynthesisApi newDashScopeSpeechSynthesisApi (String apiKey) { + return new DashScopeSpeechSynthesisApi(apiKey); + } + + private DashScopeAudioTranscriptionApi newDashScopeAudioTranscriptionApi (String apiKey) { + return new DashScopeAudioTranscriptionApi(apiKey); + } + + private DashScopeImageApi newDashScopeImageApi (String apiKey) { + return new DashScopeImageApi(apiKey); + } + + private String getApiKey () { + String apiKey = System.getenv("DASHSCOPE_API_KEY"); + if (!StringUtils.hasText(apiKey)) { + throw new IllegalArgumentException("You must provide an API key. Put it in an environment variable under the name DASHSCOPE_API_KEY"); + } + return apiKey; + } + + @Bean + public ChatModel dashscopeChatModel (DashScopeApi dashscopeChatApi, TestObservationRegistry observationRegistry) { + return new DashScopeChatModel(dashscopeChatApi, DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL).build(), null, RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); + } + + @Bean + public EmbeddingModel dashscopeEmbeddingModel (DashScopeApi dashscopeApi) { + return new DashScopeEmbeddingModel(dashscopeApi); + } + + @Bean + public DashScopeImageModel dashscopeImageModel (DashScopeImageApi dashscopeImageApi, TestObservationRegistry observationRegistry) { + return new DashScopeImageModel(dashscopeImageApi, observationRegistry); + } + + @Bean + public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel (DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi) { + return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, DashScopeSpeechSynthesisOptions.builder().withModel("cosyvoice-v1").withVoice("longhua").build()); + } + + @Bean + public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel (DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi) { + return new DashScopeAudioTranscriptionModel(dashScopeAudioTranscriptionApi); + } + + @Bean + public TestObservationRegistry observationRegistry () { + return TestObservationRegistry.create(); + } + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public SpeechSynthesizer speechSynthesizer () { + return new SpeechSynthesizer(); + } + + @Bean + @ConditionalOnMissingBean + public Transcription transcription () { + return new Transcription(); + } + + @Bean + @ConditionalOnMissingBean + public RerankModel dashscopeRerankModel (DashScopeApi dashscopeApi) { + return new DashScopeRerankModel(dashscopeApi); + } } From 9725819e5ef3678aa5d67472d128f3591e061683 Mon Sep 17 00:00:00 2001 From: PolarishT Date: Sun, 12 Jan 2025 19:02:06 +0800 Subject: [PATCH 3/9] [WIP FC]: load config Signed-off-by: PolarishT --- .../dashscope/DashScopeAutoConfiguration.java | 20 +- .../ai/dashscope/api/DashScopeImageApi.java | 221 ++++++++---------- .../dashscope/image/DashScopeImageModel.java | 116 +++++++-- .../image/DashScopeImageModelIT.java | 88 +++++-- 4 files changed, 282 insertions(+), 163 deletions(-) diff --git a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java index b35bed63c..b54c6a592 100644 --- a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java +++ b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java @@ -115,7 +115,16 @@ public Transcription transcription () { @Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeChatModel dashscopeChatModel (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { + public DashScopeChatModel dashscopeChatModel (DashScopeConnectionProperties commonProperties, + DashScopeChatProperties chatProperties, + RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, + List toolFunctionCallbacks, + FunctionCallbackResolver functionCallbackResolver, + RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler, + ObjectProvider observationRegistry, + ObjectProvider observationConvention) { if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); @@ -198,7 +207,14 @@ public RestClientCustomizer restClientCustomizer (DashScopeConnectionProperties @Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeImageModel dashScopeImageModel (DashScopeConnectionProperties commonProperties, DashScopeImageProperties imageProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { + public DashScopeImageModel dashScopeImageModel (DashScopeConnectionProperties commonProperties, + DashScopeImageProperties imageProperties, + RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, + RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler, + ObjectProvider observationRegistry, + ObjectProvider observationConvention) { ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java index d5f42f5cd..e494e93b6 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java @@ -15,17 +15,16 @@ */ package com.alibaba.cloud.ai.dashscope.api; -import java.util.List; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import org.springframework.ai.retry.RetryUtils; import org.springframework.http.ResponseEntity; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; +import java.util.List; + import static com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants.DEFAULT_BASE_URL; /** @@ -34,121 +33,105 @@ */ public class DashScopeImageApi { - public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); - - private final RestClient restClient; - - public DashScopeImageApi(String apiKey) { - this(DEFAULT_BASE_URL, apiKey, RestClient.builder(), WebClient.builder(), - RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); - } - - public DashScopeImageApi(String apiKey, String workSpaceId) { - this(DEFAULT_BASE_URL, apiKey, workSpaceId, RestClient.builder(), WebClient.builder(), - RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); - } - - public DashScopeImageApi(String baseUrl, String apiKey, String workSpaceId) { - this(baseUrl, apiKey, workSpaceId, RestClient.builder(), WebClient.builder(), - RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); - } - - public DashScopeImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - this.restClient = restClientBuilder.baseUrl(baseUrl) - .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)) - .defaultStatusHandler(responseErrorHandler) - .build(); - } - - public DashScopeImageApi(String baseUrl, String apiKey, String workSpaceId, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - this.restClient = restClientBuilder.baseUrl(baseUrl) - .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey, workSpaceId)) - .defaultStatusHandler(responseErrorHandler) - .build(); - } - - public ResponseEntity submitImageGenTask(DashScopeImageRequest request) { - return this.restClient.post() - .uri("/api/v1/services/aigc/text2image/image-synthesis") - // issue: https://github.com/alibaba/spring-ai-alibaba/issues/29 - .header("X-DashScope-Async", "enable") - .body(request) - .retrieve() - .toEntity(DashScopeImageAsyncReponse.class); - } - - public ResponseEntity getImageGenTaskResult(String taskId) { - return this.restClient.get() - .uri("/api/v1/tasks/{task_id}", taskId) - .retrieve() - .toEntity(DashScopeImageAsyncReponse.class); - } - - /******************************************* - * Embedding相关 - **********************************************/ - - public enum ImageModel { - - WANX_V1("wanx-v1"); - - public final String value; - - ImageModel(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageRequest(@JsonProperty("model") String model, - @JsonProperty("input") DashScopeImageRequestInput input, - @JsonProperty("parameters") DashScopeImageRequestParameter parameters - - ) { - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageRequestInput(@JsonProperty("prompt") String prompt, - @JsonProperty("negative_prompt") String negativePrompt, @JsonProperty("ref_img") String refImg) { - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageRequestParameter(@JsonProperty("style") String style, - @JsonProperty("size") String size, @JsonProperty("n") Integer n, @JsonProperty("seed") Integer seed, - @JsonProperty("ref_strength") Float refStrength, @JsonProperty("ref_mode") String refMode) { - } - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponse(@JsonProperty("request_id") String requestId, - @JsonProperty("output") DashScopeImageAsyncReponseOutput output, - @JsonProperty("usage") DashScopeImageAsyncReponseUsage usage) { - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponseOutput(@JsonProperty("task_id") String taskId, - @JsonProperty("task_status") String taskStatus, - @JsonProperty("results") List results, - @JsonProperty("task_metrics") DashScopeImageAsyncReponseTaskMetrics taskMetrics, - @JsonProperty("code") String code, @JsonProperty("message") String message) { - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponseTaskMetrics(@JsonProperty("TOTAL") Integer total, - @JsonProperty("SUCCEEDED") Integer SUCCEEDED, @JsonProperty("FAILED") Integer FAILED) { - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponseUsage(@JsonProperty("image_count") Integer imageCount) { - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponseResult(@JsonProperty("url") String url) { - } - } + // public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); + public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); + + private final RestClient restClient; + + public DashScopeImageApi (String apiKey) { + this(DEFAULT_BASE_URL, apiKey, RestClient.builder(), WebClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); + } + + public DashScopeImageApi (String apiKey, String workSpaceId) { + this(DEFAULT_BASE_URL, apiKey, workSpaceId, RestClient.builder(), WebClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); + } + + public DashScopeImageApi (String baseUrl, String apiKey, String workSpaceId) { + this(baseUrl, apiKey, workSpaceId, RestClient.builder(), WebClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); + } + + public DashScopeImageApi (String baseUrl, String apiKey, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)).defaultStatusHandler(responseErrorHandler).build(); + } + + public DashScopeImageApi (String baseUrl, String apiKey, String workSpaceId, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey, workSpaceId)).defaultStatusHandler(responseErrorHandler).build(); + } + + public ResponseEntity submitImageGenTask (DashScopeImageRequest request) { + return this.restClient.post().uri("/api/v1/services/aigc/text2image/image-synthesis") + // issue: https://github.com/alibaba/spring-ai-alibaba/issues/29 + .header("X-DashScope-Async", "enable").body(request).retrieve().toEntity(DashScopeImageAsyncReponse.class); + } + + public ResponseEntity getImageGenTaskResult (String taskId) { + return this.restClient.get().uri("/api/v1/tasks/{task_id}", taskId).retrieve().toEntity(DashScopeImageAsyncReponse.class); + } + + /******************************************* + * Embedding相关 + **********************************************/ + + public enum ImageModel { + + WANX_V1("wanx-v1"), + + WANX_V2_T2I_TURBO("wanx2.1-t2i-turbo"); + + public final String value; + + ImageModel (String value) { + this.value = value; + } + + public String getValue () { + return value; + } + + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageRequest(@JsonProperty("model") String model, + @JsonProperty("input") DashScopeImageRequestInput input, + @JsonProperty("parameters") DashScopeImageRequestParameter parameters + + ) { + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageRequestInput(@JsonProperty("prompt") String prompt, + @JsonProperty("negative_prompt") String negativePrompt, + @JsonProperty("ref_img") String refImg) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageRequestParameter(@JsonProperty("style") String style, + @JsonProperty("size") String size, @JsonProperty("n") Integer n, + @JsonProperty("seed") Integer seed, + @JsonProperty("ref_strength") Float refStrength, + @JsonProperty("ref_mode") String refMode) {} + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponse(@JsonProperty("request_id") String requestId, + @JsonProperty("output") DashScopeImageAsyncReponseOutput output, + @JsonProperty("usage") DashScopeImageAsyncReponseUsage usage) { + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponseOutput(@JsonProperty("task_id") String taskId, + @JsonProperty("task_status") String taskStatus, + @JsonProperty("results") List results, + @JsonProperty("task_metrics") DashScopeImageAsyncReponseTaskMetrics taskMetrics, + @JsonProperty("code") String code, + @JsonProperty("message") String message) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponseTaskMetrics(@JsonProperty("TOTAL") Integer total, + @JsonProperty("SUCCEEDED") Integer SUCCEEDED, + @JsonProperty("FAILED") Integer FAILED) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponseUsage(@JsonProperty("image_count") Integer imageCount) {} + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponseResult(@JsonProperty("url") String url) {} + } } diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java index d0dc9cd20..409266c4b 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java @@ -18,6 +18,7 @@ import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants; import com.alibaba.cloud.ai.dashscope.image.observation.DashScopeImageModelObservationConvention; +import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,7 +28,6 @@ import org.springframework.ai.image.observation.ImageModelObservationDocumentation; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.retry.RetryUtils; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -77,6 +77,14 @@ public class DashScopeImageModel implements ImageModel { */ private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi) { + + this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; + this.observationRegistry = ObservationRegistry.NOOP; + } + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); @@ -106,27 +114,73 @@ public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, DashScopeImageO @Override public ImageResponse call (ImagePrompt prompt) { - ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(prompt).provider(DashScopeApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions).build(); - - return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).observe(() -> { - - DashScopeImageApi.DashScopeImageRequest request = constructImageRequest(prompt, toImageOptions(prompt.getOptions())); - - ResponseEntity completionEntity = this.retryTemplate.execute(ctx -> this.dashScopeImageApi.submitImageGenTask(request), ctx -> new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)); - - var imageCompletion = completionEntity.getBody(); + String taskId = submitImageGenTask(prompt); + if (taskId == null) { + return new ImageResponse(List.of()); + } - if (imageCompletion == null || imageCompletion.output() == null) { - logger.warn("No Image completion returned for prompt: {}", prompt); - return new ImageResponse(List.of()); +// ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(prompt).provider(DashScopeApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions).build(); + +// Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry); + + // ImageResponse imageResponse = observation.observe(() -> { + int retryCount = 0; + while (retryCount < 10) { + DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); + if (getResultResponse != null) { + DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse.output(); + String taskStatus = output.taskStatus(); + switch (taskStatus) { + case "SUCCEEDED" -> { + return toImageResponse(output); + } + case "FAILED", "UNKNOWN" -> { + return new ImageResponse(List.of()); + } + } } - - ImageResponse response = toImageResponse(imageCompletion.output()); - observationContext.setResponse(response); - return response; - }); + try { + Thread.sleep(15000L); + retryCount++; + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + return new ImageResponse(null); + // }); + // return imageResponse; } + // String taskId = submitImageGenTask(request); + // if (taskId == null) { + // return new ImageResponse(List.of()); + // } + + // return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).observe(() -> { + // + // ImageResponse response = retryTemplate.execute(ctx -> { + // DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput imageGenTaskResult = Objects.requireNonNull(dashScopeImageApi.getImageGenTaskResult(taskId).getBody()).output(); + // String taskStatus = imageGenTaskResult.taskStatus(); + // if ("SUCCEEDED".equals(taskStatus)) { + // return toImageResponse(imageGenTaskResult); + // } + // return new ImageResponse(List.of()); + // }); + // + // // var imageCompletion = completionEntity.getBody(); + // // + // // if (imageCompletion == null || imageCompletion.output() == null) { + // // logger.warn("No Image completion returned for prompt: {}", prompt); + // // return new ImageResponse(List.of()); + // // } + // // + // // ImageResponse response = toImageResponse(imageCompletion.output()); + // observationContext.setResponse(response); + // return response; + // }); + + /** * Merge Image options. Notice: Programmatically set options parameters take * precedence @@ -169,4 +223,30 @@ public void setObservationConvention (ImageModelObservationConvention observatio Assert.notNull(observationConvention, "observationConvention cannot be null"); this.observationConvention = observationConvention; } + + public String submitImageGenTask (ImagePrompt request) { + + DashScopeImageOptions imageOptions = toImageOptions(request.getOptions()); + logger.debug("Image options: {}", imageOptions); + + DashScopeImageApi.DashScopeImageRequest dashScopeImageRequest = constructImageRequest(request, imageOptions); + + ResponseEntity submitResponse = dashScopeImageApi.submitImageGenTask(dashScopeImageRequest); + + if (submitResponse == null || submitResponse.getBody() == null) { + logger.warn("Submit imageGen error,request: {}", request); + return null; + } + + return submitResponse.getBody().output().taskId(); + } + + public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask (String taskId) { + ResponseEntity getImageGenResponse = dashScopeImageApi.getImageGenTaskResult(taskId); + if (getImageGenResponse == null || getImageGenResponse.getBody() == null) { + logger.warn("No image response returned for taskId: {}", taskId); + return null; + } + return getImageGenResponse.getBody(); + } } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java index e0685e538..fd97d19a9 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java @@ -16,15 +16,14 @@ package com.alibaba.cloud.ai.dashscope.image; import com.alibaba.cloud.ai.dashscope.DashscopeAiTestConfiguration; +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; - -import org.springframework.ai.image.Image; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.image.ImageOptionsBuilder; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.image.ImageResponseMetadata; +import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention; +import org.springframework.ai.image.*; +import org.springframework.ai.image.observation.DefaultImageModelObservationConvention; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -35,29 +34,70 @@ @EnabledIfEnvironmentVariable(named = "DASHSCOPE_HTTP_BASE_URL", matches = ".+") public class DashScopeImageModelIT { - @Autowired - protected ImageModel imageModel; + @Autowired + protected ImageModel imageModel; + + @Autowired + TestObservationRegistry observationRegistry; + + @Test + void imageModelObservationTest () { + + var options = ImageOptionsBuilder.builder() + .model("wanx2.1-t2i-turbo") + .withHeight(1024) + .withWidth(1024) + .N(1) + .build(); + + var instructions = """ + A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; + + ImagePrompt imagePrompt = new ImagePrompt(instructions, options); + + ImageResponse imageResponse = imageModel.call(imagePrompt); + +// assertThat(imageResponse.getResults()).hasSize(1); + + ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); + assertThat(imageResponseMetadata.getCreated()).isPositive(); + + var generation = imageResponse.getResult(); + Image image = generation.getOutput(); + assertThat(image.getUrl()).isNotEmpty(); +// assertThat(image.getB64Json()).isNull(); + + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .doesNotHaveAnyRemainingCurrentObservation() + .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) + .that().hasContextualNameEqualTo("image" + DashScopeImageApi.ImageModel.WANX_V2_T2I_TURBO); + } - @Test - void imageAsUrlTest() { - var options = ImageOptionsBuilder.builder().withHeight(1024).withWidth(1024).build(); - var instructions = """ - A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; + @Test + void imageAsUrlTest () { + var options = ImageOptionsBuilder.builder() + .model("wanx2.1-t2i-turbo") + .withHeight(1024) + .withWidth(1024) + .N(1) + .build(); - ImagePrompt imagePrompt = new ImagePrompt(instructions, options); + var instructions = """ + A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; - ImageResponse imageResponse = imageModel.call(imagePrompt); + ImagePrompt imagePrompt = new ImagePrompt(instructions, options); - assertThat(imageResponse.getResults()).hasSize(1); + ImageResponse imageResponse = imageModel.call(imagePrompt); - ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); - assertThat(imageResponseMetadata.getCreated()).isPositive(); + assertThat(imageResponse.getResults()).hasSize(1); - var generation = imageResponse.getResult(); - Image image = generation.getOutput(); - assertThat(image.getUrl()).isNotEmpty(); - assertThat(image.getB64Json()).isNull(); - } + ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); + assertThat(imageResponseMetadata.getCreated()).isPositive(); + var generation = imageResponse.getResult(); + Image image = generation.getOutput(); + assertThat(image.getUrl()).isNotEmpty(); + assertThat(image.getB64Json()).isNull(); + } } From c961d7405c37456cdee4c7209f5cc44b0137f190 Mon Sep 17 00:00:00 2001 From: PolarishT Date: Sun, 12 Jan 2025 23:17:27 +0800 Subject: [PATCH 4/9] [WIP Observation]: add imageModel observation Signed-off-by: PolarishT --- .../ai/dashscope/api/DashScopeImageApi.java | 4 +- .../dashscope/image/DashScopeImageModel.java | 93 ++++++++----------- .../image/DashScopeImageModelIT.java | 18 +++- .../src/test/resources/application.yml | 2 +- 4 files changed, 53 insertions(+), 64 deletions(-) diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java index e494e93b6..4c60fc1fc 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java @@ -74,9 +74,7 @@ public ResponseEntity getImageGenTaskResult (String public enum ImageModel { - WANX_V1("wanx-v1"), - - WANX_V2_T2I_TURBO("wanx2.1-t2i-turbo"); + WANX_V1("wanx-v1"); public final String value; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java index 409266c4b..0f6338f0e 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java @@ -17,6 +17,7 @@ import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants; +import com.alibaba.cloud.ai.dashscope.common.DashScopeException; import com.alibaba.cloud.ai.dashscope.image.observation.DashScopeImageModelObservationConvention; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; @@ -31,7 +32,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import java.util.Base64; import java.util.List; import java.util.Objects; @@ -119,68 +122,43 @@ public ImageResponse call (ImagePrompt prompt) { return new ImageResponse(List.of()); } -// ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(prompt).provider(DashScopeApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions).build(); + ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(prompt).provider(DashScopeApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions).build(); -// Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry); + Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry); - // ImageResponse imageResponse = observation.observe(() -> { - int retryCount = 0; - while (retryCount < 10) { - DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); - if (getResultResponse != null) { - DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse.output(); - String taskStatus = output.taskStatus(); - switch (taskStatus) { - case "SUCCEEDED" -> { - return toImageResponse(output); - } - case "FAILED", "UNKNOWN" -> { - return new ImageResponse(List.of()); - } - } - } + return observation.observe(() -> { + ImageResponse imageResponse = null; try { - Thread.sleep(15000L); - retryCount++; + imageResponse = this.retryTemplate.execute(ctx -> { + + DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); + + int maxAttempts = 3; + int attempt = 0; + + while (attempt < maxAttempts) { + if (!isTaskCompleted(getResultResponse)) { + getResultResponse = getImageGenTask(taskId); + Thread.sleep(7000L); + attempt++; + } + else break; + } + + DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse.output(); + logger.info("current imageModel generated result --> {}", getResultResponse.output()); + + return !CollectionUtils.isEmpty(getResultResponse.output().results()) ? toImageResponse(output) : new ImageResponse(List.of()); + }); } catch (InterruptedException e) { - throw new RuntimeException(e); + throw new DashScopeException("Error while waiting for image generation task to complete", e); } - } - return new ImageResponse(null); - // }); - // return imageResponse; + observationContext.setResponse(imageResponse); + return imageResponse; + }); } - // String taskId = submitImageGenTask(request); - // if (taskId == null) { - // return new ImageResponse(List.of()); - // } - - // return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).observe(() -> { - // - // ImageResponse response = retryTemplate.execute(ctx -> { - // DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput imageGenTaskResult = Objects.requireNonNull(dashScopeImageApi.getImageGenTaskResult(taskId).getBody()).output(); - // String taskStatus = imageGenTaskResult.taskStatus(); - // if ("SUCCEEDED".equals(taskStatus)) { - // return toImageResponse(imageGenTaskResult); - // } - // return new ImageResponse(List.of()); - // }); - // - // // var imageCompletion = completionEntity.getBody(); - // // - // // if (imageCompletion == null || imageCompletion.output() == null) { - // // logger.warn("No Image completion returned for prompt: {}", prompt); - // // return new ImageResponse(List.of()); - // // } - // // - // // ImageResponse response = toImageResponse(imageCompletion.output()); - // observationContext.setResponse(response); - // return response; - // }); - - /** * Merge Image options. Notice: Programmatically set options parameters take * precedence @@ -204,7 +182,7 @@ private ImageResponse toImageResponse (DashScopeImageApi.DashScopeImageAsyncRepo if (genImageList == null || genImageList.isEmpty()) { return new ImageResponse(List.of()); } - List imageGenerationList = genImageList.stream().map(entry -> new ImageGeneration(new Image(entry.url(), null))).toList(); + List imageGenerationList = genImageList.stream().map(entry -> new ImageGeneration(new Image(entry.url(), Base64.getEncoder().encodeToString(entry.url().getBytes())))).toList(); return new ImageResponse(imageGenerationList); } @@ -249,4 +227,9 @@ public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask (String task } return getImageGenResponse.getBody(); } + + private boolean isTaskCompleted (DashScopeImageApi.DashScopeImageAsyncReponse response) { + List checkedTaskStatus = List.of("SUCCEEDED", "FAILED"); + return checkedTaskStatus.contains(response.output().taskStatus()); + } } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java index fd97d19a9..3395a3e50 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java @@ -17,13 +17,17 @@ import com.alibaba.cloud.ai.dashscope.DashscopeAiTestConfiguration; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import com.alibaba.cloud.ai.dashscope.observation.conventions.AiProvider; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.ai.chat.observation.ChatModelObservationDocumentation; import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention; import org.springframework.ai.image.*; import org.springframework.ai.image.observation.DefaultImageModelObservationConvention; +import org.springframework.ai.image.observation.ImageModelObservationDocumentation; +import org.springframework.ai.observation.conventions.AiOperationType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -57,7 +61,7 @@ void imageModelObservationTest () { ImageResponse imageResponse = imageModel.call(imagePrompt); -// assertThat(imageResponse.getResults()).hasSize(1); + assertThat(imageResponse.getResults()).hasSize(1); ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); assertThat(imageResponseMetadata.getCreated()).isPositive(); @@ -65,15 +69,19 @@ void imageModelObservationTest () { var generation = imageResponse.getResult(); Image image = generation.getOutput(); assertThat(image.getUrl()).isNotEmpty(); -// assertThat(image.getB64Json()).isNull(); TestObservationRegistryAssert.assertThat(this.observationRegistry) .doesNotHaveAnyRemainingCurrentObservation() .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) - .that().hasContextualNameEqualTo("image" + DashScopeImageApi.ImageModel.WANX_V2_T2I_TURBO); + .that().hasContextualNameEqualTo("image " + "wanx2.1-t2i-turbo") + .hasHighCardinalityKeyValue(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(),"1024x1024") + .hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), + AiProvider.DASHSCOPE.value()) + .hasLowCardinalityKeyValue( + ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), + AiOperationType.IMAGE.value()); } - @Test void imageAsUrlTest () { var options = ImageOptionsBuilder.builder() @@ -98,6 +106,6 @@ void imageAsUrlTest () { var generation = imageResponse.getResult(); Image image = generation.getOutput(); assertThat(image.getUrl()).isNotEmpty(); - assertThat(image.getB64Json()).isNull(); + assertThat(image.getB64Json()).isNotEmpty(); } } diff --git a/spring-ai-alibaba-core/src/test/resources/application.yml b/spring-ai-alibaba-core/src/test/resources/application.yml index 97b3ae0fc..c63515b72 100644 --- a/spring-ai-alibaba-core/src/test/resources/application.yml +++ b/spring-ai-alibaba-core/src/test/resources/application.yml @@ -1,7 +1,7 @@ spring: ai: dashscope: - api-key: ${AI_DASHSCOPE_API_KEY} + api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen-max From 0f26d1b3d150546e86c4c06ee513d7dc93258fc3 Mon Sep 17 00:00:00 2001 From: PolarishT Date: Sun, 12 Jan 2025 23:24:10 +0800 Subject: [PATCH 5/9] code format Signed-off-by: PolarishT --- .../dashscope/DashScopeAutoConfiguration.java | 458 ++++++++++-------- .../ai/dashscope/api/DashScopeImageApi.java | 215 ++++---- .../dashscope/image/DashScopeImageModel.java | 398 ++++++++------- .../image/DashScopeImageOptions.java | 1 + ...hScopeImageModelObservationConvention.java | 13 +- .../dashscope/DashScopeAutoConfiguration.java | 423 +++++++++------- .../DashscopeAiTestConfiguration.java | 209 ++++---- .../image/DashScopeImageModelIT.java | 110 +++-- 8 files changed, 1007 insertions(+), 820 deletions(-) diff --git a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java index b54c6a592..2197d5ee4 100644 --- a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java +++ b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeAutoConfiguration.java @@ -70,207 +70,265 @@ * @since 2024/8/16 11:45 */ @ConditionalOnClass(DashScopeApi.class) -@AutoConfiguration(after = {RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class}) -@EnableConfigurationProperties({DashScopeConnectionProperties.class, DashScopeChatProperties.class, DashScopeImageProperties.class, DashScopeSpeechSynthesisProperties.class, DashScopeAudioTranscriptionProperties.class, DashScopeEmbeddingProperties.class, DashScopeRerankProperties.class}) -@ImportAutoConfiguration(classes = {SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class}) +@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, + SpringAiRetryAutoConfiguration.class }) +@EnableConfigurationProperties({ DashScopeConnectionProperties.class, DashScopeChatProperties.class, + DashScopeImageProperties.class, DashScopeSpeechSynthesisProperties.class, + DashScopeAudioTranscriptionProperties.class, DashScopeEmbeddingProperties.class, + DashScopeRerankProperties.class }) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) public class DashScopeAutoConfiguration { - private static @NotNull ResolvedConnectionProperties resolveConnectionProperties (DashScopeParentProperties commonProperties, DashScopeParentProperties modelProperties, String modelType) { - - String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() : commonProperties.getBaseUrl(); - String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() : commonProperties.getApiKey(); - String workspaceId = StringUtils.hasText(modelProperties.getWorkspaceId()) ? modelProperties.getWorkspaceId() : commonProperties.getWorkspaceId(); - - Map> connectionHeaders = new HashMap<>(); - if (StringUtils.hasText(workspaceId)) { - connectionHeaders.put("DashScope-Workspace", List.of(workspaceId)); - } - - // get apikey from system env. - if (Objects.isNull(apiKey)) { - if (Objects.nonNull(System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY))) { - apiKey = System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY); - } - } - - Assert.hasText(baseUrl, "DashScope base URL must be set. Use the connection property: spring.ai.dashscope.base-url or spring.ai.dashscope." + modelType + ".base-url property."); - Assert.hasText(apiKey, "DashScope API key must be set. Use the connection property: spring.ai.dashscope.api-key or spring.ai.dashscope." + modelType + ".api-key property."); - - return new ResolvedConnectionProperties(baseUrl, apiKey, workspaceId, CollectionUtils.toMultiValueMap(connectionHeaders)); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public SpeechSynthesizer speechSynthesizer () { - return new SpeechSynthesizer(); - } - - @Bean - @ConditionalOnMissingBean - public Transcription transcription () { - return new Transcription(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeChatModel dashscopeChatModel (DashScopeConnectionProperties commonProperties, - DashScopeChatProperties chatProperties, - RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, - List toolFunctionCallbacks, - FunctionCallbackResolver functionCallbackResolver, - RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, responseErrorHandler); - - var dashscopeModel = new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackResolver, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(dashscopeModel::setObservationConvention); - - return dashscopeModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeApi dashscopeChatApi (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); - - return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeEmbeddingModel dashscopeEmbeddingModel (DashScopeConnectionProperties commonProperties, DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { - - var dashScopeApi = dashscopeEmbeddingApi(commonProperties, embeddingProperties, restClientBuilder, webClientBuilder, responseErrorHandler); - - var embeddingModel = new DashScopeEmbeddingModel(dashScopeApi, embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - public DashScopeApi dashscopeEmbeddingApi (DashScopeConnectionProperties commonProperties, DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, "embedding"); - - return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi (DashScopeConnectionProperties commonProperties, DashScopeSpeechSynthesisProperties speechSynthesisProperties) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, "speechsynthesis"); - - return new DashScopeSpeechSynthesisApi(resolved.apiKey()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi (DashScopeConnectionProperties commonProperties, DashScopeAudioTranscriptionProperties audioTranscriptionProperties) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, audioTranscriptionProperties, "audiotranscription"); - - return new DashScopeAudioTranscriptionApi(resolved.apiKey()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeAgentApi dashscopeAgentApi (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); - - return new DashScopeAgentApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - } - - @Bean - public RestClientCustomizer restClientCustomizer (DashScopeConnectionProperties commonProperties) { - return restClientBuilder -> restClientBuilder.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS.withReadTimeout(Duration.ofSeconds(commonProperties.getReadTimeout())))); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeImageModel dashScopeImageModel (DashScopeConnectionProperties commonProperties, - DashScopeImageProperties imageProperties, - RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, - RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); - - var dashScopeImageApi = new DashScopeImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - - var dashScopeImageModel = new DashScopeImageModel(dashScopeImageApi, imageProperties.getOptions(), retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(dashScopeImageModel::setObservationConvention); - - return dashScopeImageModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeRerankProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeRerankModel dashscopeRerankModel (DashScopeConnectionProperties commonProperties, DashScopeRerankProperties rerankProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, rerankProperties, "rerank"); - - var dashscopeApi = new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - - return new DashScopeRerankModel(dashscopeApi, rerankProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel (DashScopeConnectionProperties commonProperties, DashScopeSpeechSynthesisProperties speechSynthesisProperties, RetryTemplate retryTemplate) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, "speechsynthesis"); - - var dashScopeSpeechSynthesisApi = dashScopeSpeechSynthesisApi(commonProperties, speechSynthesisProperties); - - return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, speechSynthesisProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel (DashScopeConnectionProperties commonProperties, DashScopeAudioTranscriptionProperties audioTranscriptionProperties, RetryTemplate retryTemplate) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, audioTranscriptionProperties, "audiotranscription"); - - var dashScopeSpeechSynthesisApi = dashScopeAudioTranscriptionApi(commonProperties, audioTranscriptionProperties); - - return new DashScopeAudioTranscriptionModel(dashScopeSpeechSynthesisApi, audioTranscriptionProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager (ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - private record ResolvedConnectionProperties(String baseUrl, String apiKey, String workspaceId, - MultiValueMap headers) {} + private static @NotNull ResolvedConnectionProperties resolveConnectionProperties( + DashScopeParentProperties commonProperties, DashScopeParentProperties modelProperties, String modelType) { + + String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() + : commonProperties.getBaseUrl(); + String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() + : commonProperties.getApiKey(); + String workspaceId = StringUtils.hasText(modelProperties.getWorkspaceId()) ? modelProperties.getWorkspaceId() + : commonProperties.getWorkspaceId(); + + Map> connectionHeaders = new HashMap<>(); + if (StringUtils.hasText(workspaceId)) { + connectionHeaders.put("DashScope-Workspace", List.of(workspaceId)); + } + + // get apikey from system env. + if (Objects.isNull(apiKey)) { + if (Objects.nonNull(System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY))) { + apiKey = System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY); + } + } + + Assert.hasText(baseUrl, + "DashScope base URL must be set. Use the connection property: spring.ai.dashscope.base-url or spring.ai.dashscope." + + modelType + ".base-url property."); + Assert.hasText(apiKey, + "DashScope API key must be set. Use the connection property: spring.ai.dashscope.api-key or spring.ai.dashscope." + + modelType + ".api-key property."); + + return new ResolvedConnectionProperties(baseUrl, apiKey, workspaceId, + CollectionUtils.toMultiValueMap(connectionHeaders)); + } + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public SpeechSynthesizer speechSynthesizer() { + return new SpeechSynthesizer(); + } + + @Bean + @ConditionalOnMissingBean + public Transcription transcription() { + return new Transcription(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeChatModel dashscopeChatModel(DashScopeConnectionProperties commonProperties, + DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, List toolFunctionCallbacks, + FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + + if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { + chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); + } + + var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, + responseErrorHandler); + + var dashscopeModel = new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackResolver, + retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(dashscopeModel::setObservationConvention); + + return dashscopeModel; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeApi dashscopeChatApi(DashScopeConnectionProperties commonProperties, + DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); + + return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, + webClientBuilder, responseErrorHandler); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeEmbeddingModel dashscopeEmbeddingModel(DashScopeConnectionProperties commonProperties, + DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, + ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + + var dashScopeApi = dashscopeEmbeddingApi(commonProperties, embeddingProperties, restClientBuilder, + webClientBuilder, responseErrorHandler); + + var embeddingModel = new DashScopeEmbeddingModel(dashScopeApi, embeddingProperties.getMetadataMode(), + embeddingProperties.getOptions(), retryTemplate, + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(embeddingModel::setObservationConvention); + + return embeddingModel; + } + + public DashScopeApi dashscopeEmbeddingApi(DashScopeConnectionProperties commonProperties, + DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, + "embedding"); + + return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, + webClientBuilder, responseErrorHandler); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) + public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi(DashScopeConnectionProperties commonProperties, + DashScopeSpeechSynthesisProperties speechSynthesisProperties) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, + "speechsynthesis"); + + return new DashScopeSpeechSynthesisApi(resolved.apiKey()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) + public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi(DashScopeConnectionProperties commonProperties, + DashScopeAudioTranscriptionProperties audioTranscriptionProperties) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, + audioTranscriptionProperties, "audiotranscription"); + + return new DashScopeAudioTranscriptionApi(resolved.apiKey()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeAgentApi dashscopeAgentApi(DashScopeConnectionProperties commonProperties, + DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); + + return new DashScopeAgentApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, + webClientBuilder, responseErrorHandler); + } + + @Bean + public RestClientCustomizer restClientCustomizer(DashScopeConnectionProperties commonProperties) { + return restClientBuilder -> restClientBuilder + .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS + .withReadTimeout(Duration.ofSeconds(commonProperties.getReadTimeout())))); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeImageModel dashScopeImageModel(DashScopeConnectionProperties commonProperties, + DashScopeImageProperties imageProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, + ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); + + var dashScopeImageApi = new DashScopeImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), + restClientBuilder, webClientBuilder, responseErrorHandler); + + var dashScopeImageModel = new DashScopeImageModel(dashScopeImageApi, imageProperties.getOptions(), + retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(dashScopeImageModel::setObservationConvention); + + return dashScopeImageModel; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeRerankProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeRerankModel dashscopeRerankModel(DashScopeConnectionProperties commonProperties, + DashScopeRerankProperties rerankProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler) { + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, rerankProperties, + "rerank"); + + var dashscopeApi = new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), + restClientBuilder, webClientBuilder, responseErrorHandler); + + return new DashScopeRerankModel(dashscopeApi, rerankProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) + public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel(DashScopeConnectionProperties commonProperties, + DashScopeSpeechSynthesisProperties speechSynthesisProperties, RetryTemplate retryTemplate) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, + "speechsynthesis"); + + var dashScopeSpeechSynthesisApi = dashScopeSpeechSynthesisApi(commonProperties, speechSynthesisProperties); + + return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, speechSynthesisProperties.getOptions(), + retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) + public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel( + DashScopeConnectionProperties commonProperties, + DashScopeAudioTranscriptionProperties audioTranscriptionProperties, RetryTemplate retryTemplate) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, + audioTranscriptionProperties, "audiotranscription"); + + var dashScopeSpeechSynthesisApi = dashScopeAudioTranscriptionApi(commonProperties, + audioTranscriptionProperties); + + return new DashScopeAudioTranscriptionModel(dashScopeSpeechSynthesisApi, + audioTranscriptionProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { + DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); + manager.setApplicationContext(context); + return manager; + } + + private record ResolvedConnectionProperties(String baseUrl, String apiKey, String workspaceId, + MultiValueMap headers) { + } } diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java index 4c60fc1fc..33e8a7706 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java @@ -33,103 +33,122 @@ */ public class DashScopeImageApi { - // public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); - public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); - - private final RestClient restClient; - - public DashScopeImageApi (String apiKey) { - this(DEFAULT_BASE_URL, apiKey, RestClient.builder(), WebClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); - } - - public DashScopeImageApi (String apiKey, String workSpaceId) { - this(DEFAULT_BASE_URL, apiKey, workSpaceId, RestClient.builder(), WebClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); - } - - public DashScopeImageApi (String baseUrl, String apiKey, String workSpaceId) { - this(baseUrl, apiKey, workSpaceId, RestClient.builder(), WebClient.builder(), RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); - } - - public DashScopeImageApi (String baseUrl, String apiKey, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)).defaultStatusHandler(responseErrorHandler).build(); - } - - public DashScopeImageApi (String baseUrl, String apiKey, String workSpaceId, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - this.restClient = restClientBuilder.baseUrl(baseUrl).defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey, workSpaceId)).defaultStatusHandler(responseErrorHandler).build(); - } - - public ResponseEntity submitImageGenTask (DashScopeImageRequest request) { - return this.restClient.post().uri("/api/v1/services/aigc/text2image/image-synthesis") - // issue: https://github.com/alibaba/spring-ai-alibaba/issues/29 - .header("X-DashScope-Async", "enable").body(request).retrieve().toEntity(DashScopeImageAsyncReponse.class); - } - - public ResponseEntity getImageGenTaskResult (String taskId) { - return this.restClient.get().uri("/api/v1/tasks/{task_id}", taskId).retrieve().toEntity(DashScopeImageAsyncReponse.class); - } - - /******************************************* - * Embedding相关 - **********************************************/ - - public enum ImageModel { - - WANX_V1("wanx-v1"); - - public final String value; - - ImageModel (String value) { - this.value = value; - } - - public String getValue () { - return value; - } - - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageRequest(@JsonProperty("model") String model, - @JsonProperty("input") DashScopeImageRequestInput input, - @JsonProperty("parameters") DashScopeImageRequestParameter parameters - - ) { - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageRequestInput(@JsonProperty("prompt") String prompt, - @JsonProperty("negative_prompt") String negativePrompt, - @JsonProperty("ref_img") String refImg) {} - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageRequestParameter(@JsonProperty("style") String style, - @JsonProperty("size") String size, @JsonProperty("n") Integer n, - @JsonProperty("seed") Integer seed, - @JsonProperty("ref_strength") Float refStrength, - @JsonProperty("ref_mode") String refMode) {} - } - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponse(@JsonProperty("request_id") String requestId, - @JsonProperty("output") DashScopeImageAsyncReponseOutput output, - @JsonProperty("usage") DashScopeImageAsyncReponseUsage usage) { - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponseOutput(@JsonProperty("task_id") String taskId, - @JsonProperty("task_status") String taskStatus, - @JsonProperty("results") List results, - @JsonProperty("task_metrics") DashScopeImageAsyncReponseTaskMetrics taskMetrics, - @JsonProperty("code") String code, - @JsonProperty("message") String message) {} - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponseTaskMetrics(@JsonProperty("TOTAL") Integer total, - @JsonProperty("SUCCEEDED") Integer SUCCEEDED, - @JsonProperty("FAILED") Integer FAILED) {} - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponseUsage(@JsonProperty("image_count") Integer imageCount) {} - - @JsonInclude(JsonInclude.Include.NON_NULL) - public record DashScopeImageAsyncReponseResult(@JsonProperty("url") String url) {} - } + // public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); + public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); + + private final RestClient restClient; + + public DashScopeImageApi(String apiKey) { + this(DEFAULT_BASE_URL, apiKey, RestClient.builder(), WebClient.builder(), + RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); + } + + public DashScopeImageApi(String apiKey, String workSpaceId) { + this(DEFAULT_BASE_URL, apiKey, workSpaceId, RestClient.builder(), WebClient.builder(), + RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); + } + + public DashScopeImageApi(String baseUrl, String apiKey, String workSpaceId) { + this(baseUrl, apiKey, workSpaceId, RestClient.builder(), WebClient.builder(), + RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER); + } + + public DashScopeImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + this.restClient = restClientBuilder.baseUrl(baseUrl) + .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)) + .defaultStatusHandler(responseErrorHandler) + .build(); + } + + public DashScopeImageApi(String baseUrl, String apiKey, String workSpaceId, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + this.restClient = restClientBuilder.baseUrl(baseUrl) + .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey, workSpaceId)) + .defaultStatusHandler(responseErrorHandler) + .build(); + } + + public ResponseEntity submitImageGenTask(DashScopeImageRequest request) { + return this.restClient.post() + .uri("/api/v1/services/aigc/text2image/image-synthesis") + // issue: https://github.com/alibaba/spring-ai-alibaba/issues/29 + .header("X-DashScope-Async", "enable") + .body(request) + .retrieve() + .toEntity(DashScopeImageAsyncReponse.class); + } + + public ResponseEntity getImageGenTaskResult(String taskId) { + return this.restClient.get() + .uri("/api/v1/tasks/{task_id}", taskId) + .retrieve() + .toEntity(DashScopeImageAsyncReponse.class); + } + + /******************************************* + * Embedding相关 + **********************************************/ + + public enum ImageModel { + + WANX_V1("wanx-v1"); + + public final String value; + + ImageModel(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageRequest(@JsonProperty("model") String model, + @JsonProperty("input") DashScopeImageRequestInput input, + @JsonProperty("parameters") DashScopeImageRequestParameter parameters + + ) { + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageRequestInput(@JsonProperty("prompt") String prompt, + @JsonProperty("negative_prompt") String negativePrompt, @JsonProperty("ref_img") String refImg) { + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageRequestParameter(@JsonProperty("style") String style, + @JsonProperty("size") String size, @JsonProperty("n") Integer n, @JsonProperty("seed") Integer seed, + @JsonProperty("ref_strength") Float refStrength, @JsonProperty("ref_mode") String refMode) { + } + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponse(@JsonProperty("request_id") String requestId, + @JsonProperty("output") DashScopeImageAsyncReponseOutput output, + @JsonProperty("usage") DashScopeImageAsyncReponseUsage usage) { + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponseOutput(@JsonProperty("task_id") String taskId, + @JsonProperty("task_status") String taskStatus, + @JsonProperty("results") List results, + @JsonProperty("task_metrics") DashScopeImageAsyncReponseTaskMetrics taskMetrics, + @JsonProperty("code") String code, @JsonProperty("message") String message) { + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponseTaskMetrics(@JsonProperty("TOTAL") Integer total, + @JsonProperty("SUCCEEDED") Integer SUCCEEDED, @JsonProperty("FAILED") Integer FAILED) { + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponseUsage(@JsonProperty("image_count") Integer imageCount) { + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record DashScopeImageAsyncReponseResult(@JsonProperty("url") String url) { + } + } } diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java index 0f6338f0e..de6edb4af 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java @@ -46,190 +46,216 @@ */ public class DashScopeImageModel implements ImageModel { - private static final Logger logger = LoggerFactory.getLogger(DashScopeImageModel.class); - - private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DashScopeImageModelObservationConvention(); - - /** - * The default model used for the image completion requests. - */ - private static final String DEFAULT_MODEL = DashScopeImageApi.DEFAULT_IMAGE_MODEL; - - /** - * Low-level access to the DashScope Image API. - */ - private final DashScopeImageApi dashScopeImageApi; - - /** - * Observation registry used for instrumentation. - */ - private final ObservationRegistry observationRegistry; - - /** - * The default options used for the image completion requests. - */ - private final DashScopeImageOptions defaultOptions; - - /** - * The retry template used to retry the OpenAI Image API calls. - */ - private final RetryTemplate retryTemplate; - - /** - * Conventions to use for generating observations. - */ - private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; - - public DashScopeImageModel (DashScopeImageApi dashScopeImageApi) { - - this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; - this.observationRegistry = ObservationRegistry.NOOP; - } - - public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; - this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; - } - - public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry, DashScopeImageOptions options, RetryTemplate retryTemplate) { - this.observationRegistry = observationRegistry; - this.defaultOptions = options; - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = retryTemplate; - } - - public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options, RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { - - Assert.notNull(dashScopeImageApi, "DashScopeImageApi must not be null"); - Assert.notNull(options, "options must not be null"); - Assert.notNull(retryTemplate, "retryTemplate must not be null"); - - this.dashScopeImageApi = dashScopeImageApi; - this.defaultOptions = options; - this.retryTemplate = retryTemplate; - this.observationRegistry = observationRegistry; - } - - @Override - public ImageResponse call (ImagePrompt prompt) { - - String taskId = submitImageGenTask(prompt); - if (taskId == null) { - return new ImageResponse(List.of()); - } - - ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(prompt).provider(DashScopeApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions).build(); - - Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry); - - return observation.observe(() -> { - ImageResponse imageResponse = null; - try { - imageResponse = this.retryTemplate.execute(ctx -> { - - DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); - - int maxAttempts = 3; - int attempt = 0; - - while (attempt < maxAttempts) { - if (!isTaskCompleted(getResultResponse)) { - getResultResponse = getImageGenTask(taskId); - Thread.sleep(7000L); - attempt++; - } - else break; - } - - DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse.output(); - logger.info("current imageModel generated result --> {}", getResultResponse.output()); - - return !CollectionUtils.isEmpty(getResultResponse.output().results()) ? toImageResponse(output) : new ImageResponse(List.of()); - }); - } - catch (InterruptedException e) { - throw new DashScopeException("Error while waiting for image generation task to complete", e); - } - observationContext.setResponse(imageResponse); - return imageResponse; - }); - } - - /** - * Merge Image options. Notice: Programmatically set options parameters take - * precedence - */ - private DashScopeImageOptions toImageOptions (ImageOptions runtimeOptions) { - - // set default image model - var currentOptions = DashScopeImageOptions.builder().withModel(DEFAULT_MODEL).build(); - - if (Objects.nonNull(runtimeOptions)) { - currentOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, DashScopeImageOptions.class); - } - - currentOptions = ModelOptionsUtils.merge(currentOptions, this.defaultOptions, DashScopeImageOptions.class); - - return currentOptions; - } - - private ImageResponse toImageResponse (DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output) { - List genImageList = output.results(); - if (genImageList == null || genImageList.isEmpty()) { - return new ImageResponse(List.of()); - } - List imageGenerationList = genImageList.stream().map(entry -> new ImageGeneration(new Image(entry.url(), Base64.getEncoder().encodeToString(entry.url().getBytes())))).toList(); - - return new ImageResponse(imageGenerationList); - } - - private DashScopeImageApi.DashScopeImageRequest constructImageRequest (ImagePrompt imagePrompt, DashScopeImageOptions options) { - - return new DashScopeImageApi.DashScopeImageRequest(options.getModel(), new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestInput(imagePrompt.getInstructions().get(0).getText(), options.getNegativePrompt(), options.getRefImg()), new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestParameter(options.getStyle(), options.getSize(), options.getN(), options.getSeed(), options.getRefStrength(), options.getRefMode())); - } - - /** - * Use the provided convention for reporting observation data - * - * @param observationConvention The provided convention - */ - public void setObservationConvention (ImageModelObservationConvention observationConvention) { - Assert.notNull(observationConvention, "observationConvention cannot be null"); - this.observationConvention = observationConvention; - } - - public String submitImageGenTask (ImagePrompt request) { - - DashScopeImageOptions imageOptions = toImageOptions(request.getOptions()); - logger.debug("Image options: {}", imageOptions); - - DashScopeImageApi.DashScopeImageRequest dashScopeImageRequest = constructImageRequest(request, imageOptions); - - ResponseEntity submitResponse = dashScopeImageApi.submitImageGenTask(dashScopeImageRequest); - - if (submitResponse == null || submitResponse.getBody() == null) { - logger.warn("Submit imageGen error,request: {}", request); - return null; - } - - return submitResponse.getBody().output().taskId(); - } - - public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask (String taskId) { - ResponseEntity getImageGenResponse = dashScopeImageApi.getImageGenTaskResult(taskId); - if (getImageGenResponse == null || getImageGenResponse.getBody() == null) { - logger.warn("No image response returned for taskId: {}", taskId); - return null; - } - return getImageGenResponse.getBody(); - } - - private boolean isTaskCompleted (DashScopeImageApi.DashScopeImageAsyncReponse response) { - List checkedTaskStatus = List.of("SUCCEEDED", "FAILED"); - return checkedTaskStatus.contains(response.output().taskStatus()); - } + private static final Logger logger = LoggerFactory.getLogger(DashScopeImageModel.class); + + private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DashScopeImageModelObservationConvention(); + + /** + * The default model used for the image completion requests. + */ + private static final String DEFAULT_MODEL = DashScopeImageApi.DEFAULT_IMAGE_MODEL; + + /** + * Low-level access to the DashScope Image API. + */ + private final DashScopeImageApi dashScopeImageApi; + + /** + * Observation registry used for instrumentation. + */ + private final ObservationRegistry observationRegistry; + + /** + * The default options used for the image completion requests. + */ + private final DashScopeImageOptions defaultOptions; + + /** + * The retry template used to retry the OpenAI Image API calls. + */ + private final RetryTemplate retryTemplate; + + /** + * Conventions to use for generating observations. + */ + private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi) { + + this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; + this.observationRegistry = ObservationRegistry.NOOP; + } + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; + } + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry, + DashScopeImageOptions options, RetryTemplate retryTemplate) { + this.observationRegistry = observationRegistry; + this.defaultOptions = options; + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = retryTemplate; + } + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options, + RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { + + Assert.notNull(dashScopeImageApi, "DashScopeImageApi must not be null"); + Assert.notNull(options, "options must not be null"); + Assert.notNull(retryTemplate, "retryTemplate must not be null"); + + this.dashScopeImageApi = dashScopeImageApi; + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + this.observationRegistry = observationRegistry; + } + + @Override + public ImageResponse call(ImagePrompt prompt) { + + String taskId = submitImageGenTask(prompt); + if (taskId == null) { + return new ImageResponse(List.of()); + } + + ImageModelObservationContext observationContext = ImageModelObservationContext.builder() + .imagePrompt(prompt) + .provider(DashScopeApiConstants.PROVIDER_NAME) + .requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions) + .build(); + + Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation( + this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, + this.observationRegistry); + + return observation.observe(() -> { + ImageResponse imageResponse = null; + try { + imageResponse = this.retryTemplate.execute(ctx -> { + + DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); + + int maxAttempts = 3; + int attempt = 0; + + while (attempt < maxAttempts) { + if (!isTaskCompleted(getResultResponse)) { + getResultResponse = getImageGenTask(taskId); + Thread.sleep(10000L); + attempt++; + } + else + break; + } + + DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse + .output(); + logger.info("current imageModel generated result --> {}", getResultResponse.output()); + + return !CollectionUtils.isEmpty(getResultResponse.output().results()) ? toImageResponse(output) + : new ImageResponse(List.of()); + }); + } + catch (InterruptedException e) { + throw new DashScopeException("Error while waiting for image generation task to complete", e); + } + observationContext.setResponse(imageResponse); + return imageResponse; + }); + } + + /** + * Merge Image options. Notice: Programmatically set options parameters take + * precedence + */ + private DashScopeImageOptions toImageOptions(ImageOptions runtimeOptions) { + + // set default image model + var currentOptions = DashScopeImageOptions.builder().withModel(DEFAULT_MODEL).build(); + + if (Objects.nonNull(runtimeOptions)) { + currentOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, + DashScopeImageOptions.class); + } + + currentOptions = ModelOptionsUtils.merge(currentOptions, this.defaultOptions, DashScopeImageOptions.class); + + return currentOptions; + } + + private ImageResponse toImageResponse( + DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output) { + List genImageList = output + .results(); + if (genImageList == null || genImageList.isEmpty()) { + return new ImageResponse(List.of()); + } + List imageGenerationList = genImageList.stream() + .map(entry -> new ImageGeneration( + new Image(entry.url(), Base64.getEncoder().encodeToString(entry.url().getBytes())))) + .toList(); + + return new ImageResponse(imageGenerationList); + } + + private DashScopeImageApi.DashScopeImageRequest constructImageRequest(ImagePrompt imagePrompt, + DashScopeImageOptions options) { + + return new DashScopeImageApi.DashScopeImageRequest(options.getModel(), + new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestInput( + imagePrompt.getInstructions().get(0).getText(), options.getNegativePrompt(), + options.getRefImg()), + new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestParameter(options.getStyle(), + options.getSize(), options.getN(), options.getSeed(), options.getRefStrength(), + options.getRefMode())); + } + + /** + * Use the provided convention for reporting observation data + * @param observationConvention The provided convention + */ + public void setObservationConvention(ImageModelObservationConvention observationConvention) { + Assert.notNull(observationConvention, "observationConvention cannot be null"); + this.observationConvention = observationConvention; + } + + public String submitImageGenTask(ImagePrompt request) { + + DashScopeImageOptions imageOptions = toImageOptions(request.getOptions()); + logger.debug("Image options: {}", imageOptions); + + DashScopeImageApi.DashScopeImageRequest dashScopeImageRequest = constructImageRequest(request, imageOptions); + + ResponseEntity submitResponse = dashScopeImageApi + .submitImageGenTask(dashScopeImageRequest); + + if (submitResponse == null || submitResponse.getBody() == null) { + logger.warn("Submit imageGen error,request: {}", request); + return null; + } + + return submitResponse.getBody().output().taskId(); + } + + public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask(String taskId) { + ResponseEntity getImageGenResponse = dashScopeImageApi + .getImageGenTaskResult(taskId); + if (getImageGenResponse == null || getImageGenResponse.getBody() == null) { + logger.warn("No image response returned for taskId: {}", taskId); + return null; + } + return getImageGenResponse.getBody(); + } + + private boolean isTaskCompleted(DashScopeImageApi.DashScopeImageAsyncReponse response) { + List checkedTaskStatus = List.of("SUCCEEDED", "FAILED"); + return checkedTaskStatus.contains(response.output().taskStatus()); + } + } diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageOptions.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageOptions.java index 256e20b58..c15031e79 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageOptions.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageOptions.java @@ -274,4 +274,5 @@ public DashScopeImageOptions build() { } } + } diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java index 74bdb2981..5de6af89a 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java @@ -37,12 +37,13 @@ */ public class DashScopeImageModelObservationConvention extends DefaultImageModelObservationConvention { - public static final String DEFAULT_NAME = "gen_ai.client.operation"; + public static final String DEFAULT_NAME = "gen_ai.client.operation"; - private static final String ILLEGAL_STOP_CONTENT = ""; + private static final String ILLEGAL_STOP_CONTENT = ""; + + @Override + public String getName() { + return DEFAULT_NAME; + } - @Override - public String getName () { - return DEFAULT_NAME; - } } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeAutoConfiguration.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeAutoConfiguration.java index 097ccf59d..53c5630dd 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeAutoConfiguration.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeAutoConfiguration.java @@ -68,183 +68,254 @@ * @since 2024/8/16 11:45 */ @ConditionalOnClass(DashScopeApi.class) -@AutoConfiguration(after = {RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class}) -@EnableConfigurationProperties({DashScopeConnectionProperties.class, DashScopeChatProperties.class, DashScopeImageProperties.class, DashScopeSpeechSynthesisProperties.class, DashScopeAudioTranscriptionProperties.class, DashScopeEmbeddingProperties.class, DashScopeRerankProperties.class}) -@ImportAutoConfiguration(classes = {SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class}) +@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, + SpringAiRetryAutoConfiguration.class }) +@EnableConfigurationProperties({ DashScopeConnectionProperties.class, DashScopeChatProperties.class, + DashScopeImageProperties.class, DashScopeSpeechSynthesisProperties.class, + DashScopeAudioTranscriptionProperties.class, DashScopeEmbeddingProperties.class, + DashScopeRerankProperties.class }) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) public class DashScopeAutoConfiguration { - private static @NotNull ResolvedConnectionProperties resolveConnectionProperties (DashScopeParentProperties commonProperties, DashScopeParentProperties modelProperties, String modelType) { - - String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() : commonProperties.getBaseUrl(); - String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() : commonProperties.getApiKey(); - String workspaceId = StringUtils.hasText(modelProperties.getWorkspaceId()) ? modelProperties.getWorkspaceId() : commonProperties.getWorkspaceId(); - - Map> connectionHeaders = new HashMap<>(); - if (StringUtils.hasText(workspaceId)) { - connectionHeaders.put("DashScope-Workspace", List.of(workspaceId)); - } - - // get apikey from system env. - if (Objects.isNull(apiKey)) { - if (Objects.nonNull(System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY))) { - apiKey = System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY); - } - } - - Assert.hasText(baseUrl, "DashScope base URL must be set. Use the connection property: spring.ai.dashscope.base-url or spring.ai.dashscope." + modelType + ".base-url property."); - Assert.hasText(apiKey, "DashScope API key must be set. Use the connection property: spring.ai.dashscope.api-key or spring.ai.dashscope." + modelType + ".api-key property."); - - return new ResolvedConnectionProperties(baseUrl, apiKey, workspaceId, CollectionUtils.toMultiValueMap(connectionHeaders)); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public SpeechSynthesizer speechSynthesizer () { - return new SpeechSynthesizer(); - } - - @Bean - @ConditionalOnMissingBean - public Transcription transcription () { - return new Transcription(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeChatModel dashscopeChatModel (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { - - if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { - chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); - } - - var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, responseErrorHandler); - - return new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackResolver, retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeApi dashscopeChatApi (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); - - return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeEmbeddingModel dashscopeEmbeddingModel (DashScopeConnectionProperties commonProperties, DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { - - var dashScopeApi = dashscopeEmbeddingApi(commonProperties, embeddingProperties, restClientBuilder, webClientBuilder, responseErrorHandler); - - return new DashScopeEmbeddingModel(dashScopeApi, embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), retryTemplate); - } - - public DashScopeApi dashscopeEmbeddingApi (DashScopeConnectionProperties commonProperties, DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, "embedding"); - - return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi (DashScopeConnectionProperties commonProperties, DashScopeSpeechSynthesisProperties speechSynthesisProperties) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, "speechsynthesis"); - - return new DashScopeSpeechSynthesisApi(resolved.apiKey()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi (DashScopeConnectionProperties commonProperties, DashScopeAudioTranscriptionProperties audioTranscriptionProperties) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, audioTranscriptionProperties, "audiotranscription"); - - return new DashScopeAudioTranscriptionApi(resolved.apiKey()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeAgentApi dashscopeAgentApi (DashScopeConnectionProperties commonProperties, DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); - - return new DashScopeAgentApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - } - - @Bean - public RestClientCustomizer restClientCustomizer (DashScopeConnectionProperties commonProperties) { - return restClientBuilder -> restClientBuilder.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS.withReadTimeout(Duration.ofSeconds(commonProperties.getReadTimeout())))); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeImageModel dashScopeImageModel (DashScopeConnectionProperties commonProperties, DashScopeImageProperties imageProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); - - var dashScopeImageApi = new DashScopeImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - - var dashScopeImageModel = new DashScopeImageModel(dashScopeImageApi, imageProperties.getOptions(), retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(dashScopeImageModel::setObservationConvention); - - return dashScopeImageModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeRerankProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeRerankModel dashscopeRerankModel (DashScopeConnectionProperties commonProperties, DashScopeRerankProperties rerankProperties, RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, rerankProperties, "rerank"); - - var dashscopeApi = new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, webClientBuilder, responseErrorHandler); - - return new DashScopeRerankModel(dashscopeApi, rerankProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel (DashScopeConnectionProperties commonProperties, DashScopeSpeechSynthesisProperties speechSynthesisProperties, RetryTemplate retryTemplate) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, "speechsynthesis"); - - var dashScopeSpeechSynthesisApi = dashScopeSpeechSynthesisApi(commonProperties, speechSynthesisProperties); - - return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, speechSynthesisProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) - public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel (DashScopeConnectionProperties commonProperties, DashScopeAudioTranscriptionProperties audioTranscriptionProperties, RetryTemplate retryTemplate) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, audioTranscriptionProperties, "audiotranscription"); - - var dashScopeSpeechSynthesisApi = dashScopeAudioTranscriptionApi(commonProperties, audioTranscriptionProperties); - - return new DashScopeAudioTranscriptionModel(dashScopeSpeechSynthesisApi, audioTranscriptionProperties.getOptions(), retryTemplate); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager (ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - private record ResolvedConnectionProperties(String baseUrl, String apiKey, String workspaceId, - MultiValueMap headers) {} + private static @NotNull ResolvedConnectionProperties resolveConnectionProperties( + DashScopeParentProperties commonProperties, DashScopeParentProperties modelProperties, String modelType) { + + String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() + : commonProperties.getBaseUrl(); + String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() + : commonProperties.getApiKey(); + String workspaceId = StringUtils.hasText(modelProperties.getWorkspaceId()) ? modelProperties.getWorkspaceId() + : commonProperties.getWorkspaceId(); + + Map> connectionHeaders = new HashMap<>(); + if (StringUtils.hasText(workspaceId)) { + connectionHeaders.put("DashScope-Workspace", List.of(workspaceId)); + } + + // get apikey from system env. + if (Objects.isNull(apiKey)) { + if (Objects.nonNull(System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY))) { + apiKey = System.getenv(DashScopeApiConstants.DASHSCOPE_API_KEY); + } + } + + Assert.hasText(baseUrl, + "DashScope base URL must be set. Use the connection property: spring.ai.dashscope.base-url or spring.ai.dashscope." + + modelType + ".base-url property."); + Assert.hasText(apiKey, + "DashScope API key must be set. Use the connection property: spring.ai.dashscope.api-key or spring.ai.dashscope." + + modelType + ".api-key property."); + + return new ResolvedConnectionProperties(baseUrl, apiKey, workspaceId, + CollectionUtils.toMultiValueMap(connectionHeaders)); + } + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public SpeechSynthesizer speechSynthesizer() { + return new SpeechSynthesizer(); + } + + @Bean + @ConditionalOnMissingBean + public Transcription transcription() { + return new Transcription(); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeChatModel dashscopeChatModel(DashScopeConnectionProperties commonProperties, + DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, List toolFunctionCallbacks, + FunctionCallbackResolver functionCallbackResolver, RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler) { + + if (!CollectionUtils.isEmpty(toolFunctionCallbacks)) { + chatProperties.getOptions().getFunctionCallbacks().addAll(toolFunctionCallbacks); + } + + var dashscopeApi = dashscopeChatApi(commonProperties, chatProperties, restClientBuilder, webClientBuilder, + responseErrorHandler); + + return new DashScopeChatModel(dashscopeApi, chatProperties.getOptions(), functionCallbackResolver, + retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeApi dashscopeChatApi(DashScopeConnectionProperties commonProperties, + DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); + + return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, + webClientBuilder, responseErrorHandler); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeEmbeddingModel dashscopeEmbeddingModel(DashScopeConnectionProperties commonProperties, + DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler) { + + var dashScopeApi = dashscopeEmbeddingApi(commonProperties, embeddingProperties, restClientBuilder, + webClientBuilder, responseErrorHandler); + + return new DashScopeEmbeddingModel(dashScopeApi, embeddingProperties.getMetadataMode(), + embeddingProperties.getOptions(), retryTemplate); + } + + public DashScopeApi dashscopeEmbeddingApi(DashScopeConnectionProperties commonProperties, + DashScopeEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, + "embedding"); + + return new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, + webClientBuilder, responseErrorHandler); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) + public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi(DashScopeConnectionProperties commonProperties, + DashScopeSpeechSynthesisProperties speechSynthesisProperties) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, + "speechsynthesis"); + + return new DashScopeSpeechSynthesisApi(resolved.apiKey()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) + public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi(DashScopeConnectionProperties commonProperties, + DashScopeAudioTranscriptionProperties audioTranscriptionProperties) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, + audioTranscriptionProperties, "audiotranscription"); + + return new DashScopeAudioTranscriptionApi(resolved.apiKey()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeAgentApi dashscopeAgentApi(DashScopeConnectionProperties commonProperties, + DashScopeChatProperties chatProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, "chat"); + + return new DashScopeAgentApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), restClientBuilder, + webClientBuilder, responseErrorHandler); + } + + @Bean + public RestClientCustomizer restClientCustomizer(DashScopeConnectionProperties commonProperties) { + return restClientBuilder -> restClientBuilder + .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS + .withReadTimeout(Duration.ofSeconds(commonProperties.getReadTimeout())))); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeImageModel dashScopeImageModel(DashScopeConnectionProperties commonProperties, + DashScopeImageProperties imageProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, + ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); + + var dashScopeImageApi = new DashScopeImageApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), + restClientBuilder, webClientBuilder, responseErrorHandler); + + var dashScopeImageModel = new DashScopeImageModel(dashScopeImageApi, imageProperties.getOptions(), + retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(dashScopeImageModel::setObservationConvention); + + return dashScopeImageModel; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeRerankProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) + public DashScopeRerankModel dashscopeRerankModel(DashScopeConnectionProperties commonProperties, + DashScopeRerankProperties rerankProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, RetryTemplate retryTemplate, + ResponseErrorHandler responseErrorHandler) { + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, rerankProperties, + "rerank"); + + var dashscopeApi = new DashScopeApi(resolved.baseUrl(), resolved.apiKey(), resolved.workspaceId(), + restClientBuilder, webClientBuilder, responseErrorHandler); + + return new DashScopeRerankModel(dashscopeApi, rerankProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeSpeechSynthesisProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) + public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel(DashScopeConnectionProperties commonProperties, + DashScopeSpeechSynthesisProperties speechSynthesisProperties, RetryTemplate retryTemplate) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechSynthesisProperties, + "speechsynthesis"); + + var dashScopeSpeechSynthesisApi = dashScopeSpeechSynthesisApi(commonProperties, speechSynthesisProperties); + + return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, speechSynthesisProperties.getOptions(), + retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = DashScopeAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) + public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel( + DashScopeConnectionProperties commonProperties, + DashScopeAudioTranscriptionProperties audioTranscriptionProperties, RetryTemplate retryTemplate) { + + ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, + audioTranscriptionProperties, "audiotranscription"); + + var dashScopeSpeechSynthesisApi = dashScopeAudioTranscriptionApi(commonProperties, + audioTranscriptionProperties); + + return new DashScopeAudioTranscriptionModel(dashScopeSpeechSynthesisApi, + audioTranscriptionProperties.getOptions(), retryTemplate); + } + + @Bean + @ConditionalOnMissingBean + public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { + DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); + manager.setApplicationContext(context); + return manager; + } + + private record ResolvedConnectionProperties(String baseUrl, String apiKey, String workspaceId, + MultiValueMap headers) { + } } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/DashscopeAiTestConfiguration.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/DashscopeAiTestConfiguration.java index 507a10195..cdf5bffb5 100755 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/DashscopeAiTestConfiguration.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/DashscopeAiTestConfiguration.java @@ -45,106 +45,113 @@ @SpringBootConfiguration public class DashscopeAiTestConfiguration { - @Bean - public DashScopeImageApi dashscopeImageApi () { - return newDashScopeImageApi(getApiKey()); - } - - @Bean - public DashScopeApi dashscopeApi () { - return newDashScopeApi(getApiKey()); - } - - @Bean - public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi () { - return newDashScopeSpeechSynthesisApi(getApiKey()); - } - - @Bean - public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi () { - return newDashScopeAudioTranscriptionApi(getApiKey()); - } - - @Bean - public DashScopeApi dashscopeChatApi () { - return newDashScopeChatApi(getApiKey()); - } - - private DashScopeApi newDashScopeChatApi (String apiKey) { - return new DashScopeApi(DEFAULT_BASE_URL, apiKey, ""); - } - - private DashScopeApi newDashScopeApi (String apiKey) { - return new DashScopeApi(apiKey); - } - - private DashScopeSpeechSynthesisApi newDashScopeSpeechSynthesisApi (String apiKey) { - return new DashScopeSpeechSynthesisApi(apiKey); - } - - private DashScopeAudioTranscriptionApi newDashScopeAudioTranscriptionApi (String apiKey) { - return new DashScopeAudioTranscriptionApi(apiKey); - } - - private DashScopeImageApi newDashScopeImageApi (String apiKey) { - return new DashScopeImageApi(apiKey); - } - - private String getApiKey () { - String apiKey = System.getenv("DASHSCOPE_API_KEY"); - if (!StringUtils.hasText(apiKey)) { - throw new IllegalArgumentException("You must provide an API key. Put it in an environment variable under the name DASHSCOPE_API_KEY"); - } - return apiKey; - } - - @Bean - public ChatModel dashscopeChatModel (DashScopeApi dashscopeChatApi, TestObservationRegistry observationRegistry) { - return new DashScopeChatModel(dashscopeChatApi, DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL).build(), null, RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); - } - - @Bean - public EmbeddingModel dashscopeEmbeddingModel (DashScopeApi dashscopeApi) { - return new DashScopeEmbeddingModel(dashscopeApi); - } - - @Bean - public DashScopeImageModel dashscopeImageModel (DashScopeImageApi dashscopeImageApi, TestObservationRegistry observationRegistry) { - return new DashScopeImageModel(dashscopeImageApi, observationRegistry); - } - - @Bean - public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel (DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi) { - return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, DashScopeSpeechSynthesisOptions.builder().withModel("cosyvoice-v1").withVoice("longhua").build()); - } - - @Bean - public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel (DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi) { - return new DashScopeAudioTranscriptionModel(dashScopeAudioTranscriptionApi); - } - - @Bean - public TestObservationRegistry observationRegistry () { - return TestObservationRegistry.create(); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public SpeechSynthesizer speechSynthesizer () { - return new SpeechSynthesizer(); - } - - @Bean - @ConditionalOnMissingBean - public Transcription transcription () { - return new Transcription(); - } - - @Bean - @ConditionalOnMissingBean - public RerankModel dashscopeRerankModel (DashScopeApi dashscopeApi) { - return new DashScopeRerankModel(dashscopeApi); - } + @Bean + public DashScopeImageApi dashscopeImageApi() { + return newDashScopeImageApi(getApiKey()); + } + + @Bean + public DashScopeApi dashscopeApi() { + return newDashScopeApi(getApiKey()); + } + + @Bean + public DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi() { + return newDashScopeSpeechSynthesisApi(getApiKey()); + } + + @Bean + public DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi() { + return newDashScopeAudioTranscriptionApi(getApiKey()); + } + + @Bean + public DashScopeApi dashscopeChatApi() { + return newDashScopeChatApi(getApiKey()); + } + + private DashScopeApi newDashScopeChatApi(String apiKey) { + return new DashScopeApi(DEFAULT_BASE_URL, apiKey, ""); + } + + private DashScopeApi newDashScopeApi(String apiKey) { + return new DashScopeApi(apiKey); + } + + private DashScopeSpeechSynthesisApi newDashScopeSpeechSynthesisApi(String apiKey) { + return new DashScopeSpeechSynthesisApi(apiKey); + } + + private DashScopeAudioTranscriptionApi newDashScopeAudioTranscriptionApi(String apiKey) { + return new DashScopeAudioTranscriptionApi(apiKey); + } + + private DashScopeImageApi newDashScopeImageApi(String apiKey) { + return new DashScopeImageApi(apiKey); + } + + private String getApiKey() { + String apiKey = System.getenv("DASHSCOPE_API_KEY"); + if (!StringUtils.hasText(apiKey)) { + throw new IllegalArgumentException( + "You must provide an API key. Put it in an environment variable under the name DASHSCOPE_API_KEY"); + } + return apiKey; + } + + @Bean + public ChatModel dashscopeChatModel(DashScopeApi dashscopeChatApi, TestObservationRegistry observationRegistry) { + return new DashScopeChatModel(dashscopeChatApi, + DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL).build(), null, + RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); + } + + @Bean + public EmbeddingModel dashscopeEmbeddingModel(DashScopeApi dashscopeApi) { + return new DashScopeEmbeddingModel(dashscopeApi); + } + + @Bean + public DashScopeImageModel dashscopeImageModel(DashScopeImageApi dashscopeImageApi, + TestObservationRegistry observationRegistry) { + return new DashScopeImageModel(dashscopeImageApi, observationRegistry); + } + + @Bean + public DashScopeSpeechSynthesisModel dashScopeSpeechSynthesisModel( + DashScopeSpeechSynthesisApi dashScopeSpeechSynthesisApi) { + return new DashScopeSpeechSynthesisModel(dashScopeSpeechSynthesisApi, + DashScopeSpeechSynthesisOptions.builder().withModel("cosyvoice-v1").withVoice("longhua").build()); + } + + @Bean + public DashScopeAudioTranscriptionModel dashScopeAudioTranscriptionModel( + DashScopeAudioTranscriptionApi dashScopeAudioTranscriptionApi) { + return new DashScopeAudioTranscriptionModel(dashScopeAudioTranscriptionApi); + } + + @Bean + public TestObservationRegistry observationRegistry() { + return TestObservationRegistry.create(); + } + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public SpeechSynthesizer speechSynthesizer() { + return new SpeechSynthesizer(); + } + + @Bean + @ConditionalOnMissingBean + public Transcription transcription() { + return new Transcription(); + } + + @Bean + @ConditionalOnMissingBean + public RerankModel dashscopeRerankModel(DashScopeApi dashscopeApi) { + return new DashScopeRerankModel(dashscopeApi); + } } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java index 3395a3e50..2026140ab 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java @@ -38,74 +38,78 @@ @EnabledIfEnvironmentVariable(named = "DASHSCOPE_HTTP_BASE_URL", matches = ".+") public class DashScopeImageModelIT { - @Autowired - protected ImageModel imageModel; + @Autowired + protected ImageModel imageModel; - @Autowired - TestObservationRegistry observationRegistry; + @Autowired + TestObservationRegistry observationRegistry; - @Test - void imageModelObservationTest () { + @Test + void imageModelObservationTest() { - var options = ImageOptionsBuilder.builder() - .model("wanx2.1-t2i-turbo") - .withHeight(1024) - .withWidth(1024) - .N(1) - .build(); + var options = ImageOptionsBuilder.builder() + .model("wanx2.1-t2i-turbo") + .withHeight(1024) + .withWidth(1024) + .N(1) + .build(); - var instructions = """ - A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; + var instructions = """ + A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; - ImagePrompt imagePrompt = new ImagePrompt(instructions, options); + ImagePrompt imagePrompt = new ImagePrompt(instructions, options); - ImageResponse imageResponse = imageModel.call(imagePrompt); + ImageResponse imageResponse = imageModel.call(imagePrompt); - assertThat(imageResponse.getResults()).hasSize(1); + assertThat(imageResponse.getResults()).hasSize(1); - ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); - assertThat(imageResponseMetadata.getCreated()).isPositive(); + ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); + assertThat(imageResponseMetadata.getCreated()).isPositive(); - var generation = imageResponse.getResult(); - Image image = generation.getOutput(); - assertThat(image.getUrl()).isNotEmpty(); + var generation = imageResponse.getResult(); + Image image = generation.getOutput(); + assertThat(image.getUrl()).isNotEmpty(); - TestObservationRegistryAssert.assertThat(this.observationRegistry) - .doesNotHaveAnyRemainingCurrentObservation() - .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) - .that().hasContextualNameEqualTo("image " + "wanx2.1-t2i-turbo") - .hasHighCardinalityKeyValue(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(),"1024x1024") - .hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), - AiProvider.DASHSCOPE.value()) - .hasLowCardinalityKeyValue( - ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), - AiOperationType.IMAGE.value()); - } + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .doesNotHaveAnyRemainingCurrentObservation() + .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) + .that() + .hasContextualNameEqualTo("image " + "wanx2.1-t2i-turbo") + .hasHighCardinalityKeyValue( + ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), + "1024x1024") + .hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), + AiProvider.DASHSCOPE.value()) + .hasLowCardinalityKeyValue( + ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), + AiOperationType.IMAGE.value()); + } - @Test - void imageAsUrlTest () { - var options = ImageOptionsBuilder.builder() - .model("wanx2.1-t2i-turbo") - .withHeight(1024) - .withWidth(1024) - .N(1) - .build(); + @Test + void imageAsUrlTest() { + var options = ImageOptionsBuilder.builder() + .model("wanx2.1-t2i-turbo") + .withHeight(1024) + .withWidth(1024) + .N(1) + .build(); - var instructions = """ - A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; + var instructions = """ + A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; - ImagePrompt imagePrompt = new ImagePrompt(instructions, options); + ImagePrompt imagePrompt = new ImagePrompt(instructions, options); - ImageResponse imageResponse = imageModel.call(imagePrompt); + ImageResponse imageResponse = imageModel.call(imagePrompt); - assertThat(imageResponse.getResults()).hasSize(1); + assertThat(imageResponse.getResults()).hasSize(1); - ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); - assertThat(imageResponseMetadata.getCreated()).isPositive(); + ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); + assertThat(imageResponseMetadata.getCreated()).isPositive(); + + var generation = imageResponse.getResult(); + Image image = generation.getOutput(); + assertThat(image.getUrl()).isNotEmpty(); + assertThat(image.getB64Json()).isNotEmpty(); + } - var generation = imageResponse.getResult(); - Image image = generation.getOutput(); - assertThat(image.getUrl()).isNotEmpty(); - assertThat(image.getB64Json()).isNotEmpty(); - } } From 70f763dd02f1ef30d1a9e070e5d7c2a016c7b2be Mon Sep 17 00:00:00 2001 From: PolarishT Date: Wed, 15 Jan 2025 00:48:05 +0800 Subject: [PATCH 6/9] chore: chore retry logic Signed-off-by: PolarishT --- .../dashscope/image/DashScopeImageModel.java | 392 ++++++++---------- .../image/DashScopeImageModelIT.java | 44 -- .../DashScopeImageModelObservationIT.java | 57 +++ 3 files changed, 237 insertions(+), 256 deletions(-) create mode 100644 spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java index de6edb4af..ec35cccde 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java @@ -17,7 +17,6 @@ import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants; -import com.alibaba.cloud.ai.dashscope.common.DashScopeException; import com.alibaba.cloud.ai.dashscope.image.observation.DashScopeImageModelObservationConvention; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; @@ -29,6 +28,7 @@ import org.springframework.ai.image.observation.ImageModelObservationDocumentation; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.retry.RetryUtils; +import org.springframework.ai.retry.TransientAiException; import org.springframework.http.ResponseEntity; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -46,216 +46,184 @@ */ public class DashScopeImageModel implements ImageModel { - private static final Logger logger = LoggerFactory.getLogger(DashScopeImageModel.class); - - private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DashScopeImageModelObservationConvention(); - - /** - * The default model used for the image completion requests. - */ - private static final String DEFAULT_MODEL = DashScopeImageApi.DEFAULT_IMAGE_MODEL; - - /** - * Low-level access to the DashScope Image API. - */ - private final DashScopeImageApi dashScopeImageApi; - - /** - * Observation registry used for instrumentation. - */ - private final ObservationRegistry observationRegistry; - - /** - * The default options used for the image completion requests. - */ - private final DashScopeImageOptions defaultOptions; - - /** - * The retry template used to retry the OpenAI Image API calls. - */ - private final RetryTemplate retryTemplate; - - /** - * Conventions to use for generating observations. - */ - private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; - - public DashScopeImageModel(DashScopeImageApi dashScopeImageApi) { - - this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; - this.observationRegistry = ObservationRegistry.NOOP; - } - - public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; - this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; - } - - public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry, - DashScopeImageOptions options, RetryTemplate retryTemplate) { - this.observationRegistry = observationRegistry; - this.defaultOptions = options; - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = retryTemplate; - } - - public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options, - RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { - - Assert.notNull(dashScopeImageApi, "DashScopeImageApi must not be null"); - Assert.notNull(options, "options must not be null"); - Assert.notNull(retryTemplate, "retryTemplate must not be null"); - - this.dashScopeImageApi = dashScopeImageApi; - this.defaultOptions = options; - this.retryTemplate = retryTemplate; - this.observationRegistry = observationRegistry; - } - - @Override - public ImageResponse call(ImagePrompt prompt) { - - String taskId = submitImageGenTask(prompt); - if (taskId == null) { - return new ImageResponse(List.of()); - } - - ImageModelObservationContext observationContext = ImageModelObservationContext.builder() - .imagePrompt(prompt) - .provider(DashScopeApiConstants.PROVIDER_NAME) - .requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions) - .build(); - - Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation( - this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, - this.observationRegistry); - - return observation.observe(() -> { - ImageResponse imageResponse = null; - try { - imageResponse = this.retryTemplate.execute(ctx -> { - - DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); - - int maxAttempts = 3; - int attempt = 0; - - while (attempt < maxAttempts) { - if (!isTaskCompleted(getResultResponse)) { - getResultResponse = getImageGenTask(taskId); - Thread.sleep(10000L); - attempt++; - } - else - break; - } - - DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse - .output(); - logger.info("current imageModel generated result --> {}", getResultResponse.output()); - - return !CollectionUtils.isEmpty(getResultResponse.output().results()) ? toImageResponse(output) - : new ImageResponse(List.of()); - }); - } - catch (InterruptedException e) { - throw new DashScopeException("Error while waiting for image generation task to complete", e); - } - observationContext.setResponse(imageResponse); - return imageResponse; - }); - } - - /** - * Merge Image options. Notice: Programmatically set options parameters take - * precedence - */ - private DashScopeImageOptions toImageOptions(ImageOptions runtimeOptions) { - - // set default image model - var currentOptions = DashScopeImageOptions.builder().withModel(DEFAULT_MODEL).build(); - - if (Objects.nonNull(runtimeOptions)) { - currentOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, - DashScopeImageOptions.class); - } - - currentOptions = ModelOptionsUtils.merge(currentOptions, this.defaultOptions, DashScopeImageOptions.class); - - return currentOptions; - } - - private ImageResponse toImageResponse( - DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output) { - List genImageList = output - .results(); - if (genImageList == null || genImageList.isEmpty()) { - return new ImageResponse(List.of()); - } - List imageGenerationList = genImageList.stream() - .map(entry -> new ImageGeneration( - new Image(entry.url(), Base64.getEncoder().encodeToString(entry.url().getBytes())))) - .toList(); - - return new ImageResponse(imageGenerationList); - } - - private DashScopeImageApi.DashScopeImageRequest constructImageRequest(ImagePrompt imagePrompt, - DashScopeImageOptions options) { - - return new DashScopeImageApi.DashScopeImageRequest(options.getModel(), - new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestInput( - imagePrompt.getInstructions().get(0).getText(), options.getNegativePrompt(), - options.getRefImg()), - new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestParameter(options.getStyle(), - options.getSize(), options.getN(), options.getSeed(), options.getRefStrength(), - options.getRefMode())); - } - - /** - * Use the provided convention for reporting observation data - * @param observationConvention The provided convention - */ - public void setObservationConvention(ImageModelObservationConvention observationConvention) { - Assert.notNull(observationConvention, "observationConvention cannot be null"); - this.observationConvention = observationConvention; - } - - public String submitImageGenTask(ImagePrompt request) { - - DashScopeImageOptions imageOptions = toImageOptions(request.getOptions()); - logger.debug("Image options: {}", imageOptions); - - DashScopeImageApi.DashScopeImageRequest dashScopeImageRequest = constructImageRequest(request, imageOptions); - - ResponseEntity submitResponse = dashScopeImageApi - .submitImageGenTask(dashScopeImageRequest); - - if (submitResponse == null || submitResponse.getBody() == null) { - logger.warn("Submit imageGen error,request: {}", request); - return null; - } - - return submitResponse.getBody().output().taskId(); - } - - public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask(String taskId) { - ResponseEntity getImageGenResponse = dashScopeImageApi - .getImageGenTaskResult(taskId); - if (getImageGenResponse == null || getImageGenResponse.getBody() == null) { - logger.warn("No image response returned for taskId: {}", taskId); - return null; - } - return getImageGenResponse.getBody(); - } - - private boolean isTaskCompleted(DashScopeImageApi.DashScopeImageAsyncReponse response) { - List checkedTaskStatus = List.of("SUCCEEDED", "FAILED"); - return checkedTaskStatus.contains(response.output().taskStatus()); - } + private static final Logger logger = LoggerFactory.getLogger(DashScopeImageModel.class); + private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DashScopeImageModelObservationConvention(); + + /** + * The default model used for the image completion requests. + */ + private static final String DEFAULT_MODEL = DashScopeImageApi.DEFAULT_IMAGE_MODEL; + + /** + * Low-level access to the DashScope Image API. + */ + private final DashScopeImageApi dashScopeImageApi; + + /** + * Observation registry used for instrumentation. + */ + private final ObservationRegistry observationRegistry; + + /** + * The default options used for the image completion requests. + */ + private final DashScopeImageOptions defaultOptions; + + /** + * The retry template used to retry the OpenAI Image API calls. + */ + private final RetryTemplate retryTemplate; + + /** + * Conventions to use for generating observations. + */ + private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; + + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi) { + + this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; + this.observationRegistry = ObservationRegistry.NOOP; + } + + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; + } + + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry, DashScopeImageOptions options, RetryTemplate retryTemplate) { + this.observationRegistry = observationRegistry; + this.defaultOptions = options; + this.dashScopeImageApi = dashScopeImageApi; + this.retryTemplate = retryTemplate; + } + + public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options, RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { + + Assert.notNull(dashScopeImageApi, "DashScopeImageApi must not be null"); + Assert.notNull(options, "options must not be null"); + Assert.notNull(retryTemplate, "retryTemplate must not be null"); + + this.dashScopeImageApi = dashScopeImageApi; + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + this.observationRegistry = observationRegistry; + } + + @Override + public ImageResponse call (ImagePrompt prompt) { + + String taskId = submitImageGenTask(prompt); + if (taskId == null) { + return new ImageResponse(List.of()); + } + + ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(prompt).provider(DashScopeApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions).build(); + + Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry); + + return observation.observe(() -> { + ImageResponse imageResponse = new ImageResponse(List.of()); + + imageResponse = this.retryTemplate.execute(ctx -> { + + DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); + + if (!isTaskCompleted(getResultResponse)) { + throw new TransientAiException("Image generation task is not completed yet, taskId: " + taskId); + } + + getResultResponse = getImageGenTask(taskId); + + DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse.output(); + logger.warn("current imageModel generated result --> {}", getResultResponse.output()); + + return !CollectionUtils.isEmpty(getResultResponse.output().results()) ? toImageResponse(output) : new ImageResponse(List.of()); + }, (ctx) -> { + logger.error("Image generation failed after {} retries", ctx.getRetryCount()); + return new ImageResponse(List.of()); + }); + + observationContext.setResponse(imageResponse); + return imageResponse; + }); + } + + /** + * Merge Image options. Notice: Programmatically set options parameters take + * precedence + */ + private DashScopeImageOptions toImageOptions (ImageOptions runtimeOptions) { + + // set default image model + var currentOptions = DashScopeImageOptions.builder().withModel(DEFAULT_MODEL).build(); + + if (Objects.nonNull(runtimeOptions)) { + currentOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, DashScopeImageOptions.class); + } + + currentOptions = ModelOptionsUtils.merge(currentOptions, this.defaultOptions, DashScopeImageOptions.class); + + return currentOptions; + } + + private ImageResponse toImageResponse (DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output) { + List genImageList = output.results(); + if (genImageList == null || genImageList.isEmpty()) { + return new ImageResponse(List.of()); + } + List imageGenerationList = genImageList.stream().map(entry -> new ImageGeneration(new Image(entry.url(), Base64.getEncoder().encodeToString(entry.url().getBytes())))).toList(); + + return new ImageResponse(imageGenerationList); + } + + private DashScopeImageApi.DashScopeImageRequest constructImageRequest (ImagePrompt imagePrompt, DashScopeImageOptions options) { + + return new DashScopeImageApi.DashScopeImageRequest(options.getModel(), new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestInput(imagePrompt.getInstructions().get(0).getText(), options.getNegativePrompt(), options.getRefImg()), new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestParameter(options.getStyle(), options.getSize(), options.getN(), options.getSeed(), options.getRefStrength(), options.getRefMode())); + } + + /** + * Use the provided convention for reporting observation data + * + * @param observationConvention The provided convention + */ + public void setObservationConvention (ImageModelObservationConvention observationConvention) { + Assert.notNull(observationConvention, "observationConvention cannot be null"); + this.observationConvention = observationConvention; + } + + public String submitImageGenTask (ImagePrompt request) { + + DashScopeImageOptions imageOptions = toImageOptions(request.getOptions()); + logger.debug("Image options: {}", imageOptions); + + DashScopeImageApi.DashScopeImageRequest dashScopeImageRequest = constructImageRequest(request, imageOptions); + + ResponseEntity submitResponse = dashScopeImageApi.submitImageGenTask(dashScopeImageRequest); + + if (submitResponse == null || submitResponse.getBody() == null) { + logger.warn("Submit imageGen error,request: {}", request); + return null; + } + + return submitResponse.getBody().output().taskId(); + } + + public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask (String taskId) { + ResponseEntity getImageGenResponse = dashScopeImageApi.getImageGenTaskResult(taskId); + if (getImageGenResponse == null || getImageGenResponse.getBody() == null) { + logger.warn("No image response returned for taskId: {}", taskId); + return null; + } + return getImageGenResponse.getBody(); + } + + private boolean isTaskCompleted (DashScopeImageApi.DashScopeImageAsyncReponse response) { + List checkedTaskStatus = List.of("SUCCEEDED", "FAILED"); + return checkedTaskStatus.contains(response.output().taskStatus()); + } } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java index 2026140ab..1f74d4367 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelIT.java @@ -41,50 +41,6 @@ public class DashScopeImageModelIT { @Autowired protected ImageModel imageModel; - @Autowired - TestObservationRegistry observationRegistry; - - @Test - void imageModelObservationTest() { - - var options = ImageOptionsBuilder.builder() - .model("wanx2.1-t2i-turbo") - .withHeight(1024) - .withWidth(1024) - .N(1) - .build(); - - var instructions = """ - A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; - - ImagePrompt imagePrompt = new ImagePrompt(instructions, options); - - ImageResponse imageResponse = imageModel.call(imagePrompt); - - assertThat(imageResponse.getResults()).hasSize(1); - - ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); - assertThat(imageResponseMetadata.getCreated()).isPositive(); - - var generation = imageResponse.getResult(); - Image image = generation.getOutput(); - assertThat(image.getUrl()).isNotEmpty(); - - TestObservationRegistryAssert.assertThat(this.observationRegistry) - .doesNotHaveAnyRemainingCurrentObservation() - .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) - .that() - .hasContextualNameEqualTo("image " + "wanx2.1-t2i-turbo") - .hasHighCardinalityKeyValue( - ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), - "1024x1024") - .hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), - AiProvider.DASHSCOPE.value()) - .hasLowCardinalityKeyValue( - ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), - AiOperationType.IMAGE.value()); - } - @Test void imageAsUrlTest() { var options = ImageOptionsBuilder.builder() diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java new file mode 100644 index 000000000..24d896546 --- /dev/null +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java @@ -0,0 +1,57 @@ +package com.alibaba.cloud.ai.dashscope.image; + +import com.alibaba.cloud.ai.dashscope.DashscopeAiTestConfiguration; +import com.alibaba.cloud.ai.dashscope.observation.conventions.AiProvider; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.ai.image.*; +import org.springframework.ai.image.observation.DefaultImageModelObservationConvention; +import org.springframework.ai.image.observation.ImageModelObservationDocumentation; +import org.springframework.ai.observation.conventions.AiOperationType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = DashscopeAiTestConfiguration.class) +@EnabledIfEnvironmentVariable(named = "AI_DASHSCOPE_API_KEY", matches = ".+") +/** + * @author 北极星 + */ public class DashScopeImageModelObservationIT { + + @Autowired + ImageModel imageModel; + + @Autowired + TestObservationRegistry observationRegistry; + + @Test + void imageModelObservationTest () { + + var options = ImageOptionsBuilder.builder().model("wanx2.1-t2i-turbo") + .withHeight(1024) + .withWidth(1024) + .N(1) + .build(); + + var instructions = """ + A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; + + ImagePrompt imagePrompt = new ImagePrompt(instructions, options); + + ImageResponse imageResponse = imageModel.call(imagePrompt); + + assertThat(imageResponse.getResults()).hasSize(1); + + ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); + assertThat(imageResponseMetadata.getCreated()).isPositive(); + + var generation = imageResponse.getResult(); + Image image = generation.getOutput(); + assertThat(image.getUrl()).isNotEmpty(); + + TestObservationRegistryAssert.assertThat(this.observationRegistry).doesNotHaveAnyRemainingCurrentObservation().hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME).that().hasContextualNameEqualTo("image " + "wanx2.1-t2i-turbo").hasHighCardinalityKeyValue(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), "1024x1024").hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.DASHSCOPE.value()).hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), AiOperationType.IMAGE.value()); + } +} From b5a3bc24854bd96e7a9ba3771adeaea902453d7f Mon Sep 17 00:00:00 2001 From: PolarishT Date: Wed, 15 Jan 2025 19:16:37 +0800 Subject: [PATCH 7/9] fix probs Signed-off-by: PolarishT --- .../ai/dashscope/api/DashScopeImageApi.java | 1 - .../dashscope/image/DashScopeImageModel.java | 383 ++++++++++-------- .../DashScopeImageModelObservationIT.java | 63 +-- 3 files changed, 242 insertions(+), 205 deletions(-) diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java index 33e8a7706..eae000339 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/api/DashScopeImageApi.java @@ -33,7 +33,6 @@ */ public class DashScopeImageApi { - // public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); public static final String DEFAULT_IMAGE_MODEL = ImageModel.WANX_V1.getValue(); private final RestClient restClient; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java index ec35cccde..b90e64dd7 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModel.java @@ -17,12 +17,12 @@ import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.common.DashScopeApiConstants; -import com.alibaba.cloud.ai.dashscope.image.observation.DashScopeImageModelObservationConvention; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.image.*; +import org.springframework.ai.image.observation.DefaultImageModelObservationConvention; import org.springframework.ai.image.observation.ImageModelObservationContext; import org.springframework.ai.image.observation.ImageModelObservationConvention; import org.springframework.ai.image.observation.ImageModelObservationDocumentation; @@ -46,184 +46,207 @@ */ public class DashScopeImageModel implements ImageModel { - private static final Logger logger = LoggerFactory.getLogger(DashScopeImageModel.class); + private static final Logger logger = LoggerFactory.getLogger(DashScopeImageModel.class); + + /** + * The default model used for the image completion requests. + */ + private static final String DEFAULT_MODEL = DashScopeImageApi.DEFAULT_IMAGE_MODEL; + + /** + * Low-level access to the DashScope Image API. + */ + private final DashScopeImageApi dashScopeImageApi; + + /** + * Observation registry used for instrumentation. + */ + private final ObservationRegistry observationRegistry; + + /** + * The default options used for the image completion requests. + */ + private final DashScopeImageOptions defaultOptions; + + /** + * The retry template used to retry the OpenAI Image API calls. + */ + private final RetryTemplate retryTemplate; + + /** + * Conventions to use for generating observations. + */ + private ImageModelObservationConvention observationConvention = new DefaultImageModelObservationConvention(); + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi) { + this(dashScopeImageApi, + DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE, ObservationRegistry.NOOP); + } + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options) { + this(dashScopeImageApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE, ObservationRegistry.NOOP); + } + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry) { + this(dashScopeImageApi, + DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(), + RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); + } + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry, + DashScopeImageOptions options) { + this(dashScopeImageApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE, observationRegistry); + } + + public DashScopeImageModel(DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options, + RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { + + Assert.notNull(dashScopeImageApi, "DashScopeImageApi must not be null"); + Assert.notNull(options, "options must not be null"); + Assert.notNull(retryTemplate, "retryTemplate must not be null"); + Assert.notNull(observationRegistry, "observationRegistry must not be null"); + + this.dashScopeImageApi = dashScopeImageApi; + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + this.observationRegistry = observationRegistry; + } + + @Override + public ImageResponse call(ImagePrompt prompt) { + + String taskId = submitImageGenTask(prompt); + if (taskId == null) { + return new ImageResponse(List.of()); + } + + ImageModelObservationContext observationContext = ImageModelObservationContext.builder() + .imagePrompt(prompt) + .provider(DashScopeApiConstants.PROVIDER_NAME) + .requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions) + .build(); + + Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation( + observationConvention, new DefaultImageModelObservationConvention(), () -> observationContext, + this.observationRegistry); + + return observation.observe(() -> { + ImageResponse imageResponse = new ImageResponse(List.of()); + + imageResponse = this.retryTemplate.execute(ctx -> { + + DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); + + if (!isTaskCompleted(getResultResponse)) { + logger.warn("Image generation task is not completed yet, taskId: {}", taskId); + throw new TransientAiException("Image generation task is not completed yet, taskId: " + taskId); + } + + getResultResponse = getImageGenTask(taskId); + + DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse + .output(); + logger.info("current imageModel generated result --> {}", getResultResponse.output()); + + return !CollectionUtils.isEmpty(getResultResponse.output().results()) ? toImageResponse(output) + : new ImageResponse(List.of()); + }, (ctx) -> { + logger.error("Image generation failed after {} retries", ctx.getRetryCount()); + return new ImageResponse(List.of()); + }); + + observationContext.setResponse(imageResponse); + return imageResponse; + }); + } + + /** + * Merge Image options. Notice: Programmatically set options parameters take + * precedence + */ + private DashScopeImageOptions toImageOptions(ImageOptions runtimeOptions) { + + // set default image model + var currentOptions = DashScopeImageOptions.builder().withModel(DEFAULT_MODEL).build(); + + if (Objects.nonNull(runtimeOptions)) { + currentOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, + DashScopeImageOptions.class); + } + + currentOptions = ModelOptionsUtils.merge(currentOptions, this.defaultOptions, DashScopeImageOptions.class); + + return currentOptions; + } + + private ImageResponse toImageResponse( + DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output) { + List genImageList = output + .results(); + if (genImageList == null || genImageList.isEmpty()) { + return new ImageResponse(List.of()); + } + List imageGenerationList = genImageList.stream() + .map(entry -> new ImageGeneration( + new Image(entry.url(), Base64.getEncoder().encodeToString(entry.url().getBytes())))) + .toList(); + + return new ImageResponse(imageGenerationList); + } + + private DashScopeImageApi.DashScopeImageRequest constructImageRequest(ImagePrompt imagePrompt, + DashScopeImageOptions options) { + + return new DashScopeImageApi.DashScopeImageRequest(options.getModel(), + new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestInput( + imagePrompt.getInstructions().get(0).getText(), options.getNegativePrompt(), + options.getRefImg()), + new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestParameter(options.getStyle(), + options.getSize(), options.getN(), options.getSeed(), options.getRefStrength(), + options.getRefMode())); + } + + /** + * Use the provided convention for reporting observation data + * @param observationConvention The provided convention + */ + public void setObservationConvention(ImageModelObservationConvention observationConvention) { + Assert.notNull(observationConvention, "observationConvention cannot be null"); + this.observationConvention = observationConvention; + } + + public String submitImageGenTask(ImagePrompt request) { + + DashScopeImageOptions imageOptions = toImageOptions(request.getOptions()); + logger.debug("Image options: {}", imageOptions); + + DashScopeImageApi.DashScopeImageRequest dashScopeImageRequest = constructImageRequest(request, imageOptions); + + ResponseEntity submitResponse = dashScopeImageApi + .submitImageGenTask(dashScopeImageRequest); + + if (submitResponse == null || submitResponse.getBody() == null) { + logger.warn("Submit imageGen error,request: {}", request); + return null; + } + + return submitResponse.getBody().output().taskId(); + } + + public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask(String taskId) { + ResponseEntity getImageGenResponse = dashScopeImageApi + .getImageGenTaskResult(taskId); + if (getImageGenResponse == null || getImageGenResponse.getBody() == null) { + logger.warn("No image response returned for taskId: {}", taskId); + return null; + } + return getImageGenResponse.getBody(); + } + + private boolean isTaskCompleted(DashScopeImageApi.DashScopeImageAsyncReponse response) { + List checkedTaskStatus = List.of("SUCCEEDED", "FAILED"); + return checkedTaskStatus.contains(response.output().taskStatus()); + } - private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DashScopeImageModelObservationConvention(); - - /** - * The default model used for the image completion requests. - */ - private static final String DEFAULT_MODEL = DashScopeImageApi.DEFAULT_IMAGE_MODEL; - - /** - * Low-level access to the DashScope Image API. - */ - private final DashScopeImageApi dashScopeImageApi; - - /** - * Observation registry used for instrumentation. - */ - private final ObservationRegistry observationRegistry; - - /** - * The default options used for the image completion requests. - */ - private final DashScopeImageOptions defaultOptions; - - /** - * The retry template used to retry the OpenAI Image API calls. - */ - private final RetryTemplate retryTemplate; - - /** - * Conventions to use for generating observations. - */ - private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; - - public DashScopeImageModel (DashScopeImageApi dashScopeImageApi) { - - this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; - this.observationRegistry = ObservationRegistry.NOOP; - } - - public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry) { - this.observationRegistry = observationRegistry; - this.defaultOptions = DashScopeImageOptions.builder().withModel(DashScopeImageApi.DEFAULT_IMAGE_MODEL).build(); - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = RetryUtils.DEFAULT_RETRY_TEMPLATE; - } - - public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, ObservationRegistry observationRegistry, DashScopeImageOptions options, RetryTemplate retryTemplate) { - this.observationRegistry = observationRegistry; - this.defaultOptions = options; - this.dashScopeImageApi = dashScopeImageApi; - this.retryTemplate = retryTemplate; - } - - public DashScopeImageModel (DashScopeImageApi dashScopeImageApi, DashScopeImageOptions options, RetryTemplate retryTemplate, ObservationRegistry observationRegistry) { - - Assert.notNull(dashScopeImageApi, "DashScopeImageApi must not be null"); - Assert.notNull(options, "options must not be null"); - Assert.notNull(retryTemplate, "retryTemplate must not be null"); - - this.dashScopeImageApi = dashScopeImageApi; - this.defaultOptions = options; - this.retryTemplate = retryTemplate; - this.observationRegistry = observationRegistry; - } - - @Override - public ImageResponse call (ImagePrompt prompt) { - - String taskId = submitImageGenTask(prompt); - if (taskId == null) { - return new ImageResponse(List.of()); - } - - ImageModelObservationContext observationContext = ImageModelObservationContext.builder().imagePrompt(prompt).provider(DashScopeApiConstants.PROVIDER_NAME).requestOptions(prompt.getOptions() != null ? prompt.getOptions() : this.defaultOptions).build(); - - Observation observation = ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry); - - return observation.observe(() -> { - ImageResponse imageResponse = new ImageResponse(List.of()); - - imageResponse = this.retryTemplate.execute(ctx -> { - - DashScopeImageApi.DashScopeImageAsyncReponse getResultResponse = getImageGenTask(taskId); - - if (!isTaskCompleted(getResultResponse)) { - throw new TransientAiException("Image generation task is not completed yet, taskId: " + taskId); - } - - getResultResponse = getImageGenTask(taskId); - - DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output = getResultResponse.output(); - logger.warn("current imageModel generated result --> {}", getResultResponse.output()); - - return !CollectionUtils.isEmpty(getResultResponse.output().results()) ? toImageResponse(output) : new ImageResponse(List.of()); - }, (ctx) -> { - logger.error("Image generation failed after {} retries", ctx.getRetryCount()); - return new ImageResponse(List.of()); - }); - - observationContext.setResponse(imageResponse); - return imageResponse; - }); - } - - /** - * Merge Image options. Notice: Programmatically set options parameters take - * precedence - */ - private DashScopeImageOptions toImageOptions (ImageOptions runtimeOptions) { - - // set default image model - var currentOptions = DashScopeImageOptions.builder().withModel(DEFAULT_MODEL).build(); - - if (Objects.nonNull(runtimeOptions)) { - currentOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class, DashScopeImageOptions.class); - } - - currentOptions = ModelOptionsUtils.merge(currentOptions, this.defaultOptions, DashScopeImageOptions.class); - - return currentOptions; - } - - private ImageResponse toImageResponse (DashScopeImageApi.DashScopeImageAsyncReponse.DashScopeImageAsyncReponseOutput output) { - List genImageList = output.results(); - if (genImageList == null || genImageList.isEmpty()) { - return new ImageResponse(List.of()); - } - List imageGenerationList = genImageList.stream().map(entry -> new ImageGeneration(new Image(entry.url(), Base64.getEncoder().encodeToString(entry.url().getBytes())))).toList(); - - return new ImageResponse(imageGenerationList); - } - - private DashScopeImageApi.DashScopeImageRequest constructImageRequest (ImagePrompt imagePrompt, DashScopeImageOptions options) { - - return new DashScopeImageApi.DashScopeImageRequest(options.getModel(), new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestInput(imagePrompt.getInstructions().get(0).getText(), options.getNegativePrompt(), options.getRefImg()), new DashScopeImageApi.DashScopeImageRequest.DashScopeImageRequestParameter(options.getStyle(), options.getSize(), options.getN(), options.getSeed(), options.getRefStrength(), options.getRefMode())); - } - - /** - * Use the provided convention for reporting observation data - * - * @param observationConvention The provided convention - */ - public void setObservationConvention (ImageModelObservationConvention observationConvention) { - Assert.notNull(observationConvention, "observationConvention cannot be null"); - this.observationConvention = observationConvention; - } - - public String submitImageGenTask (ImagePrompt request) { - - DashScopeImageOptions imageOptions = toImageOptions(request.getOptions()); - logger.debug("Image options: {}", imageOptions); - - DashScopeImageApi.DashScopeImageRequest dashScopeImageRequest = constructImageRequest(request, imageOptions); - - ResponseEntity submitResponse = dashScopeImageApi.submitImageGenTask(dashScopeImageRequest); - - if (submitResponse == null || submitResponse.getBody() == null) { - logger.warn("Submit imageGen error,request: {}", request); - return null; - } - - return submitResponse.getBody().output().taskId(); - } - - public DashScopeImageApi.DashScopeImageAsyncReponse getImageGenTask (String taskId) { - ResponseEntity getImageGenResponse = dashScopeImageApi.getImageGenTaskResult(taskId); - if (getImageGenResponse == null || getImageGenResponse.getBody() == null) { - logger.warn("No image response returned for taskId: {}", taskId); - return null; - } - return getImageGenResponse.getBody(); - } - - private boolean isTaskCompleted (DashScopeImageApi.DashScopeImageAsyncReponse response) { - List checkedTaskStatus = List.of("SUCCEEDED", "FAILED"); - return checkedTaskStatus.contains(response.output().taskStatus()); - } } diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java index 24d896546..0d31a3641 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java @@ -19,39 +19,54 @@ @EnabledIfEnvironmentVariable(named = "AI_DASHSCOPE_API_KEY", matches = ".+") /** * @author 北极星 - */ public class DashScopeImageModelObservationIT { + */ +public class DashScopeImageModelObservationIT { - @Autowired - ImageModel imageModel; + @Autowired + ImageModel imageModel; - @Autowired - TestObservationRegistry observationRegistry; + @Autowired + TestObservationRegistry observationRegistry; - @Test - void imageModelObservationTest () { + @Test + void imageModelObservationTest() { - var options = ImageOptionsBuilder.builder().model("wanx2.1-t2i-turbo") - .withHeight(1024) - .withWidth(1024) - .N(1) - .build(); + var options = ImageOptionsBuilder.builder() + .model("wanx2.1-t2i-turbo") + .withHeight(1024) + .withWidth(1024) + .N(1) + .build(); - var instructions = """ - A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; + var instructions = """ + A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; - ImagePrompt imagePrompt = new ImagePrompt(instructions, options); + ImagePrompt imagePrompt = new ImagePrompt(instructions, options); - ImageResponse imageResponse = imageModel.call(imagePrompt); + ImageResponse imageResponse = imageModel.call(imagePrompt); - assertThat(imageResponse.getResults()).hasSize(1); + assertThat(imageResponse.getResults()).hasSize(1); - ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); - assertThat(imageResponseMetadata.getCreated()).isPositive(); + ImageResponseMetadata imageResponseMetadata = imageResponse.getMetadata(); + assertThat(imageResponseMetadata.getCreated()).isPositive(); - var generation = imageResponse.getResult(); - Image image = generation.getOutput(); - assertThat(image.getUrl()).isNotEmpty(); + var generation = imageResponse.getResult(); + Image image = generation.getOutput(); + assertThat(image.getUrl()).isNotEmpty(); + + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .doesNotHaveAnyRemainingCurrentObservation() + .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) + .that() + .hasContextualNameEqualTo("image " + "wanx2.1-t2i-turbo") + .hasHighCardinalityKeyValue( + ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), + "1024x1024") + .hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), + AiProvider.DASHSCOPE.value()) + .hasLowCardinalityKeyValue( + ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), + AiOperationType.IMAGE.value()); + } - TestObservationRegistryAssert.assertThat(this.observationRegistry).doesNotHaveAnyRemainingCurrentObservation().hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME).that().hasContextualNameEqualTo("image " + "wanx2.1-t2i-turbo").hasHighCardinalityKeyValue(ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), "1024x1024").hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), AiProvider.DASHSCOPE.value()).hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), AiOperationType.IMAGE.value()); - } } From 2315c4255def0b0d4453017b50e3a07897b96ece Mon Sep 17 00:00:00 2001 From: PolarishT Date: Wed, 15 Jan 2025 19:19:05 +0800 Subject: [PATCH 8/9] add Test License header Signed-off-by: PolarishT --- .../apache/pdfbox/PagePdfDocumentParser.java | 3 +-- .../apache/pdfbox/ParagraphPdfDocumentParser.java | 3 +-- .../apache/pdfbox/PagePdfDocumentParserTests.java | 3 +-- .../pdfbox/ParagraphPdfDocumentParserTests.java | 3 +-- .../ai/parser/pdf/tables/PdfTablesParser.java | 1 - .../parser/pdf/tables/PdfTablesParserTests.java | 1 - .../baidumap/BaiDuMapAutoConfiguration.java | 3 +-- .../baidumap/BaiDuMapProperties.java | 3 +-- .../baidumap/MapSearchService.java | 3 +-- .../ai/functioncalling/baidumap/MapTools.java | 3 +-- .../BaidutranslateAutoConfiguration.java | 1 - .../baidutranslate/BaidutranslateProperties.java | 1 - .../baidutranslate/BaidutranslateService.java | 1 - .../dashscope/DashScopeRerankProperties.java | 1 - .../cloud/ai/advisor/RetrievalRerankAdvisor.java | 1 - .../cloud/ai/dashscope/chat/MessageFormat.java | 1 - .../cloud/ai/dashscope/rag/OpenSearchConfig.java | 3 +-- .../cloud/ai/dashscope/rag/OpenSearchVector.java | 3 +-- .../ai/dashscope/rerank/DashScopeRerankModel.java | 1 - .../dashscope/rerank/DashScopeRerankOptions.java | 1 - .../cloud/ai/document/DocumentWithScore.java | 1 - .../com/alibaba/cloud/ai/model/RerankModel.java | 1 - .../com/alibaba/cloud/ai/model/RerankOptions.java | 1 - .../com/alibaba/cloud/ai/model/RerankRequest.java | 1 - .../alibaba/cloud/ai/model/RerankResponse.java | 1 - .../cloud/ai/model/RerankResponseMetadata.java | 1 - .../cloud/ai/model/RerankResultMetadata.java | 1 - .../ai/transformer/splitter/SentenceSplitter.java | 1 - .../dashscope/DashScopeRerankProperties.java | 1 - .../image/DashScopeImageModelObservationIT.java | 15 +++++++++++++++ .../rerank/DashScopeRerankModelTest.java | 1 - .../alibaba/cloud/ai/evaluation/EvaluationIT.java | 1 - .../splitter/SentenceSplitterTest.java | 1 - 33 files changed, 25 insertions(+), 42 deletions(-) diff --git a/community/document-parsers/document-parser-apache-pdfbox/src/main/java/com/alibaba/cloud/ai/parser/apache/pdfbox/PagePdfDocumentParser.java b/community/document-parsers/document-parser-apache-pdfbox/src/main/java/com/alibaba/cloud/ai/parser/apache/pdfbox/PagePdfDocumentParser.java index 1200553ed..fe6e8a999 100644 --- a/community/document-parsers/document-parser-apache-pdfbox/src/main/java/com/alibaba/cloud/ai/parser/apache/pdfbox/PagePdfDocumentParser.java +++ b/community/document-parsers/document-parser-apache-pdfbox/src/main/java/com/alibaba/cloud/ai/parser/apache/pdfbox/PagePdfDocumentParser.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.parser.apache.pdfbox; import java.awt.Rectangle; diff --git a/community/document-parsers/document-parser-apache-pdfbox/src/main/java/com/alibaba/cloud/ai/parser/apache/pdfbox/ParagraphPdfDocumentParser.java b/community/document-parsers/document-parser-apache-pdfbox/src/main/java/com/alibaba/cloud/ai/parser/apache/pdfbox/ParagraphPdfDocumentParser.java index efc44c895..70863a937 100644 --- a/community/document-parsers/document-parser-apache-pdfbox/src/main/java/com/alibaba/cloud/ai/parser/apache/pdfbox/ParagraphPdfDocumentParser.java +++ b/community/document-parsers/document-parser-apache-pdfbox/src/main/java/com/alibaba/cloud/ai/parser/apache/pdfbox/ParagraphPdfDocumentParser.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.parser.apache.pdfbox; import java.awt.Rectangle; diff --git a/community/document-parsers/document-parser-apache-pdfbox/src/test/java/com/alibaba/cloud/ai/parser/apache/pdfbox/PagePdfDocumentParserTests.java b/community/document-parsers/document-parser-apache-pdfbox/src/test/java/com/alibaba/cloud/ai/parser/apache/pdfbox/PagePdfDocumentParserTests.java index f5ed5dfda..b07333729 100644 --- a/community/document-parsers/document-parser-apache-pdfbox/src/test/java/com/alibaba/cloud/ai/parser/apache/pdfbox/PagePdfDocumentParserTests.java +++ b/community/document-parsers/document-parser-apache-pdfbox/src/test/java/com/alibaba/cloud/ai/parser/apache/pdfbox/PagePdfDocumentParserTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.parser.apache.pdfbox; import java.io.IOException; diff --git a/community/document-parsers/document-parser-apache-pdfbox/src/test/java/com/alibaba/cloud/ai/parser/apache/pdfbox/ParagraphPdfDocumentParserTests.java b/community/document-parsers/document-parser-apache-pdfbox/src/test/java/com/alibaba/cloud/ai/parser/apache/pdfbox/ParagraphPdfDocumentParserTests.java index 199f5f445..ab282d286 100644 --- a/community/document-parsers/document-parser-apache-pdfbox/src/test/java/com/alibaba/cloud/ai/parser/apache/pdfbox/ParagraphPdfDocumentParserTests.java +++ b/community/document-parsers/document-parser-apache-pdfbox/src/test/java/com/alibaba/cloud/ai/parser/apache/pdfbox/ParagraphPdfDocumentParserTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.parser.apache.pdfbox; import org.junit.jupiter.api.Test; diff --git a/community/document-parsers/document-parser-pdf-tables/src/main/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParser.java b/community/document-parsers/document-parser-pdf-tables/src/main/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParser.java index 3731573c5..4d8a31614 100644 --- a/community/document-parsers/document-parser-pdf-tables/src/main/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParser.java +++ b/community/document-parsers/document-parser-pdf-tables/src/main/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParser.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.parser.pdf.tables; import java.io.InputStream; diff --git a/community/document-parsers/document-parser-pdf-tables/src/test/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParserTests.java b/community/document-parsers/document-parser-pdf-tables/src/test/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParserTests.java index cc20bc4cf..8448d70ba 100644 --- a/community/document-parsers/document-parser-pdf-tables/src/test/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParserTests.java +++ b/community/document-parsers/document-parser-pdf-tables/src/test/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParserTests.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.parser.pdf.tables; import java.io.FileInputStream; diff --git a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/BaiDuMapAutoConfiguration.java b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/BaiDuMapAutoConfiguration.java index d76aea56c..10e7b30ae 100644 --- a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/BaiDuMapAutoConfiguration.java +++ b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/BaiDuMapAutoConfiguration.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.functioncalling.baidumap; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; diff --git a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/BaiDuMapProperties.java b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/BaiDuMapProperties.java index b925d5547..43ccee359 100644 --- a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/BaiDuMapProperties.java +++ b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/BaiDuMapProperties.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.functioncalling.baidumap; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/MapSearchService.java b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/MapSearchService.java index c0f62715d..f3630638d 100644 --- a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/MapSearchService.java +++ b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/MapSearchService.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.functioncalling.baidumap; import com.fasterxml.jackson.annotation.JsonClassDescription; diff --git a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/MapTools.java b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/MapTools.java index 03ac5c6a1..74c251a84 100644 --- a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/MapTools.java +++ b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidumap/src/main/java/com/alibaba/cloud/ai/functioncalling/baidumap/MapTools.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.functioncalling.baidumap; import java.net.URI; diff --git a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateAutoConfiguration.java b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateAutoConfiguration.java index c2d8084d8..083e7fcc0 100644 --- a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateAutoConfiguration.java +++ b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateAutoConfiguration.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.functioncalling.baidutranslate; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; diff --git a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateProperties.java b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateProperties.java index c3de29c4e..ab35cc689 100644 --- a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateProperties.java +++ b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateProperties.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.functioncalling.baidutranslate; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateService.java b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateService.java index 505726a60..73900fc63 100644 --- a/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateService.java +++ b/community/function-calling/spring-ai-alibaba-starter-function-calling-baidutranslate/src/main/java/com/alibaba/cloud/ai/functioncalling/baidutranslate/BaidutranslateService.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.functioncalling.baidutranslate; import com.fasterxml.jackson.annotation.JsonClassDescription; diff --git a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeRerankProperties.java b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeRerankProperties.java index 8a939b16e..9017ac332 100644 --- a/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeRerankProperties.java +++ b/spring-ai-alibaba-autoconfigure/src/main/java/com/alibaba/cloud/ai/autoconfigure/dashscope/DashScopeRerankProperties.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.autoconfigure.dashscope; import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/RetrievalRerankAdvisor.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/RetrievalRerankAdvisor.java index 0ed763c70..e665a8a49 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/RetrievalRerankAdvisor.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/advisor/RetrievalRerankAdvisor.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.advisor; import com.alibaba.cloud.ai.document.DocumentWithScore; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/chat/MessageFormat.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/chat/MessageFormat.java index e4d466361..346cfc942 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/chat/MessageFormat.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/chat/MessageFormat.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.dashscope.chat; /** diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rag/OpenSearchConfig.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rag/OpenSearchConfig.java index c26f7eb81..6c3bc766b 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rag/OpenSearchConfig.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rag/OpenSearchConfig.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.dashscope.rag; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rag/OpenSearchVector.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rag/OpenSearchVector.java index 906ef931d..632e8b970 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rag/OpenSearchVector.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rag/OpenSearchVector.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.dashscope.rag; import com.alibaba.fastjson.JSON; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankModel.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankModel.java index c862e920d..8dd1cedf8 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankModel.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankModel.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.dashscope.rerank; import java.util.Collections; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankOptions.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankOptions.java index 5e726d8f4..400bbeaa2 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankOptions.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankOptions.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.dashscope.rerank; import com.alibaba.cloud.ai.model.RerankOptions; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/document/DocumentWithScore.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/document/DocumentWithScore.java index 6c4fbace4..2ddadc74c 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/document/DocumentWithScore.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/document/DocumentWithScore.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.document; import com.alibaba.cloud.ai.model.RerankResultMetadata; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankModel.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankModel.java index 1079caa8c..106a39778 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankModel.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankModel.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.model; import org.springframework.ai.model.Model; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankOptions.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankOptions.java index 24d247081..ff936373d 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankOptions.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankOptions.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.model; import org.springframework.ai.model.ModelOptions; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankRequest.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankRequest.java index 494899f9b..ce7bc3b80 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankRequest.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankRequest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.model; import org.springframework.ai.document.Document; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResponse.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResponse.java index cf74094cc..d4b3e53db 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResponse.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResponse.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.model; import java.util.List; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResponseMetadata.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResponseMetadata.java index 3c89e0d3b..3825b9751 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResponseMetadata.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResponseMetadata.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.model; import java.util.Map; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResultMetadata.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResultMetadata.java index d3cbf787d..c754dfb92 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResultMetadata.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/model/RerankResultMetadata.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.model; import org.springframework.ai.model.ResultMetadata; diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitter.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitter.java index 3516f85f3..f3f0c8f6f 100644 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitter.java +++ b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitter.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.transformer.splitter; import java.io.IOException; diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeRerankProperties.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeRerankProperties.java index cc8b0052e..5d9775c12 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeRerankProperties.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/autoconfig/dashscope/DashScopeRerankProperties.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.autoconfig.dashscope; import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions; diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java index 0d31a3641..109a3bc15 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/image/DashScopeImageModelObservationIT.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.alibaba.cloud.ai.dashscope.image; import com.alibaba.cloud.ai.dashscope.DashscopeAiTestConfiguration; diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankModelTest.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankModelTest.java index ffa5c0bf0..0b3857749 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankModelTest.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/dashscope/rerank/DashScopeRerankModelTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.dashscope.rerank; import com.alibaba.cloud.ai.dashscope.DashscopeAiTestConfiguration; diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/evaluation/EvaluationIT.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/evaluation/EvaluationIT.java index 5000f18a1..0f30db48a 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/evaluation/EvaluationIT.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/evaluation/EvaluationIT.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.evaluation; import java.io.IOException; diff --git a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitterTest.java b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitterTest.java index bf3c6358f..7a364b644 100644 --- a/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitterTest.java +++ b/spring-ai-alibaba-core/src/test/java/com/alibaba/cloud/ai/transformer/splitter/SentenceSplitterTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.alibaba.cloud.ai.transformer.splitter; import java.io.IOException; From c38bd9d5aa78add8f514700cd85a501f44666d45 Mon Sep 17 00:00:00 2001 From: PolarishT Date: Wed, 15 Jan 2025 19:22:48 +0800 Subject: [PATCH 9/9] delete useless impl Signed-off-by: PolarishT --- ...hScopeImageModelObservationConvention.java | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java diff --git a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java b/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java deleted file mode 100644 index 5de6af89a..000000000 --- a/spring-ai-alibaba-core/src/main/java/com/alibaba/cloud/ai/dashscope/image/observation/DashScopeImageModelObservationConvention.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.alibaba.cloud.ai.dashscope.image.observation; - -import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; -import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; -import com.alibaba.fastjson.JSON; -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; -import org.springframework.ai.chat.observation.ChatModelObservationContext; -import org.springframework.ai.chat.observation.ChatModelObservationDocumentation; -import org.springframework.ai.image.observation.DefaultImageModelObservationConvention; -import org.springframework.ai.image.observation.ImageModelObservationContext; -import org.springframework.util.CollectionUtils; - -import java.util.List; -import java.util.Objects; - -/** - * Dashscope conventions to populate observations for Image model operations. - * - * @author Lumian - * @since 1.0.0 - */ -public class DashScopeImageModelObservationConvention extends DefaultImageModelObservationConvention { - - public static final String DEFAULT_NAME = "gen_ai.client.operation"; - - private static final String ILLEGAL_STOP_CONTENT = ""; - - @Override - public String getName() { - return DEFAULT_NAME; - } - -}