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