feat: adapt spring-ai images api (#3678)

Signed-off-by: yuluo-yx <yuluo08290126@gmail.com>
pull/3686/head
YuLuo 10 months ago committed by GitHub
parent 7ac3db5ff8
commit cb3d302f24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -6,7 +6,7 @@ The Spring Cloud Alibaba AI module is based on [Spring AI 0.8.1](https://docs.sp
[model service dashscope](https://help.aliyun.com/zh/dashscope/) It is a big model application service launched by Alibaba. Based on the concept of "Model-as-a-Service" (MaaS), Lingji Model Service provides a variety of model services including model reasoning and model fine-tuning training through standardized APIs around AI models in various fields.
- Current completion of spring-ai chat api.
- Current completion of spring-ai chat api and image api.
## Application access
@ -28,23 +28,47 @@ The Spring Cloud Alibaba AI module is based on [Spring AI 0.8.1](https://docs.sp
cloud:
ai:
tongyi:
chat:
options:
# api_key is invalied.
api-key: sk-a3d73b1709bf4a178c28ed7c8b3b5a45
# apikey is invalid.
api-key: sk-a3d73b1709bf4a178c28ed7c8b3b5a45
```
3. Add the following code:
```yml
spring:
cloud:
ai:
tongyi:
chat:
options:
# api_key is invalied.
api-key: sk-a3d73b1709bf4a178c28ed7c8b3b5a45
controller:
@Autowired
@Qualifier("tongYiSimpleServiceImpl")
private TongYiService tongYiSimpleService;
@GetMapping("/example")
public String completion(
@RequestParam(value = "message", defaultValue = "Tell me a joke")
String message
) {
return tongYiSimpleService.completion(message);
}
service:
private final ChatClient chatClient;
@Autowired
public TongYiSimpleServiceImpl(ChatClient chatClient, StreamingChatClient streamingChatClient) {
this.chatClient = chatClient;
this.streamingChatClient = streamingChatClient;
}
@Override
public String completion(String message) {
Prompt prompt = new Prompt(new UserMessage(message));
return chatClient.call(prompt).getResult().getOutput().getContent();
}
```
At this point, the simplest model access is complete! It is slightly different from the code in this example, but the code in the example does not need to be modified. The corresponding function can be accomplished without modification.
@ -83,6 +107,6 @@ https://help.aliyun.com/zh/dashscope/developer-reference/api-details
## More examples:
This example consists of 5 samples, implemented by different serviceimpl, you can refer to the readme file in each serviceimpl, use @RestController as the entry point in the controller, you can use the browser or curl tool to request the interface. You can use a browser or curl tool to request the interface.
This example consists of 6 samples, implemented by different serviceimpl, you can refer to the readme file in each serviceimpl, use @RestController as the entry point in the controller, you can use the browser or curl tool to request the interface. You can use a browser or curl tool to request the interface.
> The example is already functional and does not require any code changes. Just replace the apikey in the corresponding example, the apikey provided in the project is invalid.

@ -6,7 +6,7 @@ Spring Cloud Alibaba AI 模块基于 [Spring AI 0.8.1](https://docs.spring.io/sp
[模型服务灵积](https://help.aliyun.com/zh/dashscope/) 是阿里巴巴推出的一个大模型应用服务。灵积模型服务建立在“模型即服务”Model-as-a-ServiceMaaS的理念基础之上围绕AI各领域模型通过标准化的API提供包括模型推理、模型微调训练在内的多种模型服务。
- 目前只完成 spring-ai chat completion api 的接入。
- 目前只完成 spring-ai chat completion api 和 image api 的接入。
## 应用接入
@ -25,15 +25,13 @@ Spring Cloud Alibaba AI 模块基于 [Spring AI 0.8.1](https://docs.spring.io/sp
```yaml
spring:
cloud:
cloud:
ai:
tongyi:
chat:
options:
# api_key is invalied.
api-key: sk-a3d73b1709bf4a178c28ed7c8b3b5a45
# apiKey is invalid.
api-key: sk-a3d73b1709bf4a178c28ed7c8b3b5a45
```
3. 添加如下代码:
```java
@ -108,6 +106,6 @@ https://help.aliyun.com/zh/dashscope/developer-reference/api-details
## 更多 example 示例:
本 example 由 5 个示例组成,由不同的 serviceimpl 实现,您可以参考每个 serviceimpl 中的 readme 文件,在 controller 中使用 @RestController 作为入口点,您可以使用浏览器或者 curl 工具对接口进行请求。完成对应功能接入的练习。
本 example 由 6 个示例组成,由不同的 serviceimpl 实现,您可以参考每个 serviceimpl 中的 readme 文件,在 controller 中使用 @RestController 作为入口点,您可以使用浏览器或者 curl 工具对接口进行请求。完成对应功能接入的练习。
> example 已完成功能,无需修改代码。只在对应的 example 中替换 apikey 即可,项目中提供的 apikey 无效。

@ -23,6 +23,7 @@ import com.alibaba.cloud.ai.example.tongyi.models.Completion;
import com.alibaba.cloud.ai.example.tongyi.service.TongYiService;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.image.ImageResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.CrossOrigin;
@ -114,4 +115,15 @@ public class TongYiController {
return tongYiStuffService.stuffCompletion(message, stuffit);
}
@Autowired
@Qualifier("tongYiImagesServiceImpl")
private TongYiService tongYiImgService;
@GetMapping("/img")
public ImageResponse genImg(@RequestParam(value = "prompt",
defaultValue = "Painting a picture of blue water and blue sky.") String imgPrompt) {
return tongYiImgService.genImg(imgPrompt);
}
}

@ -22,6 +22,7 @@ import com.alibaba.cloud.ai.example.tongyi.models.ActorsFilms;
import com.alibaba.cloud.ai.example.tongyi.models.Completion;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.image.ImageResponse;
/**
* @author yuluo
@ -66,4 +67,10 @@ public abstract class AbstractTongYiServiceImpl implements TongYiService {
return null;
}
@Override
public ImageResponse genImg(String imgPrompt) {
return null;
}
}

@ -22,6 +22,7 @@ import com.alibaba.cloud.ai.example.tongyi.models.ActorsFilms;
import com.alibaba.cloud.ai.example.tongyi.models.Completion;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.image.ImageResponse;
/**
* @author yuluo
@ -82,4 +83,11 @@ public interface TongYiService {
* @return Completion object.
*/
Completion stuffCompletion(String message, boolean stuffit);
/**
* Gen images.
* @param imgPrompt prompt info.
* @return {@link ImageResponse}
*/
ImageResponse genImg(String imgPrompt);
}

@ -0,0 +1,75 @@
# Spring Cloud Alibaba AI Image
`TongYiController` 接受一个 HTTP GET 请求 `http://localhost:8080/ai/img`
`controller` 将会调用 `TongYiService` 中的 `genImg` 方法,完成服务请求得到响应。
有一个可选的 `prompt` 参数为生成图片的提示信息其默认值为“Painting a picture of blue water and blue sky.”。
## 构建和运行
1. 修改配置文件 `application.yml` 中的 apikey 为有效的 apikey
2. 通过 IDE 或者 `./mvnw spring-boot:run` 运行应用程序。
## 访问接口
使用 curl 工具对接口发起请求:
```shell
$ curl http://localhost:8080/ai/img
```
响应结果为:(base64 数据太多,使用 `xxx` 代替)
```json
{
"result": {
"output": {
"url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1d/42/20240408/8d820c8d/e9913e23-24e9-4de7-8977-e4ccab33a231-1.png?Expires=1712670359&OSSAccessKeyId=LTAI5tQZd8AEcZX6KZV4G8qL&Signature=bMEaNS0RGTpD2yXO0lTMMY5AWxM%3D",
"b64Json": "xxxx"
},
"metadata": null
},
"metadata": {
"created": 1712583967354,
"taskId": "43d7e458-9f3e-4a51-9a39-17559d8f135d",
"metrics": {
"total": 4,
"succeeded": 4,
"failed": 0
},
"usage": {
"imageCount": 4
}
},
"results": [
{
"output": {
"url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1d/42/20240408/8d820c8d/e9913e23-24e9-4de7-8977-e4ccab33a231-1.png?Expires=1712670359&OSSAccessKeyId=LTAI5tQZd8AEcZX6KZV4G8qL&Signature=bMEaNS0RGTpD2yXO0lTMMY5AWxM%3D",
"b64Json": "xxxx"
},
"metadata": null
},
{
"output": {
"url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1d/2b/20240408/8d820c8d/0bd0b40f-4e34-46da-8706-8f2ec86274d7-1.png?Expires=1712670359&OSSAccessKeyId=LTAI5tQZd8AEcZX6KZV4G8qL&Signature=dl3sMGQn8p7y%2FzKOmPR%2B64prQV4%3D",
"b64Json": "xxxx"
},
"metadata": null
},
{
"output": {
"url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1d/62/20240408/c34adf05/ffb89a14-14c5-4740-ab55-37b59a69aaef-1.png?Expires=1712670359&OSSAccessKeyId=LTAI5tQZd8AEcZX6KZV4G8qL&Signature=vYd667hVPQUTt8aiJDBFxN%2B08jI%3D",
"b64Json": "xxxx"
},
"metadata": null
},
{
"output": {
"url": "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1d/b5/20240408/8d820c8d/594b8672-c1ce-49b6-bab0-06e3616b4e0e-1.png?Expires=1712670359&OSSAccessKeyId=LTAI5tQZd8AEcZX6KZV4G8qL&Signature=ERyoV7pmmI8sZJwSFLpzknhfFEk%3D",
"b64Json": "xxxx"
},
"metadata": null
}
]
}
```

@ -0,0 +1,61 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.example.tongyi.service.impl.images;
import com.alibaba.cloud.ai.example.tongyi.service.AbstractTongYiServiceImpl;
import com.alibaba.cloud.ai.example.tongyi.service.TongYiService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.image.ImageClient;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* gen images example.
*
* @author yuluo
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
*/
@Slf4j
@Service
public class TongYiImagesServiceImpl extends AbstractTongYiServiceImpl {
private static final Logger logger = LoggerFactory.getLogger(TongYiService.class);
private final ImageClient imageClient;
@Autowired
public TongYiImagesServiceImpl(ImageClient client) {
this.imageClient = client;
}
@Override
public ImageResponse genImg(String imgPrompt) {
var prompt = new ImagePrompt(imgPrompt);
return imageClient.call(prompt);
}
}

@ -1,4 +1,4 @@
# Spring Cloud Alibaba AI Hello World
# Spring Cloud Alibaba AI Output
`TongYiController` 接受一个 HTTP GET 请求 `http://localhost:8080/ai/output`
`controller` 将会调用 `TongYiService` 中的 `genOutputParse` 方法,完成服务请求得到响应。

@ -1,4 +1,4 @@
# Spring Cloud Alibaba AI Hello World
# Spring Cloud Alibaba AI Prompt Template
`TongYiController` 接受一个 HTTP GET 请求 `http://localhost:8080/ai/prompt-tmpl`
`controller` 将会调用 `TongYiService` 中的 `genPromptTemplates` 方法,完成服务请求得到响应。

@ -1,4 +1,4 @@
# Spring Cloud Alibaba AI Hello World
# Spring Cloud Alibaba AI Roles
`TongYiController` 接受一个 HTTP GET 请求 `http://localhost:8080/ai/roles`
`controller` 将会调用 `TongYiService` 中的 `genRole` 方法,完成服务请求得到响应。

@ -1,4 +1,4 @@
# Spring Cloud Alibaba AI Hello World
# Spring Cloud Alibaba AI Stuff models
`TongYiController` 接受一个 HTTP GET 请求 `http://localhost:8080/ai/stuff`
`controller` 将会调用 `TongYiService` 中的 `stuffCompletion` 方法,完成服务请求得到响应。

@ -20,9 +20,8 @@ server:
spring:
application:
name: tongyi-example
cloud:
ai:
tongyi:
chat:
options:
api-key: sk-a3d73b1709bf4a178c28ed7c8b3b5a45
api-key: sk-a3d73b1709bf4a178c28ed7c8b3b5a54

@ -1,3 +1,3 @@
[Spring AI](https://docs.spring.io/spring-ai/reference/0.8-SNAPSHOT/index.html) <br>
[通义大模型](https://help.aliyun.com/zh/dashscope/developer-reference/model-introduction) <br>
[Spring AI](https://docs.spring.io/spring-ai/reference/0.8-SNAPSHOT/index.html) <br><br>
[通义大模型](https://help.aliyun.com/zh/dashscope/developer-reference/model-introduction) <br><br>
[Spring AI chat completion api](https://docs.spring.io/spring-ai/reference/0.8-SNAPSHOT/api/chatclient.html)

@ -16,24 +16,42 @@
package com.alibaba.cloud.ai.tongyi;
import java.util.Objects;
import com.alibaba.cloud.ai.tongyi.exception.TongYiException;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
import com.alibaba.dashscope.common.MessageManager;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.ApiKey;
import com.alibaba.dashscope.utils.Constants;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
/**
* @author yuluo
* @since 2023.0.0.0
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
*/
@AutoConfiguration
@ConditionalOnClass({TongYiChatClient.class, MessageManager.class})
@EnableConfigurationProperties(TongYiChatProperties.class)
@ConditionalOnClass({
MessageManager.class,
TongYiChatClient.class,
TongYiImagesClient.class
})
@EnableConfigurationProperties({
TongYiChatProperties.class,
TongYiImagesProperties.class,
TongYiConnectionProperties.class
})
public class TongYiAutoConfiguration {
@Bean
@ -50,6 +68,23 @@ public class TongYiAutoConfiguration {
return new MessageManager(10);
}
@Bean
@ConditionalOnMissingBean
public ImageSynthesis imageSynthesis() {
return new ImageSynthesis();
}
@Bean
@ConditionalOnMissingBean
public FunctionCallbackContext springAiFunctionManager(ApplicationContext context) {
FunctionCallbackContext manager = new FunctionCallbackContext();
manager.setApplicationContext(context);
return manager;
}
@Bean
@ConditionalOnProperty(
prefix = TongYiChatProperties.CONFIG_PREFIX,
@ -57,9 +92,55 @@ public class TongYiAutoConfiguration {
havingValue = "true",
matchIfMissing = true
)
public TongYiChatClient tongYiChatClient(Generation generation, TongYiChatProperties chatOptions) {
public TongYiChatClient tongYiChatClient(Generation generation,
TongYiChatProperties chatOptions,
TongYiConnectionProperties connectionProperties
) {
settingApiKey(connectionProperties);
return new TongYiChatClient(generation, chatOptions.getOptions());
}
@Bean
@ConditionalOnProperty(
prefix = TongYiImagesProperties.CONFIG_PREFIX,
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
public TongYiImagesClient tongYiImagesClient(
ImageSynthesis imageSynthesis,
TongYiImagesProperties imagesOptions,
TongYiConnectionProperties connectionProperties
) {
settingApiKey(connectionProperties);
return new TongYiImagesClient(imageSynthesis, imagesOptions.getOptions());
}
/**
* Setting TongYi model apiKey .
*/
public void settingApiKey(TongYiConnectionProperties connectionProperties) {
String apiKey;
try {
if (Objects.nonNull(connectionProperties.getApiKey())) {
apiKey = connectionProperties.getApiKey();
}
else {
apiKey = ApiKey.getApiKey(null);
}
Constants.apiKey = apiKey;
}
catch (NoApiKeyException e) {
throw new TongYiException(e.getMessage());
}
}
}

@ -33,9 +33,7 @@ import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.tools.FunctionDefinition;
import com.alibaba.dashscope.tools.ToolCallBase;
import com.alibaba.dashscope.tools.ToolCallFunction;
import com.alibaba.dashscope.utils.ApiKey;
import com.alibaba.dashscope.utils.ApiKeywords;
import com.alibaba.dashscope.utils.Constants;
import com.alibaba.dashscope.utils.JsonUtils;
import io.reactivex.Flowable;
import org.slf4j.Logger;
@ -61,6 +59,7 @@ import org.springframework.util.CollectionUtils;
* backed by {@link Generation}.
*
* @author yuluo
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
* @see ChatClient
* @see com.alibaba.dashscope.aigc.generation
@ -224,14 +223,6 @@ public class TongYiChatClient extends
*/
ConversationParam toTongYiChatParams(Prompt prompt) {
try {
Constants.apiKey = getKey();
}
catch (NoApiKeyException e) {
logger.warn(e.getMessage());
throw new TongYiException(e.getMessage());
}
Set<String> functionsForThisRequest = new HashSet<>();
List<com.alibaba.dashscope.common.Message> tongYiMessage = prompt.getInstructions().stream()
@ -385,25 +376,6 @@ public class TongYiChatClient extends
};
}
/**
* Get TongYi model api_key .
* todo: Get key from env and env_file.
* @return api_key.
*/
private String getKey() throws NoApiKeyException {
String apiKey;
if (Objects.nonNull(this.defaultOptions.getApiKey())) {
apiKey = this.defaultOptions.getApiKey();
}
else {
apiKey = ApiKey.getApiKey(null);
}
return apiKey;
}
@Override
protected ConversationParam doCreateToolResponseRequest(
@ -437,6 +409,8 @@ public class TongYiChatClient extends
}
ConversationParam newRequest = ConversationParam.builder().messages(conversationHistory).build();
// todo: No @JsonProperty fields.
newRequest = ModelOptionsUtils.merge(newRequest, previousRequest, ConversationParam.class);
return newRequest;

@ -38,11 +38,6 @@ import org.springframework.util.Assert;
public class TongYiChatOptions implements FunctionCallingOptions, ChatOptions {
/**
* TongYi model api key.
*/
private String apiKey;
/**
* TongYi Models.
* {@link Generation.Models}
@ -173,16 +168,6 @@ public class TongYiChatOptions implements FunctionCallingOptions, ChatOptions {
this.topK = topK;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getModel() {
return model;
@ -324,8 +309,7 @@ public class TongYiChatOptions implements FunctionCallingOptions, ChatOptions {
TongYiChatOptions that = (TongYiChatOptions) o;
return Objects.equals(apiKey, that.apiKey)
&& Objects.equals(model, that.model)
return Objects.equals(model, that.model)
&& Objects.equals(seed, that.seed)
&& Objects.equals(maxTokens, that.maxTokens)
&& Objects.equals(topP, that.topP)
@ -346,7 +330,6 @@ public class TongYiChatOptions implements FunctionCallingOptions, ChatOptions {
public int hashCode() {
return Objects.hash(
apiKey,
model,
seed,
maxTokens,
@ -370,7 +353,6 @@ public class TongYiChatOptions implements FunctionCallingOptions, ChatOptions {
final StringBuilder sb = new StringBuilder("TongYiChatOptions{");
sb.append("apiKey='").append(apiKey).append('\'');
sb.append(", model='").append(model).append('\'');
sb.append(", seed=").append(seed);
sb.append(", maxTokens=").append(maxTokens);

@ -17,7 +17,7 @@
package com.alibaba.cloud.ai.tongyi;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.models.QwenParam;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ -56,7 +56,7 @@ public class TongYiChatProperties {
.withModel(DEFAULT_DEPLOYMENT_NAME)
.withTemperature(DEFAULT_TEMPERATURE)
.withEnableSearch(true)
.withResultFormat(QwenParam.ResultFormat.MESSAGE)
.withResultFormat(GenerationParam.ResultFormat.MESSAGE)
.build();
public TongYiChatOptions getOptions() {

@ -0,0 +1,49 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.tongyi;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* TongYi connection API properties.
*
* @author yuluo
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
*/
@ConfigurationProperties(TongYiConnectionProperties.CONFIG_PREFIX)
public class TongYiConnectionProperties {
/**
* Spring Cloud Alibaba AI configuration prefix.
*/
public static final String CONFIG_PREFIX = "spring.cloud.ai.tongyi";
/**
* ApiKey.
*/
private String apiKey;
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
}

@ -0,0 +1,233 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.tongyi;
import java.io.ByteArrayOutputStream;
import java.net.URL;
import java.util.Base64;
import java.util.stream.Collectors;
import com.alibaba.cloud.ai.tongyi.exception.TongYiImagesException;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult;
import com.alibaba.dashscope.exception.NoApiKeyException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.image.Image;
import org.springframework.ai.image.ImageClient;
import org.springframework.ai.image.ImageGeneration;
import org.springframework.ai.image.ImageOptions;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.util.Assert;
import static com.alibaba.cloud.ai.tongyi.metadata.TongYiImagesResponseMetadata.from;
/**
* TongYiImagesClient is a class that implements the ImageClient interface. It provides a
* client for calling the TongYi AI image generation API.
*
* @author yuluo
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
*/
public class TongYiImagesClient implements ImageClient {
private final Logger logger = LoggerFactory.getLogger(TongYiImagesClient.class);
/**
* Gen Images API.
*/
private final ImageSynthesis imageSynthesis;
/**
* TongYi Gen images properties.
*/
private TongYiImagesOptions defaultOptions;
/**
* Adapt TongYi images api size properties.
*/
private final String sizeConnection = "*";
/**
* Get default images options.
*
* @return Default TongYiImagesOptions.
*/
public TongYiImagesOptions getDefaultOptions() {
return this.defaultOptions;
}
public TongYiImagesClient(ImageSynthesis imageSynthesis) {
this(imageSynthesis, TongYiImagesOptions.
builder()
.withModel(ImageSynthesis.Models.WANX_V1)
.withN(1)
.build()
);
}
public TongYiImagesClient(ImageSynthesis imageSynthesis, TongYiImagesOptions imagesOptions) {
Assert.notNull(imageSynthesis, "ImageSynthesis must not be null");
Assert.notNull(imagesOptions, "TongYiImagesOptions must not be null");
this.imageSynthesis = imageSynthesis;
this.defaultOptions = imagesOptions;
}
@Override
public ImageResponse call(ImagePrompt imagePrompt) {
ImageSynthesisResult result;
String prompt = imagePrompt.getInstructions().get(0).getText();
var imgParams = ImageSynthesisParam.builder()
.prompt("")
.model(ImageSynthesis.Models.WANX_V1)
.build();
if (this.defaultOptions != null) {
imgParams = merge(
this.defaultOptions,
imgParams
);
}
if (imagePrompt.getOptions() != null) {
imgParams = merge(
toTingYiImageOptions(imagePrompt.getOptions()),
imgParams
);
}
imgParams.setPrompt(prompt);
try {
result = imageSynthesis.call(imgParams);
}
catch (NoApiKeyException e) {
logger.error("TongYi models gen images failed: {}.", e.getMessage());
throw new TongYiImagesException(e.getMessage());
}
return convert(result);
}
private ImageSynthesisParam merge(TongYiImagesOptions source, ImageSynthesisParam target) {
var builder = ImageSynthesisParam.builder();
if (source != null) {
if (
source.getHeight() != null
&&
source.getWidth() != null
) {
builder.size(source.getHeight() + sizeConnection + source.getWidth());
}
if (source.getModel() != null) {
builder.model(source.getModel());
}
if (source.getN() != null) {
builder.n(source.getN());
}
}
// prompt is marked non-null but is null.
builder.prompt("");
return builder.build();
}
private ImageResponse convert(ImageSynthesisResult result) {
return new ImageResponse(
result.getOutput().getResults().stream()
.flatMap(value -> value.entrySet().stream())
.map(entry -> {
String key = entry.getKey();
String value = entry.getValue();
try {
String base64Image = convertImageToBase64(value);
Image image = new Image(value, base64Image);
return new ImageGeneration(image);
}
catch (Exception e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList()),
from(result)
);
}
private TongYiImagesOptions toTingYiImageOptions(ImageOptions runtimeImageOptions) {
var builder = TongYiImagesOptions.builder();
if (runtimeImageOptions != null) {
if (runtimeImageOptions.getN() != null) {
builder.withN(runtimeImageOptions.getN());
}
if (runtimeImageOptions.getModel() != null) {
builder.withModel(runtimeImageOptions.getModel());
}
if (runtimeImageOptions.getHeight() != null) {
builder.withHeight(runtimeImageOptions.getHeight());
}
if (runtimeImageOptions.getWidth() != null) {
builder.withWidth(runtimeImageOptions.getWidth());
}
// todo ImagesParams.
}
return builder.build();
}
public String convertImageToBase64(String imageUrl) throws Exception {
var url = new URL(imageUrl);
var inputStream = url.openStream();
var outputStream = new ByteArrayOutputStream();
var buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
var imageBytes = outputStream.toByteArray();
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
inputStream.close();
outputStream.close();
return base64Image;
}
}

@ -0,0 +1,188 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.tongyi;
import java.util.Objects;
import com.alibaba.cloud.ai.tongyi.exception.TongYiImagesException;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
import org.springframework.ai.image.ImageOptions;
/**
* TongYi Image API options.
*
* @author yuluo
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
*/
public class TongYiImagesOptions implements ImageOptions {
/**
* Specify the model name, currently only wanx-v1 is supported.
*/
private String model = ImageSynthesis.Models.WANX_V1;
/**
* Gen images number.
*/
private Integer n;
/**
* The width of the generated images.
*/
private Integer width = 1024;
/**
* The height of the generated images.
*/
private Integer height = 1024;
@Override
public Integer getN() {
return this.n;
}
@Override
public String getModel() {
return this.model;
}
@Override
public Integer getWidth() {
return this.width;
}
@Override
public Integer getHeight() {
return this.height;
}
@Override
public String getResponseFormat() {
throw new TongYiImagesException("unimplemented!");
}
public void setModel(String model) {
this.model = model;
}
public void setN(Integer n) {
this.n = n;
}
public void setWidth(Integer width) {
this.width = width;
}
public void setHeight(Integer height) {
this.height = height;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TongYiImagesOptions that = (TongYiImagesOptions) o;
return Objects.equals(model, that.model)
&& Objects.equals(n, that.n)
&& Objects.equals(width, that.width)
&& Objects.equals(height, that.height);
}
@Override
public int hashCode() {
return Objects.hash(model, n, width, height);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("TongYiImagesOptions{");
sb.append("model='").append(model).append('\'');
sb.append(", n=").append(n);
sb.append(", width=").append(width);
sb.append(", height=").append(height);
sb.append('}');
return sb.toString();
}
public static Builder builder() {
return new Builder();
}
public final static class Builder {
private final TongYiImagesOptions options;
private Builder() {
this.options = new TongYiImagesOptions();
}
public Builder withN(Integer n) {
options.setN(n);
return this;
}
public Builder withModel(String model) {
options.setModel(model);
return this;
}
public Builder withWidth(Integer width) {
options.setWidth(width);
return this;
}
public Builder withHeight(Integer height) {
options.setHeight(height);
return this;
}
public TongYiImagesOptions build() {
return options;
}
}
}

@ -0,0 +1,80 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.tongyi;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
/**
* TongYi Image API properties.
*
* @author yuluo
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
*/
@ConfigurationProperties(TongYiImagesProperties.CONFIG_PREFIX)
public class TongYiImagesProperties {
private final Logger logger = LoggerFactory.getLogger(TongYiImagesProperties.class);
/**
* Spring Cloud Alibaba AI configuration prefix.
*/
public static final String CONFIG_PREFIX = "spring.cloud.ai.tongyi.images";
/**
* Default TongYi Chat model.
*/
public static final String DEFAULT_IMAGES_MODEL_NAME = ImageSynthesis.Models.WANX_V1;
/**
* Enable TongYiQWEN ai chat client.
*/
private boolean enabled = true;
@NestedConfigurationProperty
private TongYiImagesOptions options = TongYiImagesOptions.builder()
.withModel(DEFAULT_IMAGES_MODEL_NAME)
.withN(1)
.build();
public TongYiImagesOptions getOptions() {
return this.options;
}
public void setOptions(TongYiImagesOptions options) {
this.options = options;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

@ -0,0 +1,39 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.tongyi.exception;
/**
* TongYi models images exception.
*
* @author yuluo
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
*/
public class TongYiImagesException extends TongYiException {
public TongYiImagesException(String message) {
super(message);
}
public TongYiImagesException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,130 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.tongyi.metadata;
import java.util.Objects;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisTaskMetrics;
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisUsage;
import org.springframework.ai.image.ImageResponseMetadata;
import org.springframework.util.Assert;
/**
* @author yuluo
* @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
* @since 2023.0.0.0-RC1
*/
public class TongYiImagesResponseMetadata implements ImageResponseMetadata {
private final Long created;
private String taskId;
private ImageSynthesisTaskMetrics metrics;
private ImageSynthesisUsage usage;
public static TongYiImagesResponseMetadata from(ImageSynthesisResult synthesisResult) {
Assert.notNull(synthesisResult, "TongYiAiImageResponse must not be null");
return new TongYiImagesResponseMetadata(
System.currentTimeMillis(),
synthesisResult.getOutput().getTaskMetrics(),
synthesisResult.getOutput().getTaskId(),
synthesisResult.getUsage()
);
}
protected TongYiImagesResponseMetadata(
Long created,
ImageSynthesisTaskMetrics metrics,
String taskId,
ImageSynthesisUsage usage
) {
this.taskId = taskId;
this.metrics = metrics;
this.created = created;
this.usage = usage;
}
public ImageSynthesisUsage getUsage() {
return usage;
}
public void setUsage(ImageSynthesisUsage usage) {
this.usage = usage;
}
public Long getCreated() {
return created;
}
public String getTaskId() {
return taskId;
}
public void setTaskId(String taskId) {
this.taskId = taskId;
}
public ImageSynthesisTaskMetrics getMetrics() {
return metrics;
}
void setMetrics(ImageSynthesisTaskMetrics metrics) {
this.metrics = metrics;
}
@Override
public Long created() {
return this.created;
}
@Override
public String toString() {
return "TongYiImagesResponseMetadata {" + "created=" + created + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TongYiImagesResponseMetadata that = (TongYiImagesResponseMetadata) o;
return Objects.equals(created, that.created)
&& Objects.equals(taskId, that.taskId)
&& Objects.equals(metrics, that.metrics);
}
@Override
public int hashCode() {
return Objects.hash(created, taskId, metrics);
}
}
Loading…
Cancel
Save