Browse Source

对接 openai dall-e-3,发送请求解析地址存在问题,链接不上

cherishsince 1 year ago
parent
commit
a14aebd5ee

+ 54 - 0
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
+ * <p>
+ * 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;
+    }
+}

+ 82 - 0
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 <T extends Object, E extends Throwable> void onError(RetryContext context,
+                                                                            RetryCallback<T, E> 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;
+        });
+    }
+}

+ 77 - 0
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;
+    }
+}

+ 58 - 0
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;
+
+}

+ 28 - 0
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<Item> data;
+
+    @Data
+    @Accessors(chain = true)
+    public static class Item {
+
+        private String url;
+
+    }
+}

+ 79 - 0
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 <T>   泛型类型参数
+     * @return 反序列化后的 Java 对象
+     * @throws IOException 当 JSON 解析过程中出现错误时抛出异常
+     */
+    public static <T> T fromJson(String json, Class<T> 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);
+    }
+}

+ 3 - 3
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(

+ 31 - 0
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("我和我的小狗,一起在北极和企鹅玩排球。"));
+    }
+}