From a14aebd5ee2a71b2548a8b71ab1cf74ea17f4efc Mon Sep 17 00:00:00 2001 From: cherishsince Date: Sun, 17 Mar 2024 11:19:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=20openai=20dall-e-3=EF=BC=8C?= =?UTF-8?q?=E5=8F=91=E9=80=81=E8=AF=B7=E6=B1=82=E8=A7=A3=E6=9E=90=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E5=AD=98=E5=9C=A8=E9=97=AE=E9=A2=98=EF=BC=8C=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E4=B8=8D=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/imageopenai/OpenAiImageApi.java | 54 ++++++++++++ .../ai/imageopenai/OpenAiImageClient.java | 82 +++++++++++++++++++ .../ai/imageopenai/OpenAiImageOptions.java | 77 +++++++++++++++++ .../imageopenai/api/OpenAiImageRequest.java | 58 +++++++++++++ .../imageopenai/api/OpenAiImageResponse.java | 28 +++++++ .../yudao/framework/ai/util/JacksonUtil.java | 79 ++++++++++++++++++ .../ai/chat/QianWenChatClientTests.java | 6 +- .../ai/image/OpenAiImageClientTests.java | 31 +++++++ 8 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageApi.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageClient.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageOptions.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageRequest.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageResponse.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/util/JacksonUtil.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageApi.java new file mode 100644 index 000000000..bcc03f2ee --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageApi.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.framework.ai.imageopenai; + +import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest; +import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse; +import cn.iocoder.yudao.framework.ai.util.JacksonUtil; +import io.netty.channel.ChannelOption; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; + +/** + * open ai image + *

+ * author: fansili + * time: 2024/3/17 09:53 + */ +public class OpenAiImageApi { + + private static final String DEFAULT_BASE_URL = "https://api.openai.com"; + private String apiKey = "your-api-key"; + // 发送请求 webClient + private final WebClient webClient; + + public OpenAiImageApi(String apiKey) { + this.apiKey = apiKey; + // 创建一个HttpClient实例并设置超时 + HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofSeconds(300)) // 设置响应超时时间为30秒 + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000 * 100); // 设置连接超时为5秒 + this.webClient = WebClient.builder() + .baseUrl(DEFAULT_BASE_URL) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + public OpenAiImageResponse createImage(OpenAiImageRequest request) { + String res = webClient.post() + .uri(uriBuilder -> uriBuilder.path("/v1/images/generations").build()) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + // 设置请求体(这里假设jsonStr是一个JSON格式的字符串) + .body(BodyInserters.fromValue(JacksonUtil.toJson(request))) + // 发送请求并获取响应体 + .retrieve() + // 转换响应体为String类型 + .bodyToMono(String.class) + .block(); + // TODO: 2024/3/17 这里发送请求会失败! + return null; + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageClient.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageClient.java new file mode 100644 index 000000000..a1bb59db1 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageClient.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.framework.ai.imageopenai; + +import cn.hutool.core.bean.BeanUtil; +import cn.iocoder.yudao.framework.ai.chat.ChatException; +import cn.iocoder.yudao.framework.ai.chatyiyan.exception.YiYanApiException; +import cn.iocoder.yudao.framework.ai.image.ImageClient; +import cn.iocoder.yudao.framework.ai.image.ImageOptions; +import cn.iocoder.yudao.framework.ai.image.ImagePrompt; +import cn.iocoder.yudao.framework.ai.image.ImageResponse; +import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest; +import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse; +import jdk.jfr.Frequency; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.retry.support.RetryTemplate; + +import java.time.Duration; + +/** + * open ai 生成 image + * + * author: fansili + * time: 2024/3/17 09:51 + */ +@Slf4j +public class OpenAiImageClient implements ImageClient { + + /** + * open image ai + */ + private OpenAiImageApi openAiImageApi; + /** + * 默认使用的 ImageOptions + */ + private OpenAiImageOptions defaultImageOptions; + + + public final RetryTemplate retryTemplate = RetryTemplate.builder() + // 最大重试次数 10 + .maxAttempts(10) + .retryOn(YiYanApiException.class) + // 最大重试5次,第一次间隔3000ms,第二次3000ms * 2,第三次3000ms * 3,以此类推,最大间隔3 * 60000ms + .exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000)) + .withListener(new RetryListener() { + @Override + public void onError(RetryContext context, + RetryCallback callback, Throwable throwable) { + log.warn("重试异常:" + context.getRetryCount(), throwable); + }; + }) + .build(); + + public OpenAiImageClient(OpenAiImageApi openAiImageApi, OpenAiImageOptions defaultImageOptions) { + this.openAiImageApi = openAiImageApi; + this.defaultImageOptions = defaultImageOptions; + } + + @Override + public ImageResponse call(ImagePrompt imagePrompt) { + return this.retryTemplate.execute(ctx -> { + // 检查是否配置了 OpenAiImageOptions + if (defaultImageOptions == null && imagePrompt.getOptions() == null) { + throw new ChatException("OpenAiImageOptions 未配置参数!"); + } + // 优先使用 request 中的 ImageOptions + ImageOptions useImageOptions = imagePrompt.getOptions() == null ? defaultImageOptions : imagePrompt.getOptions(); + if (!(useImageOptions instanceof OpenAiImageOptions)) { + throw new ChatException("配置信息不正确,传入的必须是 OpenAiImageOptions!"); + } + // 转换 OpenAiImageOptions + OpenAiImageOptions openAiImageOptions = (OpenAiImageOptions) useImageOptions; + // 创建请求 + OpenAiImageRequest request = new OpenAiImageRequest(); + BeanUtil.copyProperties(openAiImageOptions, request); + // 发送请求 + OpenAiImageResponse response = openAiImageApi.createImage(request); + return null; + }); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageOptions.java new file mode 100644 index 000000000..f18ccd298 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageOptions.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.framework.ai.imageopenai; + +import cn.iocoder.yudao.framework.ai.image.ImageOptions; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * open ai 配置文件 + * + * 文档地址:https://platform.openai.com/docs/api-reference/images/create + * + * author: fansili + * time: 2024/3/17 09:53 + */ +@Data +@Accessors(chain = true) +public class OpenAiImageOptions implements ImageOptions { + + // 必填字段,用于描述期望生成图像的文字说明。对于dall-e-2模型最大长度为1000个字符,对于dall-e-3模型最大长度为4000个字符。 + private String prompt; + + // 可选字段,默认为dall-e-2 + // 指定用于生成图像的模型名称。 + private String model = "dall-e-2"; + + // 可选字段,默认为1 + // 生成图像的数量,必须在1到10之间。对于dall-e-3模型,目前仅支持n=1。 + private Integer n = 1; + + // 可选字段,默认为standard + // 设置生成图像的质量。hd质量将创建细节更丰富、图像整体一致性更高的图片。该参数仅对dall-e-3模型有效。 + private String quality = "standard"; + + // 可选字段,默认为url + // 返回生成图像的格式。必须是url或b64_json中的一种。URL链接的有效期是从生成图像后开始计算的60分钟内有效。 + private String responseFormat = "url"; + + // 可选字段,默认为1024x1024 + // 生成图像的尺寸大小。对于dall-e-2模型,尺寸可为256x256, 512x512, 或 1024x1024。对于dall-e-3模型,尺寸可为1024x1024, 1792x1024, 或 1024x1792。 + private String imageSize = "1024x1024"; + + // 可选字段,默认为vivid + // 图像生成的风格。可为vivid(生动)或natural(自然)。vivid会使模型偏向生成超现实和戏剧性的图像,而natural则会让模型产出更自然、不那么超现实的图像。该参数仅对dall-e-3模型有效。 + private String style = "vivid"; + + // 可选字段 + // 代表您的终端用户的唯一标识符,有助于OpenAI监控并检测滥用行为。了解更多信息请参考官方文档。 + private String endUserId; + + // + // 适配 spring ai + + @Override + public Integer getN() { + return this.n; + } + + @Override + public String getModel() { + return this.model; + } + + @Override + public Integer getWidth() { + return null; + } + + @Override + public Integer getHeight() { + return null; + } + + @Override + public String getResponseFormat() { + return this.responseFormat; + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageRequest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageRequest.java new file mode 100644 index 000000000..865dbd136 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageRequest.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.framework.ai.imageopenai.api; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * open ai 配置文件 + * + * 文档地址:https://platform.openai.com/docs/api-reference/images/create + * + * author: fansili + * time: 2024/3/17 09:53 + */ +@Data +@Accessors(chain = true) +public class OpenAiImageRequest { + + // 必填字段,用于描述期望生成图像的文字说明。对于dall-e-2模型最大长度为1000个字符,对于dall-e-3模型最大长度为4000个字符。 + @JsonProperty("prompt") + private String prompt; + + // 可选字段,默认为dall-e-2、dall-e-3 + // 指定用于生成图像的模型名称。 + @JsonProperty("model") + private String model = "dall-e-2"; + + // 可选字段,默认为1 + // 生成图像的数量,必须在1到10之间。对于dall-e-3模型,目前仅支持n=1。 + @JsonProperty("n") + private Integer n = 1; + + // 可选字段,默认为standard + // 设置生成图像的质量。hd质量将创建细节更丰富、图像整体一致性更高的图片。该参数仅对dall-e-3模型有效。 + @JsonProperty("quality") + private String quality = "standard"; + + // 可选字段,默认为url + // 返回生成图像的格式。必须是url或b64_json中的一种。URL链接的有效期是从生成图像后开始计算的60分钟内有效。 + @JsonProperty("response_format") + private String responseFormat = "url"; + + // 可选字段,默认为1024x1024 + // 生成图像的尺寸大小。对于dall-e-2模型,尺寸可为256x256, 512x512, 或 1024x1024。对于dall-e-3模型,尺寸可为1024x1024, 1792x1024, 或 1024x1792。 + @JsonProperty("size") + private String imageSize = "1024x1024"; + + // 可选字段,默认为vivid + // 图像生成的风格。可为vivid(生动)或natural(自然)。vivid会使模型偏向生成超现实和戏剧性的图像,而natural则会让模型产出更自然、不那么超现实的图像。该参数仅对dall-e-3模型有效。 + @JsonProperty("style") + private String style = "vivid"; + + // 可选字段 + // 代表您的终端用户的唯一标识符,有助于OpenAI监控并检测滥用行为。了解更多信息请参考官方文档。 + @JsonProperty("user") + private String endUserId; + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageResponse.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageResponse.java new file mode 100644 index 000000000..04de1494d --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageResponse.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.ai.imageopenai.api; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * image 返回 + * + * author: fansili + * time: 2024/3/17 10:27 + */ +@Data +@Accessors(chain = true) +public class OpenAiImageResponse { + + private long created; + private List data; + + @Data + @Accessors(chain = true) + public static class Item { + + private String url; + + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/util/JacksonUtil.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/util/JacksonUtil.java new file mode 100644 index 000000000..046a481dc --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/util/JacksonUtil.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.framework.ai.util; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; + +import java.io.IOException; + +/** + * Jackson工具类 + * + * author: fansili + * time: 2024/3/17 10:13 + */ +public class JacksonUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 初始化 ObjectMapper 以美化输出(即格式化JSON内容) + */ + static { + // 美化输出(缩进) + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + // 忽略值为 null 的属性 + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + // 配置一个模块来将 Long 类型转换为 String 类型 + SimpleModule module = new SimpleModule(); + module.addSerializer(Long.class, ToStringSerializer.instance); + objectMapper.registerModule(module); + } + + /** + * 将对象转换为 JSON 字符串 + * + * @param obj 需要序列化的Java对象 + * @return 序列化后的 JSON 字符串 + * @throws JsonProcessingException 当 JSON 序列化过程中出现错误时抛出异常 + */ + public static String toJson(Object obj) { + try { + return objectMapper.writeValueAsString(obj); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + /** + * 将 JSON 字符串反序列化为指定类型的对象 + * + * @param json JSON 字符串 + * @param clazz 目标类型 Class 对象 + * @param 泛型类型参数 + * @return 反序列化后的 Java 对象 + * @throws IOException 当 JSON 解析过程中出现错误时抛出异常 + */ + public static T fromJson(String json, Class clazz) { + try { + return objectMapper.readValue(json, clazz); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + /** + * 将对象转换为格式化的 JSON 字符串(已启用 INDENT_OUTPUT 功能,所以所有方法都会返回格式化后的 JSON) + * + * @param obj 需要序列化的Java对象 + * @return 格式化后的 JSON 字符串 + * @throws JsonProcessingException 当 JSON 序列化过程中出现错误时抛出异常 + */ + public static String toFormattedJson(Object obj) { + // 已在类初始化时设置了 SerializationFeature.INDENT_OUTPUT,此处无需额外操作 + return toJson(obj); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java index 2c2b44685..14b839747 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java @@ -23,9 +23,9 @@ public class QianWenChatClientTests { @Before public void setup() { QianWenApi qianWenApi = new QianWenApi( - "", - "", - "", + "LTAI5tNTVhXW4fLKUjMrr98z", + "ZJ0JQeyjzxxm5CfeTV6k1wNE9UsvZP", + "f0c1088824594f589c8f10567ccd929f_p_efm", null ); qianWenChatClient = new QianWenChatClient( diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java new file mode 100644 index 000000000..83d556930 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.framework.ai.image; + +import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageApi; +import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageClient; +import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageOptions; +import org.junit.Before; +import org.junit.Test; + +/** + * author: fansili + * time: 2024/3/17 10:40 + */ +public class OpenAiImageClientTests { + + + private OpenAiImageClient openAiImageClient; + + @Before + public void setup() { + // 初始化 openAiImageClient + this.openAiImageClient = new OpenAiImageClient( + new OpenAiImageApi(""), + new OpenAiImageOptions() + ); + } + + @Test + public void callTest() { + openAiImageClient.call(new ImagePrompt("我和我的小狗,一起在北极和企鹅玩排球。")); + } +}