From 2d36ec485832d02d14d0d92fd45cb561210738a4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 6 Jul 2024 08:56:36 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E3=80=91AI=EF=BC=9A=E4=BC=98=E5=8C=96=20xinghuo=20=E7=9A=84?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=EF=BC=8C=E5=A4=8D=E7=94=A8=20OpenAI=EF=BC=8C?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E5=B0=86=20ws=20=E6=9B=BF=E6=8D=A2=E6=88=90?= =?UTF-8?q?=20htt=20=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao-spring-boot-starter-ai/pom.xml | 9 +- .../ai/config/YudaoAiAutoConfiguration.java | 26 +-- .../ai/config/YudaoAiProperties.java | 56 ++--- .../ai/core/factory/AiClientFactoryImpl.java | 5 +- .../core/model/xinghuo/XingHuoChatClient.java | 217 ++++++++++-------- .../core/model/xinghuo/XingHuoChatModel.java | 53 ----- .../model/xinghuo/XingHuoChatOptions.java | 55 +++++ .../ai/core/model/xinghuo/XingHuoOptions.java | 77 ------- .../ai/core/model/xinghuo/api/XingHuoApi.java | 149 ------------ .../xinghuo/api/XingHuoChatCompletion.java | 48 ---- .../api/XingHuoChatCompletionMessage.java | 8 - .../api/XingHuoChatCompletionRequest.java | 107 --------- .../yudao/framework/ai/core/util/AiUtils.java | 6 +- .../ai/chat/XingHuoChatClientMainTests.java | 118 ---------- .../ai/chat/XingHuoChatClientTests.java | 72 +++--- .../framework/ai/chat/XingHuoOkHttpTests.java | 131 ----------- .../ai/image/OpenAiImageClientTests.java | 6 +- .../yudao/framework/ai/music/SunoTests.java | 13 +- .../src/main/resources/application.yaml | 54 ++--- 19 files changed, 259 insertions(+), 951 deletions(-) delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoOptions.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoApi.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletion.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionMessage.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionRequest.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index fff4dc052..8bf489686 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -33,11 +33,6 @@ spring-ai-stability-ai-spring-boot-starter ${spring-ai.version} - - - - - cn.iocoder.boot @@ -61,8 +56,8 @@ - junit - junit + org.springframework.boot + spring-boot-starter-test test diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 0f21a3c5b..c311ea95e 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -5,8 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi; +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -36,21 +35,14 @@ public class YudaoAiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") public XingHuoChatClient xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.XingHuoProperties xingHuoProperties = yudaoAiProperties.getXinghuo(); - // 转换配置 - XingHuoOptions xingHuoOptions = new XingHuoOptions(); - xingHuoOptions.setChatModel(xingHuoProperties.getModel()); - xingHuoOptions.setTopK(xingHuoProperties.getTopK()); - xingHuoOptions.setTemperature(xingHuoProperties.getTemperature()); - xingHuoOptions.setMaxTokens(xingHuoProperties.getMaxTokens()); - return new XingHuoChatClient( - new XingHuoApi( - xingHuoProperties.getAppId(), - xingHuoProperties.getAppKey(), - xingHuoProperties.getSecretKey() - ), - xingHuoOptions - ); + YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); + XingHuoChatOptions options = XingHuoChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topK(properties.getTopK()) + .build(); + return new XingHuoChatClient(properties.getAppKey(), properties.getSecretKey(), options); } @Bean diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index ac9459acf..dcc5e7411 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -1,55 +1,45 @@ package cn.iocoder.yudao.framework.ai.config; -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; import lombok.Data; -import lombok.experimental.Accessors; -import org.springframework.ai.autoconfigure.openai.OpenAiImageProperties; import org.springframework.boot.context.properties.ConfigurationProperties; /** - * ai 自动配置 + * 芋道 AI 配置类 * * @author fansili - * @time 2024/4/12 16:29 * @since 1.0 */ -@Data -@Accessors(chain = true) @ConfigurationProperties(prefix = "yudao.ai") +@Data public class YudaoAiProperties { + /** + * 讯飞星火 + */ private XingHuoProperties xinghuo; - private OpenAiImageProperties openAiImage; + + /** + * Midjourney 绘图 + */ private MidjourneyProperties midjourney; + + /** + * Suno 音乐 + */ private SunoProperties suno; @Data - @Accessors(chain = true) - public static class ChatProperties { - - private boolean enable = false; - - private AiPlatformEnum aiPlatform; - - private Float temperature; - - private Float topP; - - private Integer topK; - /** - * 用于限制模型生成token的数量,max_tokens设置的是生成上限,并不表示一定会生成这么多的token数量 - */ - private Integer maxTokens; - } - - @Data - public static class XingHuoProperties extends ChatProperties { + public static class XingHuoProperties { + private String enable; private String appId; private String appKey; private String secretKey; - private XingHuoChatModel model; + + private String model; + private Float temperature; + private Integer maxTokens; + private Integer topK; } @@ -57,8 +47,9 @@ public class YudaoAiProperties { public static class MidjourneyProperties { private String enable; - private String apiKey; private String baseUrl; + + private String apiKey; private String notifyUrl; } @@ -68,9 +59,6 @@ public class YudaoAiProperties { private boolean enable = false; - /** - * API 服务的基本地址 - */ private String baseUrl; } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java index 0ab34fdf8..a6252bd04 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java @@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi; import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; @@ -178,11 +177,9 @@ public class AiClientFactoryImpl implements AiClientFactory { private static XingHuoChatClient buildXingHuoChatClient(String key) { List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); - String appId = keys.get(0); String appKey = keys.get(1); String secretKey = keys.get(2); - XingHuoApi xingHuoApi = new XingHuoApi(appId, appKey, secretKey); - return new XingHuoChatClient(xingHuoApi); + return new XingHuoChatClient(appKey, secretKey); } /** diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatClient.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatClient.java index 9c4e52c3a..52b58c6e8 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatClient.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatClient.java @@ -1,145 +1,162 @@ package cn.iocoder.yudao.framework.ai.core.model.xinghuo; -import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.exceptions.ExceptionUtil; -import cn.iocoder.yudao.framework.ai.core.exception.ChatException; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest; +import cn.hutool.core.lang.Assert; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.metadata.ChatGenerationMetadata; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; -import org.springframework.ai.chat.model.StreamingChatModel; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata; +import org.springframework.ai.retry.RetryUtils; import org.springframework.http.ResponseEntity; -import org.springframework.retry.RetryCallback; -import org.springframework.retry.RetryContext; -import org.springframework.retry.RetryListener; import org.springframework.retry.support.RetryTemplate; import reactor.core.publisher.Flux; -import java.time.Duration; +import java.util.HashMap; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; + +import static cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions.MODEL_DEFAULT; -// TODO @fan:参考 yiyan 的修改建议,调整下 xinghuo 的实现;可以等 yiyan 修改完建议,然后我 review 完,再改这个哈; /** - * 讯飞星火 client - *

