Commit a14aebd5ee2a71b2548a8b71ab1cf74ea17f4efc

Authored by cherishsince
1 parent 633b1126

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

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,9 +23,9 @@ public class QianWenChatClientTests {
23 @Before 23 @Before
24 public void setup() { 24 public void setup() {
25 QianWenApi qianWenApi = new QianWenApi( 25 QianWenApi qianWenApi = new QianWenApi(
26 - "",  
27 - "",  
28 - "", 26 + "LTAI5tNTVhXW4fLKUjMrr98z",
  27 + "ZJ0JQeyjzxxm5CfeTV6k1wNE9UsvZP",
  28 + "f0c1088824594f589c8f10567ccd929f_p_efm",
29 null 29 null
30 ); 30 );
31 qianWenChatClient = new QianWenChatClient( 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 +}