- * author: fansili - * time: 2024/3/11 10:19 + * 讯飞星火 {@link ChatModel} 实现类 + * + * @author fansili */ @Slf4j -public class XingHuoChatClient implements ChatModel, StreamingChatModel { +public class XingHuoChatClient implements ChatModel { - private XingHuoApi xingHuoApi; + private static final String BASE_URL = "https://spark-api-open.xf-yun.com"; - private XingHuoOptions xingHuoOptions; + private final XingHuoChatOptions defaultOptions; + private final RetryTemplate retryTemplate; - public final RetryTemplate retryTemplate = RetryTemplate.builder() - // 最大重试次数 10 - .maxAttempts(3) - .retryOn(ChatException.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) { - System.err.println("正在重试... " + ExceptionUtil.getMessage(throwable)); - log.warn("重试异常:" + context.getRetryCount(), throwable); - } + /** + * 星火兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本 + * + * 不过要注意,星火没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它 + */ + private final OpenAiApi openAiApi; - ; - }) - .build(); - - public XingHuoChatClient(XingHuoApi xingHuoApi) { - this.xingHuoApi = xingHuoApi; + public XingHuoChatClient(String apiKey, String secretKey) { + this(apiKey, secretKey, + XingHuoChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build()); } - public XingHuoChatClient(XingHuoApi xingHuoApi, XingHuoOptions xingHuoOptions) { - this.xingHuoApi = xingHuoApi; - this.xingHuoOptions = xingHuoOptions; + public XingHuoChatClient(String apiKey, String secretKey, XingHuoChatOptions options) { + this(apiKey, secretKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); + } + + public XingHuoChatClient(String apiKey, String secretKey, XingHuoChatOptions options, RetryTemplate retryTemplate) { + Assert.notEmpty(apiKey, "apiKey 不能为空"); + Assert.notEmpty(secretKey, "secretKey 不能为空"); + Assert.notNull(options, "options 不能为空"); + Assert.notNull(retryTemplate, "retryTemplate 不能为空"); + this.openAiApi = new OpenAiApi(BASE_URL, apiKey + ":" + secretKey); + this.defaultOptions = options; + this.retryTemplate = retryTemplate; } @Override public ChatResponse call(Prompt prompt) { + OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false); return this.retryTemplate.execute(ctx -> { - // ctx 会有重试的信息 - // 获取 chatOptions 属性 - XingHuoOptions chatOptions = this.getChatOptions(prompt); - // 创建 request 请求,stream模式需要供应商支持 - XingHuoChatCompletionRequest request = this.createRequest(prompt, chatOptions); - // 调用 callWithFunctionSupport 发送请求 - ResponseEntity response = xingHuoApi.chatCompletionEntity(request, chatOptions.getChatModel()); - // 获取结果封装 ChatResponse - return new ChatResponse(List.of(new Generation(response.getBody().getPayload().getChoices().getText().get(0).getContent()))); + // 1.1 发起调用 + ResponseEntity completionEntity = openAiApi.chatCompletionEntity(request); + // 1.2 校验结果 + OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody(); + if (chatCompletion == null) { + log.warn("No chat completion returned for prompt: {}", prompt); + return new ChatResponse(List.of()); + } + List choices = chatCompletion.choices(); + if (choices == null) { + log.warn("No choices returned for prompt: {}", prompt); + return new ChatResponse(List.of()); + } + + // 2. 转换 ChatResponse 返回 + List generations = choices.stream().map(choice -> { + Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice)); + if (choice.finishReason() != null) { + generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)); + } + return generation; + }).toList(); + return new ChatResponse(generations, + OpenAiChatResponseMetadata.from(completionEntity.getBody())); }); } - @Override - public ChatOptions getDefaultOptions() { - // TODO 芋艿:需要跟进下 - throw new UnsupportedOperationException(); + private Map toMap(String id, OpenAiApi.ChatCompletion.Choice choice) { + Map map = new HashMap<>(); + OpenAiApi.ChatCompletionMessage message = choice.message(); + if (message.role() != null) { + map.put("role", message.role().name()); + } + if (choice.finishReason() != null) { + map.put("finishReason", choice.finishReason().name()); + } + map.put("id", id); + return map; } @Override public Flux stream(Prompt prompt) { - // 获取 chatOptions 属性 - XingHuoOptions chatOptions = this.getChatOptions(prompt); - // 创建 request 请求,stream模式需要供应商支持 - XingHuoChatCompletionRequest request = this.createRequest(prompt, chatOptions); - // 发送请求 - Flux response = this.xingHuoApi.chatCompletionStream(request, chatOptions.getChatModel()); - return response.map(res -> { - String content = res.getPayload().getChoices().getText().stream() - .map(item -> item.getContent()).collect(Collectors.joining()); - return new ChatResponse(List.of(new Generation(content))); + OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true); + return this.retryTemplate.execute(ctx -> { + // 1. 发起调用 + Flux response = this.openAiApi.chatCompletionStream(request); + return response.map(chatCompletion -> { + String id = chatCompletion.id(); + // 2. 转换 ChatResponse 返回 + List generations = chatCompletion.choices().stream().map(choice -> { + String finish = (choice.finishReason() != null ? choice.finishReason().name() : ""); + Generation generation = new Generation(choice.delta().content(), + Map.of("id", id, "role", choice.delta().role().name(), "finishReason", finish)); + if (choice.finishReason() != null) { + generation = generation.withGenerationMetadata( + ChatGenerationMetadata.from(choice.finishReason().name(), null)); + } + return generation; + }).toList(); + return new ChatResponse(generations); + }); }); } - private XingHuoOptions getChatOptions(Prompt prompt) { - // 两个都为null 则没有配置文件 - if (xingHuoOptions == null && prompt.getOptions() == null) { - throw new ChatException("ChatOptions 未配置参数!"); - } - // 优先使用 Prompt 里面的 ChatOptions - ChatOptions options = xingHuoOptions; + OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) { + // 1. 构建 ChatCompletionMessage 对象 + List chatCompletionMessages = prompt.getInstructions().stream().map(m -> + new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList(); + OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream); + + // 2.1 补充 prompt 内置的 options if (prompt.getOptions() != null) { - options = (ChatOptions) prompt.getOptions(); + if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { + OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, + ChatOptions.class, OpenAiChatOptions.class); + request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class); + } else { + throw new IllegalArgumentException("Prompt options are not of type ChatOptions: " + + prompt.getOptions().getClass().getSimpleName()); + } } - // Prompt 里面是一个 ChatOptions,用户可以随意传入,这里做一下判断 - if (!(options instanceof XingHuoOptions)) { - throw new ChatException("Prompt 传入的不是 XingHuoOptions!"); + // 2.2 补充默认 options + if (this.defaultOptions != null) { + request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class); } - return (XingHuoOptions) options; + return request; } - private XingHuoChatCompletionRequest createRequest(Prompt prompt, XingHuoOptions xingHuoOptions) { - // 创建 header - XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id(xingHuoApi.getAppId()); - // 创建 params - XingHuoChatCompletionRequest.Parameter.Chat chatParameter = new XingHuoChatCompletionRequest.Parameter.Chat(); - BeanUtil.copyProperties(xingHuoOptions, chatParameter); - chatParameter.setDomain(xingHuoOptions.getChatModel().getModel()); - XingHuoChatCompletionRequest.Parameter parameter = new XingHuoChatCompletionRequest.Parameter().setChat(chatParameter); - // 创建 payload text 信息 - List texts = prompt.getInstructions().stream().map(message -> { - XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text(); - text.setContent(message.getContent()); - text.setRole(message.getMessageType().getValue()); - return text; - }).collect(Collectors.toList()); - // 创建 payload - XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload() - .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(texts)); - // 创建 request - return new XingHuoChatCompletionRequest() - .setHeader(header) - .setParameter(parameter) - .setPayload(payload); + @Override + public ChatOptions getDefaultOptions() { + return XingHuoChatOptions.fromOptions(defaultOptions); } + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java deleted file mode 100644 index e9ef5ef35..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java +++ /dev/null @@ -1,53 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.xinghuo; - -import lombok.Getter; - -/** - * 讯飞星火 模型 - * - * 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E - * - * 1tokens 约等于1.5个中文汉字 或者 0.8个英文单词 - * 星火V1.5支持[搜索]内置插件;星火V2.0、V3.0和V3.5支持[搜索]、[天气]、[日期]、[诗词]、[字词]、[股票]六个内置插件 - * 星火V3.5 现已支持system、Function Call 功能。 - * - * author: fansili - * time: 2024/3/11 10:12 - */ -@Getter -public enum XingHuoChatModel { - -// 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E -// general指向V1.5版本; -// generalv2指向V2版本; -// generalv3指向V3版本; -// generalv3.5指向V3.5版本; - - XING_HUO_1_5("星火大模型1.5", "general", "/v1.1/chat"), - XING_HUO_2_0("星火大模型2.0", "generalv2", "/v2.1/chat"), - XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"), - XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"), - - ; - - XingHuoChatModel(String name, String model, String uri) { - this.name = name; - this.model = model; - this.uri = uri; - } - - private String name; - - private String model; - - private String uri; - - public static XingHuoChatModel valueOfModel(String model) { - for (XingHuoChatModel itemEnum : XingHuoChatModel.values()) { - if (itemEnum.getModel().equals(model)) { - return itemEnum; - } - } - throw new IllegalArgumentException("Invalid MessageType value: " + model); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java new file mode 100644 index 000000000..e3287b613 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.ai.core.model.xinghuo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.ai.chat.prompt.ChatOptions; + +/** + * 讯飞星火 {@link ChatOptions} 实现类 + * + * 参考文档:HTTP 调用 + * + * @author fansili + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class XingHuoChatOptions implements ChatOptions { + + public static final String MODEL_DEFAULT = "generalv3.5"; + + /** + * 模型 + */ + private String model; + /** + * 温度 + */ + private Float temperature; + /** + * 最大 Token + */ + private Integer maxTokens; + /** + * K 个候选 + */ + private Integer topK; + + @Override + public Float getTopP() { + return null; + } + + public static XingHuoChatOptions fromOptions(XingHuoChatOptions fromOptions) { + return XingHuoChatOptions.builder() + .model(fromOptions.getModel()) + .temperature(fromOptions.getTemperature()) + .maxTokens(fromOptions.getMaxTokens()) + .topK(fromOptions.getTopK()) + .build(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoOptions.java deleted file mode 100644 index ccec8598d..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoOptions.java +++ /dev/null @@ -1,77 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.xinghuo; - -import org.springframework.ai.chat.prompt.ChatOptions; -import lombok.Data; -import lombok.experimental.Accessors; - -/** - * 讯飞星火 - *

- * author: fansili - * time: 2024/3/16 20:29 - */ -@Data -@Accessors(chain = true) -public class XingHuoOptions implements ChatOptions { - - // TODO @fan:这里 model 参数,然后使用 string - /** - * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E - *

- * 指定访问的领域: - * general指向V1.5版本; - * generalv2指向V2版本; - * generalv3指向V3版本; - * generalv3.5指向V3.5版本; - * 注意:不同的取值对应的url也不一样! - */ - private XingHuoChatModel chatModel = XingHuoChatModel.XING_HUO_3_5; - /** - * 取值范围 (0,1] ,默认值0.5 - */ - private Float temperature; - /** - * V1.5取值为[1,4096] - * V2.0、V3.0和V3.5取值为[1,8192],默认为2048。 - */ - private Integer maxTokens; - /** - * 取值为[1,6],默认为4 - */ - private Integer topK; - /** - * 需要保障用户下的唯一性,用于关联用户会话 - */ - private String chatId; - - @Override - public Float getTemperature() { - return this.temperature; - } - -// @Override -// public void setTemperature(Float temperature) { -// this.temperature = temperature; -// } - - @Override - public Float getTopP() { - return null; - } - -// @Override -// public void setTopP(Float topP) { -// -// } - - @Override - public Integer getTopK() { - return this.topK; - } - -// @Override -// public void setTopK(Integer topK) { -// this.topK = topK; -// } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoApi.java deleted file mode 100644 index eeceb525a..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoApi.java +++ /dev/null @@ -1,149 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api; - -import cn.hutool.http.HttpUtil; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; -import lombok.Data; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ResponseEntity; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.net.URI; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.text.SimpleDateFormat; -import java.util.*; -import java.util.stream.Collectors; - -// TODO @fan:讯飞使用 spring websocket 接入,还是 okhttp?确认了,未使用的最好删除下,反正 git 也能找回 history -/** - * 讯飞星火 属性、api - *

- * 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E - *

- * author: fansili - * time: 2024/3/11 10:12 - */ -@Data -public class XingHuoApi { - - private static final String DEFAULT_BASE_URL = "wss://spark-api.xf-yun.com"; - - private String appId; - private String appKey; - private String secretKey; - private WebClient webClient; - // 创建 WebSocketClient 实例 - private ReactorNettyWebSocketClient socketClient = new ReactorNettyWebSocketClient(); - - public XingHuoApi(String appId, String appKey, String secretKey) { - this.appId = appId; - this.appKey = appKey; - this.secretKey = secretKey; - } - - public ResponseEntity chatCompletionEntity(XingHuoChatCompletionRequest request, XingHuoChatModel xingHuoChatModel) { - String authUrl; - try { -// XingHuoChatModel useChatModel; - authUrl = getAuthorizationUrl("spark-api.xf-yun.com", xingHuoChatModel.getUri()); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new RuntimeException(e); - } - // wss 请求的 URI - URI uri = URI.create(authUrl); - // 发起 wss 请求并处理响应 - Flux messageFlux = Flux.create(sink -> { - socketClient.execute(uri, session -> - session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request)))) - .thenMany(session.receive() - .map(WebSocketMessage -> { - return JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class); - }) - .doOnNext(sink::next) // 将接收到的消息推送到 Flux 中 - .doOnError(sink::error) // 处理错误 - .doOnTerminate(sink::complete)) // 完成时关闭 sink - .then()) - .subscribe(); // 订阅以开始会话 - }); - // 阻塞获取所有结果 - List responseList = messageFlux.collectList().block(); - // 拼接 content - String responseContent = responseList.stream().map(item -> { - // 获取 content - return item.getPayload().getChoices().getText().stream().map(XingHuoChatCompletion.Text::getContent).collect(Collectors.joining()); - }).collect(Collectors.joining()); - // 将多个合并成一个 - XingHuoChatCompletion xingHuoChatCompletion = new XingHuoChatCompletion(); - xingHuoChatCompletion.setPayload(new XingHuoChatCompletion.Payload().setChoices(new XingHuoChatCompletion.Choices().setText(List.of(new XingHuoChatCompletion.Text().setContent(responseContent))))); - return new ResponseEntity<>(xingHuoChatCompletion, HttpStatusCode.valueOf(200)); - } - - - /** - * 获取验证请求url - * - * @return - */ - public String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException { - // 获取鉴权时间 date - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - String date = format.format(new Date()); - - // 获取signature_origin字段 - StringBuilder builder = new StringBuilder("host: ").append(host).append("\n"). - append("date: ").append(date).append("\n"). - append("GET ").append(path).append(" HTTP/1.1"); - - // 获得signatue - Charset charset = Charset.forName("UTF-8"); - Mac mac = Mac.getInstance("hmacsha256"); - SecretKeySpec sp = new SecretKeySpec(secretKey.getBytes(charset), "hmacsha256"); - mac.init(sp); - byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset)); - String signature = Base64.getEncoder().encodeToString(basebefore); - //获得 authorization_origin - String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", appKey, "hmac-sha256", "host date request-line", signature); - //获得authorization - String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset)); - // 获取httpUrl - Map param = new HashMap<>(); - param.put("authorization", authorization); - param.put("date", date); - param.put("host", host); - - String toParams = HttpUtil.toParams(param); - return "wss://" + host + path + "?" + toParams; - } - - public Flux chatCompletionStream(XingHuoChatCompletionRequest request, XingHuoChatModel xingHuoChatModel) { - String authUrl; - try { - authUrl = getAuthorizationUrl("spark-api.xf-yun.com", xingHuoChatModel.getUri()); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new RuntimeException(e); - } - // wss 请求的 URI - URI uri = URI.create(authUrl); - // 发起 wss 请求并处理响应 - // 创建一个 Flux 来处理接收到的消息 - return Flux.create(sink -> { - socketClient.execute(uri, session -> - session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request)))) - .thenMany(session.receive() - .map(WebSocketMessage -> JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class)) - .doOnNext(sink::next) // 将接收到的消息推送到 Flux 中 - .doOnError(sink::error) // 处理错误 - .doOnTerminate(sink::complete)) // 完成时关闭 sink - .then()) - .subscribe(); // 订阅以开始会话 - }); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletion.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletion.java deleted file mode 100644 index 46aff8717..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletion.java +++ /dev/null @@ -1,48 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api; - -import lombok.Data; -import lombok.experimental.Accessors; - -import java.util.List; - -/** - * author: fansili - * time: 2024/3/11 10:20 - */ -@Data -@Accessors(chain = true) -public class XingHuoChatCompletion { - private Header header; - private Payload payload; - - @Data - @Accessors(chain = true) - public static class Header { - private int code; - private String message; - private String sid; - private int status; - } - - @Data - @Accessors(chain = true) - public static class Payload { - private Choices choices; - } - - @Data - @Accessors(chain = true) - public static class Choices { - private int status; - private int seq; - private List text; - } - - @Data - @Accessors(chain = true) - public static class Text { - private String content; - private String role; - private int index; - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionMessage.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionMessage.java deleted file mode 100644 index f527b10d4..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionMessage.java +++ /dev/null @@ -1,8 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api; - -/** - * author: fansili - * time: 2024/3/11 10:20 - */ -public class XingHuoChatCompletionMessage { -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionRequest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionRequest.java deleted file mode 100644 index f3ca1eb30..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionRequest.java +++ /dev/null @@ -1,107 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api; - -import lombok.Data; -import lombok.Getter; -import lombok.experimental.Accessors; - -import java.util.List; - -/** - * 讯飞星火 request - * - * author: fansili - * time: 2024/3/11 10:20 - */ -@Data -@Accessors(chain = true) -public class XingHuoChatCompletionRequest { - - private Header header; - private Parameter parameter; - private Payload payload; - - @Data - @Accessors(chain = true) - public static class Header { - private String app_id; - private String uid; - } - - @Data - @Accessors(chain = true) - public static class Parameter { - private Chat chat; - - @Data - @Accessors(chain = true) - public static class Chat { - /** - * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E - * - * 指定访问的领域: - * general指向V1.5版本; - * generalv2指向V2版本; - * generalv3指向V3版本; - * generalv3.5指向V3.5版本; - * 注意:不同的取值对应的url也不一样! - */ - private String domain = "generalv3.5"; - /** - * 取值范围 (0,1] ,默认值0.5 - */ - private Float temperature; - /** - * V1.5取值为[1,4096] - * V2.0、V3.0和V3.5取值为[1,8192],默认为2048。 - */ - private Integer max_tokens; - /** - * 取值为[1,6],默认为4 - */ - private Integer top_k; - /** - * 需要保障用户下的唯一性,用于关联用户会话 - */ - private String chat_id; - } - } - - @Data - @Accessors(chain = true) - public static class Payload { - private Message message; - - @Data - @Accessors(chain = true) - public static class Message { - private List text; - - - @Data - @Accessors(chain = true) - public static class Text { - /** - * 角色 - */ - private String role; - /** - * 消息内容 - */ - private String content; - private Integer index; - - @Getter - public static enum Role { - SYSTEM("system"), - USER("user"), - ASSISTANT("assistant"); - private String name; - - private Role(String name) { - this.name = name; - } - } - } - } - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index 306b216a3..f3f6f7d9b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -2,8 +2,7 @@ package cn.iocoder.yudao.framework.ai.core.util; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions; +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.prompt.ChatOptions; @@ -31,8 +30,7 @@ public class AiUtils { // return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build(); case XING_HUO: - return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF) - .setMaxTokens(maxTokens); + return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); case QIAN_WEN: return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build(); default: diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java deleted file mode 100644 index 22bb31ae0..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java +++ /dev/null @@ -1,118 +0,0 @@ -package cn.iocoder.yudao.framework.ai.chat; - -import cn.hutool.http.HttpUtil; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest; -import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; -import org.springframework.web.reactive.socket.client.WebSocketClient; -import reactor.core.publisher.Flux; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.net.MalformedURLException; -import java.net.URI; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.text.SimpleDateFormat; -import java.util.*; - -// TODO 芋艿:整理单测 -/** - * author: fansili - * time: 2024/3/13 20:47 - */ -public class XingHuoChatClientMainTests { - - - private static final String HOST_URL = "http://spark-api.xf-yun.com/v3.5/chat"; - private static final String API_KEY = "cb6415c19d6162cda07b47316fcb0416"; - private static final String API_SECRET = "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"; - - public static void main(String[] args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException { - String authUrl = getAuthorizationUrl("spark-api.xf-yun.com", "/v3.5/chat"); - System.err.println(authUrl); - - XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id("13c8cca6"); - XingHuoChatCompletionRequest.Parameter parameter - = new XingHuoChatCompletionRequest.Parameter() - .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain("generalv3.5")); - - - XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text(); - text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName()); - text.setContent("世界上最好的开发语言是什么?"); - XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload() - .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text))); - XingHuoChatCompletionRequest request = new XingHuoChatCompletionRequest() - .setHeader(header) - .setParameter(parameter) - .setPayload(payload); - - System.err.println(JSONUtil.toJsonPrettyStr(request)); - - - // 创建 WebSocketClient 实例 - WebSocketClient client = new ReactorNettyWebSocketClient(); - - // wss 请求的 URI - URI uri = URI.create(authUrl); - - // 发起 wss 请求并处理响应 - client.execute(uri, session -> - // 使用会话发送消息,并接收回应 - session.send(Flux.just(session.textMessage(JSONUtil.toJsonStr(request)))) - .thenMany(session.receive() - .map(WebSocketMessage -> { - System.err.println(WebSocketMessage.getPayloadAsText()); - return JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class); - }) - .log()) // 打印接收到的消息 - .then()) - .block(); // 等待操作完成或超时 - - // 阻止退出 - Scanner scanner = new Scanner(System.in); - scanner.nextLine(); - } - - - /** - * 获取验证请求url - * - * @return - */ - public static String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException { - // 获取鉴权时间 date - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - String date = format.format(new Date()); - - // 获取signature_origin字段 - StringBuilder builder = new StringBuilder("host: ").append(host).append("\n"). - append("date: ").append(date).append("\n"). - append("GET ").append(path).append(" HTTP/1.1"); - - // 获得signatue - Charset charset = Charset.forName("UTF-8"); - Mac mac = Mac.getInstance("hmacsha256"); - SecretKeySpec sp = new SecretKeySpec(API_SECRET.getBytes(charset), "hmacsha256"); - mac.init(sp); - byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset)); - String signature = Base64.getEncoder().encodeToString(basebefore); - //获得 authorization_origin - String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", API_KEY, "hmac-sha256", "host date request-line", signature); - //获得authorization - String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset)); - // 获取httpUrl - Map param = new HashMap<>(); - param.put("authorization", authorization); - param.put("date", date); - param.put("host", host); - - String toParams = HttpUtil.toParams(param); - return "wss://" + host + path + "?" + toParams; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java index 00f4c1d37..b55aecfdb 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java @@ -1,11 +1,7 @@ package cn.iocoder.yudao.framework.ai.chat; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; @@ -15,58 +11,44 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; -import java.util.Scanner; -import java.util.function.Consumer; -// TODO 芋艿:整理单测 /** - * 讯飞星火 tests - *

- * author: fansili - * time: 2024/3/11 11:00 + * {@link XingHuoChatClient} 集成测试 + * + * @author fansili */ public class XingHuoChatClientTests { - private XingHuoChatClient xingHuoChatClient; + private final XingHuoChatClient client = new XingHuoChatClient( + "cb6415c19d6162cda07b47316fcb0416", + "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"); - @Before - public void setup() { - // 初始化 xingHuoChatClient - xingHuoChatClient = new XingHuoChatClient( - new XingHuoApi( - "13c8cca6", - "cb6415c19d6162cda07b47316fcb0416", - "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh" - ), - new XingHuoOptions().setChatModel(XingHuoChatModel.XING_HUO_3_5) - ); + @Test + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = client.call(new Prompt(messages)); + // 打印结果 + System.err.println(response); } @Test - public void callTest() { + public void testStream() { + // 准备参数 List messages = new ArrayList<>(); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("长沙怎么样?")); + messages.add(new UserMessage("1 + 1 = ?")); - ChatResponse call = xingHuoChatClient.call(new Prompt(messages)); - System.err.println(call.getResult()); + // 调用 + Flux flux = client.stream(new Prompt(messages)); + // 打印结果 + List responses = flux.collectList().block(); + assert responses != null; + responses.forEach(System.err::println); } - @Test - public void streamTest() { - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("长沙怎么样?")); - - Flux stream = xingHuoChatClient.stream(new Prompt(messages)); - stream.subscribe(new Consumer() { - @Override - public void accept(ChatResponse chatResponse) { - System.err.print(chatResponse.getResult().getOutput().getContent()); - } - }); - // 阻止退出 - Scanner scanner = new Scanner(System.in); - scanner.nextLine(); - } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java deleted file mode 100644 index c0d4211e3..000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java +++ /dev/null @@ -1,131 +0,0 @@ -package cn.iocoder.yudao.framework.ai.chat; - -import cn.hutool.http.HttpUtil; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest; -import okhttp3.*; -import org.jetbrains.annotations.NotNull; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.net.MalformedURLException; -import java.nio.charset.Charset; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.text.SimpleDateFormat; -import java.util.*; - -// TODO 芋艿:整理单测 -/** - * 讯飞星火 tests - *

- * author: fansili - * time: 2024/3/11 11:00 - */ -public class XingHuoOkHttpTests { - - private static final String HOST_URL = "http://spark-api.xf-yun.com/v3.5/chat"; - private static final String API_KEY = "cb6415c19d6162cda07b47316fcb0416"; - private static final String API_SECRET = "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"; - - private XingHuoChatClient xingHuoChatClient; - - public static void main(String[] args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException { - String authUrl = getAuthorizationUrl("spark-api.xf-yun.com", "/v3.5/chat"); - System.err.println(authUrl); - - XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id("13c8cca6"); - XingHuoChatCompletionRequest.Parameter parameter - = new XingHuoChatCompletionRequest.Parameter() - .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain("generalv3.5")); - - - XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text(); - text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName()); - text.setContent("世界上最好的开发语言是什么?"); - XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload() - .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text))); - XingHuoChatCompletionRequest request = new XingHuoChatCompletionRequest() - .setHeader(header) - .setParameter(parameter) - .setPayload(payload); - - System.err.println(JSONUtil.toJsonPrettyStr(request)); - - OkHttpClient client = new OkHttpClient(); - Request request2 = new Request.Builder() - .url(authUrl) // 替换为你的 wss URL - .build(); - - WebSocketListener webSocketListener = new WebSocketListener() { - - @Override - public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { - boolean send = webSocket.send(JSONUtil.toJsonStr(request)); - System.err.println("发送 -> " + send); - System.err.println("链接成功!"); - } - - @Override - public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { - super.onMessage(webSocket, text); -// System.err.println(text); - XingHuoChatCompletion response = JSONUtil.toBean(text, XingHuoChatCompletion.class); - for (XingHuoChatCompletion.Text text1 : response.getPayload().getChoices().getText()) { - System.err.print(text1.getContent()); - } - } - }; - - WebSocket webSocket = client.newWebSocket(request2, webSocketListener); -// webSocket.send(JSONUtil.toJsonStr(request)); - - - // Trigger shutdown of the dispatcher's executor so this process can exit cleanly. - client.dispatcher().executorService().shutdown(); - // 阻止退出 - Scanner scanner = new Scanner(System.in); - scanner.nextLine(); - } - - - /** - * 获取验证请求url - * - * @return - */ - public static String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException { - // 获取鉴权时间 date - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - String date = format.format(new Date()); - - // 获取signature_origin字段 - StringBuilder builder = new StringBuilder("host: ").append(host).append("\n"). - append("date: ").append(date).append("\n"). - append("GET ").append(path).append(" HTTP/1.1"); - - // 获得signatue - Charset charset = Charset.forName("UTF-8"); - Mac mac = Mac.getInstance("hmacsha256"); - SecretKeySpec sp = new SecretKeySpec(API_SECRET.getBytes(charset), "hmacsha256"); - mac.init(sp); - byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset)); - String signature = Base64.getEncoder().encodeToString(basebefore); - //获得 authorization_origin - String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", API_KEY, "hmac-sha256", "host date request-line", signature); - //获得authorization - String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset)); - // 获取httpUrl - Map param = new HashMap<>(); - param.put("authorization", authorization); - param.put("date", date); - param.put("host", host); - - String toParams = HttpUtil.toParams(param); - return "wss://" + host + path + "?" + toParams; - } - -} 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 index 7c73b2de2..f942f25bf 100644 --- 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 @@ -1,9 +1,9 @@ package cn.iocoder.yudao.framework.ai.image; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; -import org.junit.Before; -import org.junit.Test; import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.api.OpenAiImageApi; @@ -25,7 +25,7 @@ public class OpenAiImageClientTests { private OpenAiImageModel openAiImageClient; - @Before + @BeforeEach public void setup() { // 初始化 openAiImageClient this.openAiImageClient = new OpenAiImageModel( diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java index e5bf6be38..f6df134ba 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java @@ -1,8 +1,7 @@ package cn.iocoder.yudao.framework.ai.music; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; @@ -13,14 +12,8 @@ import java.util.List; */ public class SunoTests { - private SunoApi sunoApi; - - @Before - public void setup() { - String url = "https://suno-55ishh05u-status2xxs-projects.vercel.app"; -// String url = "http://127.0.0.1:3001"; - this.sunoApi = new SunoApi(url); - } + private final SunoApi sunoApi = new SunoApi("https://suno-55ishh05u-status2xxs-projects.vercel.app"); +// private final SunoApi sunoApi = new SunoApi("http://127.0.0.1:3001"); @Test public void selectById() { diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index b2e2ddcd7..8c325b88d 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -162,11 +162,6 @@ spring: base-url: https://api.gptsapi.net stabilityai: api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx - vertex: - ai: - gemini: - project-id: 1 # TODO 芋艿:缺配置 - location: 2 qianfan: # 文心一言 api-key: x0cuLZ7XsaTCU08vuJWO87Lg secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK @@ -176,37 +171,24 @@ spring: tongyi: api-key: sk-Zsd81gZYg7 -yudao.ai: - xinghuo: - enable: true - aiPlatform: XING_HUO # TODO @fan:建议每个都独立配置属性类 - max-tokens: 1500 - temperature: 0.85 - topP: 0.8 - topK: 0 - appId: 13c8cca6 - appKey: cb6415c19d6162cda07b47316fcb0416 - secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh - model: XING_HUO_3_5 - qianwen: - enable: true - aiPlatform: QIAN_WEN - max-tokens: 1500 - temperature: 0.85 - topP: 0.8 - topK: 0 - api-key: sk-Zsd81gZYg7 - model: QWEN_TURBO - midjourney: - enable: true -# base-url: https://api.holdai.top/mj-relax/mj - base-url: https://api.holdai.top/mj - api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf - notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify - suno: - enable: true -# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app - base-url: http://127.0.0.1:3001 +yudao: + ai: + xinghuo: + enable: true + appId: 13c8cca6 + appKey: cb6415c19d6162cda07b47316fcb0416 + secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh + model: generalv3.5 + midjourney: + enable: true + # base-url: https://api.holdai.top/mj-relax/mj + base-url: https://api.holdai.top/mj + api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf + notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify + suno: + enable: true + # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app + base-url: http://127.0.0.1:3001 --- #################### 芋道相关配置 ####################