Commit 2d36ec485832d02d14d0d92fd45cb561210738a4

Authored by YunaiV
1 parent 2e73959a

【代码优化】AI:优化 xinghuo 的接入,复用 OpenAI,同时将 ws 替换成 htt 调用

Showing 19 changed files with 259 additions and 951 deletions
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
... ... @@ -33,11 +33,6 @@
33 33 <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId>
34 34 <version>${spring-ai.version}</version>
35 35 </dependency>
36   -<!-- <dependency>-->
37   -<!-- <groupId>org.springframework.ai</groupId>-->
38   -<!-- <artifactId>spring-ai-vertex-ai-gemini</artifactId>-->
39   -<!-- <version>1.0.3</version>-->
40   -<!-- </dependency>-->
41 36  
42 37 <dependency>
43 38 <groupId>cn.iocoder.boot</groupId>
... ... @@ -61,8 +56,8 @@
61 56  
62 57 <!-- Test 测试相关 -->
63 58 <dependency>
64   - <groupId>junit</groupId>
65   - <artifactId>junit</artifactId>
  59 + <groupId>org.springframework.boot</groupId>
  60 + <artifactId>spring-boot-starter-test</artifactId>
66 61 <scope>test</scope>
67 62 </dependency>
68 63 </dependencies>
... ...
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;
5 5 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
6 6 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
7 7 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
8   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
9   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
  8 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
10 9 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
11 10 import lombok.extern.slf4j.Slf4j;
12 11 import org.springframework.boot.autoconfigure.AutoConfiguration;
... ... @@ -36,21 +35,14 @@ public class YudaoAiAutoConfiguration {
36 35 @Bean
37 36 @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
38 37 public XingHuoChatClient xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
39   - YudaoAiProperties.XingHuoProperties xingHuoProperties = yudaoAiProperties.getXinghuo();
40   - // 转换配置
41   - XingHuoOptions xingHuoOptions = new XingHuoOptions();
42   - xingHuoOptions.setChatModel(xingHuoProperties.getModel());
43   - xingHuoOptions.setTopK(xingHuoProperties.getTopK());
44   - xingHuoOptions.setTemperature(xingHuoProperties.getTemperature());
45   - xingHuoOptions.setMaxTokens(xingHuoProperties.getMaxTokens());
46   - return new XingHuoChatClient(
47   - new XingHuoApi(
48   - xingHuoProperties.getAppId(),
49   - xingHuoProperties.getAppKey(),
50   - xingHuoProperties.getSecretKey()
51   - ),
52   - xingHuoOptions
53   - );
  38 + YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo();
  39 + XingHuoChatOptions options = XingHuoChatOptions.builder()
  40 + .model(properties.getModel())
  41 + .temperature(properties.getTemperature())
  42 + .maxTokens(properties.getMaxTokens())
  43 + .topK(properties.getTopK())
  44 + .build();
  45 + return new XingHuoChatClient(properties.getAppKey(), properties.getSecretKey(), options);
54 46 }
55 47  
56 48 @Bean
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java
1 1 package cn.iocoder.yudao.framework.ai.config;
2 2  
3   -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
5 3 import lombok.Data;
6   -import lombok.experimental.Accessors;
7   -import org.springframework.ai.autoconfigure.openai.OpenAiImageProperties;
8 4 import org.springframework.boot.context.properties.ConfigurationProperties;
9 5  
10 6 /**
11   - * ai 自动配置
  7 + * 芋道 AI 配置类
12 8 *
13 9 * @author fansili
14   - * @time 2024/4/12 16:29
15 10 * @since 1.0
16 11 */
17   -@Data
18   -@Accessors(chain = true)
19 12 @ConfigurationProperties(prefix = "yudao.ai")
  13 +@Data
20 14 public class YudaoAiProperties {
21 15  
  16 + /**
  17 + * 讯飞星火
  18 + */
22 19 private XingHuoProperties xinghuo;
23   - private OpenAiImageProperties openAiImage;
24   - private MidjourneyProperties midjourney;
25   - private SunoProperties suno;
26   -
27   - @Data
28   - @Accessors(chain = true)
29   - public static class ChatProperties {
30 20  
31   - private boolean enable = false;
32   -
33   - private AiPlatformEnum aiPlatform;
34   -
35   - private Float temperature;
36   -
37   - private Float topP;
  21 + /**
  22 + * Midjourney 绘图
  23 + */
  24 + private MidjourneyProperties midjourney;
38 25  
39   - private Integer topK;
40   - /**
41   - * 用于限制模型生成token的数量,max_tokens设置的是生成上限,并不表示一定会生成这么多的token数量
42   - */
43   - private Integer maxTokens;
44   - }
  26 + /**
  27 + * Suno 音乐
  28 + */
  29 + private SunoProperties suno;
45 30  
46 31 @Data
47   - public static class XingHuoProperties extends ChatProperties {
  32 + public static class XingHuoProperties {
48 33  
  34 + private String enable;
49 35 private String appId;
50 36 private String appKey;
51 37 private String secretKey;
52   - private XingHuoChatModel model;
  38 +
  39 + private String model;
  40 + private Float temperature;
  41 + private Integer maxTokens;
  42 + private Integer topK;
53 43  
54 44 }
55 45  
... ... @@ -57,8 +47,9 @@ public class YudaoAiProperties {
57 47 public static class MidjourneyProperties {
58 48  
59 49 private String enable;
60   - private String apiKey;
61 50 private String baseUrl;
  51 +
  52 + private String apiKey;
62 53 private String notifyUrl;
63 54  
64 55 }
... ... @@ -68,9 +59,6 @@ public class YudaoAiProperties {
68 59  
69 60 private boolean enable = false;
70 61  
71   - /**
72   - * API 服务的基本地址
73   - */
74 62 private String baseUrl;
75 63  
76 64 }
... ...
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;
12 12 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
13 13 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
14 14 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
15   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
16 15 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
17 16 import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties;
18 17 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel;
... ... @@ -178,11 +177,9 @@ public class AiClientFactoryImpl implements AiClientFactory {
178 177 private static XingHuoChatClient buildXingHuoChatClient(String key) {
179 178 List<String> keys = StrUtil.split(key, '|');
180 179 Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式");
181   - String appId = keys.get(0);
182 180 String appKey = keys.get(1);
183 181 String secretKey = keys.get(2);
184   - XingHuoApi xingHuoApi = new XingHuoApi(appId, appKey, secretKey);
185   - return new XingHuoChatClient(xingHuoApi);
  182 + return new XingHuoChatClient(appKey, secretKey);
186 183 }
187 184  
188 185 /**
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatClient.java
1 1 package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
2 2  
3   -import cn.hutool.core.bean.BeanUtil;
4   -import cn.hutool.core.exceptions.ExceptionUtil;
5   -import cn.iocoder.yudao.framework.ai.core.exception.ChatException;
6   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
7   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion;
8   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest;
  3 +import cn.hutool.core.lang.Assert;
9 4 import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
10 6 import org.springframework.ai.chat.model.ChatModel;
11 7 import org.springframework.ai.chat.model.ChatResponse;
12 8 import org.springframework.ai.chat.model.Generation;
13   -import org.springframework.ai.chat.model.StreamingChatModel;
14 9 import org.springframework.ai.chat.prompt.ChatOptions;
15 10 import org.springframework.ai.chat.prompt.Prompt;
  11 +import org.springframework.ai.model.ModelOptionsUtils;
  12 +import org.springframework.ai.openai.OpenAiChatOptions;
  13 +import org.springframework.ai.openai.api.OpenAiApi;
  14 +import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata;
  15 +import org.springframework.ai.retry.RetryUtils;
16 16 import org.springframework.http.ResponseEntity;
17   -import org.springframework.retry.RetryCallback;
18   -import org.springframework.retry.RetryContext;
19   -import org.springframework.retry.RetryListener;
20 17 import org.springframework.retry.support.RetryTemplate;
21 18 import reactor.core.publisher.Flux;
22 19  
23   -import java.time.Duration;
  20 +import java.util.HashMap;
24 21 import java.util.List;
25   -import java.util.stream.Collectors;
  22 +import java.util.Map;
  23 +
  24 +import static cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions.MODEL_DEFAULT;
26 25  
27   -// TODO @fan:参考 yiyan 的修改建议,调整下 xinghuo 的实现;可以等 yiyan 修改完建议,然后我 review 完,再改这个哈;
28 26 /**
29   - * 讯飞星火 client
30   - * <p>
31   - * author: fansili
32   - * time: 2024/3/11 10:19
  27 + * 讯飞星火 {@link ChatModel} 实现类
  28 + *
  29 + * @author fansili
33 30 */
34 31 @Slf4j
35   -public class XingHuoChatClient implements ChatModel, StreamingChatModel {
36   -
37   - private XingHuoApi xingHuoApi;
38   -
39   - private XingHuoOptions xingHuoOptions;
40   -
41   - public final RetryTemplate retryTemplate = RetryTemplate.builder()
42   - // 最大重试次数 10
43   - .maxAttempts(3)
44   - .retryOn(ChatException.class)
45   - // 最大重试5次,第一次间隔3000ms,第二次3000ms * 2,第三次3000ms * 3,以此类推,最大间隔3 * 60000ms
46   - .exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000))
47   - .withListener(new RetryListener() {
48   - @Override
49   - public <T extends Object, E extends Throwable> void onError(RetryContext context,
50   - RetryCallback<T, E> callback, Throwable throwable) {
51   - System.err.println("正在重试... " + ExceptionUtil.getMessage(throwable));
52   - log.warn("重试异常:" + context.getRetryCount(), throwable);
53   - }
  32 +public class XingHuoChatClient implements ChatModel {
  33 +
  34 + private static final String BASE_URL = "https://spark-api-open.xf-yun.com";
54 35  
55   - ;
56   - })
57   - .build();
  36 + private final XingHuoChatOptions defaultOptions;
  37 + private final RetryTemplate retryTemplate;
58 38  
59   - public XingHuoChatClient(XingHuoApi xingHuoApi) {
60   - this.xingHuoApi = xingHuoApi;
  39 + /**
  40 + * 星火兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本
  41 + *
  42 + * 不过要注意,星火没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它
  43 + */
  44 + private final OpenAiApi openAiApi;
  45 +
  46 + public XingHuoChatClient(String apiKey, String secretKey) {
  47 + this(apiKey, secretKey,
  48 + XingHuoChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build());
61 49 }
62 50  
63   - public XingHuoChatClient(XingHuoApi xingHuoApi, XingHuoOptions xingHuoOptions) {
64   - this.xingHuoApi = xingHuoApi;
65   - this.xingHuoOptions = xingHuoOptions;
  51 + public XingHuoChatClient(String apiKey, String secretKey, XingHuoChatOptions options) {
  52 + this(apiKey, secretKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE);
  53 + }
  54 +
  55 + public XingHuoChatClient(String apiKey, String secretKey, XingHuoChatOptions options, RetryTemplate retryTemplate) {
  56 + Assert.notEmpty(apiKey, "apiKey 不能为空");
  57 + Assert.notEmpty(secretKey, "secretKey 不能为空");
  58 + Assert.notNull(options, "options 不能为空");
  59 + Assert.notNull(retryTemplate, "retryTemplate 不能为空");
  60 + this.openAiApi = new OpenAiApi(BASE_URL, apiKey + ":" + secretKey);
  61 + this.defaultOptions = options;
  62 + this.retryTemplate = retryTemplate;
66 63 }
67 64  
68 65 @Override
69 66 public ChatResponse call(Prompt prompt) {
  67 + OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false);
70 68 return this.retryTemplate.execute(ctx -> {
71   - // ctx 会有重试的信息
72   - // 获取 chatOptions 属性
73   - XingHuoOptions chatOptions = this.getChatOptions(prompt);
74   - // 创建 request 请求,stream模式需要供应商支持
75   - XingHuoChatCompletionRequest request = this.createRequest(prompt, chatOptions);
76   - // 调用 callWithFunctionSupport 发送请求
77   - ResponseEntity<XingHuoChatCompletion> response = xingHuoApi.chatCompletionEntity(request, chatOptions.getChatModel());
78   - // 获取结果封装 ChatResponse
79   - return new ChatResponse(List.of(new Generation(response.getBody().getPayload().getChoices().getText().get(0).getContent())));
  69 + // 1.1 发起调用
  70 + ResponseEntity<OpenAiApi.ChatCompletion> completionEntity = openAiApi.chatCompletionEntity(request);
  71 + // 1.2 校验结果
  72 + OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody();
  73 + if (chatCompletion == null) {
  74 + log.warn("No chat completion returned for prompt: {}", prompt);
  75 + return new ChatResponse(List.of());
  76 + }
  77 + List<OpenAiApi.ChatCompletion.Choice> choices = chatCompletion.choices();
  78 + if (choices == null) {
  79 + log.warn("No choices returned for prompt: {}", prompt);
  80 + return new ChatResponse(List.of());
  81 + }
  82 +
  83 + // 2. 转换 ChatResponse 返回
  84 + List<Generation> generations = choices.stream().map(choice -> {
  85 + Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice));
  86 + if (choice.finishReason() != null) {
  87 + generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null));
  88 + }
  89 + return generation;
  90 + }).toList();
  91 + return new ChatResponse(generations,
  92 + OpenAiChatResponseMetadata.from(completionEntity.getBody()));
80 93 });
81 94 }
82 95  
83   - @Override
84   - public ChatOptions getDefaultOptions() {
85   - // TODO 芋艿:需要跟进下
86   - throw new UnsupportedOperationException();
  96 + private Map<String, Object> toMap(String id, OpenAiApi.ChatCompletion.Choice choice) {
  97 + Map<String, Object> map = new HashMap<>();
  98 + OpenAiApi.ChatCompletionMessage message = choice.message();
  99 + if (message.role() != null) {
  100 + map.put("role", message.role().name());
  101 + }
  102 + if (choice.finishReason() != null) {
  103 + map.put("finishReason", choice.finishReason().name());
  104 + }
  105 + map.put("id", id);
  106 + return map;
87 107 }
88 108  
89 109 @Override
90 110 public Flux<ChatResponse> stream(Prompt prompt) {
91   - // 获取 chatOptions 属性
92   - XingHuoOptions chatOptions = this.getChatOptions(prompt);
93   - // 创建 request 请求,stream模式需要供应商支持
94   - XingHuoChatCompletionRequest request = this.createRequest(prompt, chatOptions);
95   - // 发送请求
96   - Flux<XingHuoChatCompletion> response = this.xingHuoApi.chatCompletionStream(request, chatOptions.getChatModel());
97   - return response.map(res -> {
98   - String content = res.getPayload().getChoices().getText().stream()
99   - .map(item -> item.getContent()).collect(Collectors.joining());
100   - return new ChatResponse(List.of(new Generation(content)));
  111 + OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true);
  112 + return this.retryTemplate.execute(ctx -> {
  113 + // 1. 发起调用
  114 + Flux<OpenAiApi.ChatCompletionChunk> response = this.openAiApi.chatCompletionStream(request);
  115 + return response.map(chatCompletion -> {
  116 + String id = chatCompletion.id();
  117 + // 2. 转换 ChatResponse 返回
  118 + List<Generation> generations = chatCompletion.choices().stream().map(choice -> {
  119 + String finish = (choice.finishReason() != null ? choice.finishReason().name() : "");
  120 + Generation generation = new Generation(choice.delta().content(),
  121 + Map.of("id", id, "role", choice.delta().role().name(), "finishReason", finish));
  122 + if (choice.finishReason() != null) {
  123 + generation = generation.withGenerationMetadata(
  124 + ChatGenerationMetadata.from(choice.finishReason().name(), null));
  125 + }
  126 + return generation;
  127 + }).toList();
  128 + return new ChatResponse(generations);
  129 + });
101 130 });
102 131 }
103 132  
104   - private XingHuoOptions getChatOptions(Prompt prompt) {
105   - // 两个都为null 则没有配置文件
106   - if (xingHuoOptions == null && prompt.getOptions() == null) {
107   - throw new ChatException("ChatOptions 未配置参数!");
108   - }
109   - // 优先使用 Prompt 里面的 ChatOptions
110   - ChatOptions options = xingHuoOptions;
  133 + OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
  134 + // 1. 构建 ChatCompletionMessage 对象
  135 + List<OpenAiApi.ChatCompletionMessage> chatCompletionMessages = prompt.getInstructions().stream().map(m ->
  136 + new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList();
  137 + OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream);
  138 +
  139 + // 2.1 补充 prompt 内置的 options
111 140 if (prompt.getOptions() != null) {
112   - options = (ChatOptions) prompt.getOptions();
  141 + if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {
  142 + OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,
  143 + ChatOptions.class, OpenAiChatOptions.class);
  144 + request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class);
  145 + } else {
  146 + throw new IllegalArgumentException("Prompt options are not of type ChatOptions: "
  147 + + prompt.getOptions().getClass().getSimpleName());
  148 + }
113 149 }
114   - // Prompt 里面是一个 ChatOptions,用户可以随意传入,这里做一下判断
115   - if (!(options instanceof XingHuoOptions)) {
116   - throw new ChatException("Prompt 传入的不是 XingHuoOptions!");
  150 + // 2.2 补充默认 options
  151 + if (this.defaultOptions != null) {
  152 + request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class);
117 153 }
118   - return (XingHuoOptions) options;
  154 + return request;
119 155 }
120 156  
121   - private XingHuoChatCompletionRequest createRequest(Prompt prompt, XingHuoOptions xingHuoOptions) {
122   - // 创建 header
123   - XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id(xingHuoApi.getAppId());
124   - // 创建 params
125   - XingHuoChatCompletionRequest.Parameter.Chat chatParameter = new XingHuoChatCompletionRequest.Parameter.Chat();
126   - BeanUtil.copyProperties(xingHuoOptions, chatParameter);
127   - chatParameter.setDomain(xingHuoOptions.getChatModel().getModel());
128   - XingHuoChatCompletionRequest.Parameter parameter = new XingHuoChatCompletionRequest.Parameter().setChat(chatParameter);
129   - // 创建 payload text 信息
130   - List<XingHuoChatCompletionRequest.Payload.Message.Text> texts = prompt.getInstructions().stream().map(message -> {
131   - XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text();
132   - text.setContent(message.getContent());
133   - text.setRole(message.getMessageType().getValue());
134   - return text;
135   - }).collect(Collectors.toList());
136   - // 创建 payload
137   - XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload()
138   - .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(texts));
139   - // 创建 request
140   - return new XingHuoChatCompletionRequest()
141   - .setHeader(header)
142   - .setParameter(parameter)
143   - .setPayload(payload);
  157 + @Override
  158 + public ChatOptions getDefaultOptions() {
  159 + return XingHuoChatOptions.fromOptions(defaultOptions);
144 160 }
  161 +
145 162 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
2   -
3   -import lombok.Getter;
4   -
5   -/**
6   - * 讯飞星火 模型
7   - *
8   - * 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
9   - *
10   - * 1tokens 约等于1.5个中文汉字 或者 0.8个英文单词
11   - * 星火V1.5支持[搜索]内置插件;星火V2.0、V3.0和V3.5支持[搜索]、[天气]、[日期]、[诗词]、[字词]、[股票]六个内置插件
12   - * 星火V3.5 现已支持system、Function Call 功能。
13   - *
14   - * author: fansili
15   - * time: 2024/3/11 10:12
16   - */
17   -@Getter
18   -public enum XingHuoChatModel {
19   -
20   -// 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
21   -// general指向V1.5版本;
22   -// generalv2指向V2版本;
23   -// generalv3指向V3版本;
24   -// generalv3.5指向V3.5版本;
25   -
26   - XING_HUO_1_5("星火大模型1.5", "general", "/v1.1/chat"),
27   - XING_HUO_2_0("星火大模型2.0", "generalv2", "/v2.1/chat"),
28   - XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"),
29   - XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"),
30   -
31   - ;
32   -
33   - XingHuoChatModel(String name, String model, String uri) {
34   - this.name = name;
35   - this.model = model;
36   - this.uri = uri;
37   - }
38   -
39   - private String name;
40   -
41   - private String model;
42   -
43   - private String uri;
44   -
45   - public static XingHuoChatModel valueOfModel(String model) {
46   - for (XingHuoChatModel itemEnum : XingHuoChatModel.values()) {
47   - if (itemEnum.getModel().equals(model)) {
48   - return itemEnum;
49   - }
50   - }
51   - throw new IllegalArgumentException("Invalid MessageType value: " + model);
52   - }
53   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +import org.springframework.ai.chat.prompt.ChatOptions;
  8 +
  9 +/**
  10 + * 讯飞星火 {@link ChatOptions} 实现类
  11 + *
  12 + * 参考文档:<a href="https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html">HTTP 调用</a>
  13 + *
  14 + * @author fansili
  15 + */
  16 +@Data
  17 +@NoArgsConstructor
  18 +@AllArgsConstructor
  19 +@Builder
  20 +public class XingHuoChatOptions implements ChatOptions {
  21 +
  22 + public static final String MODEL_DEFAULT = "generalv3.5";
  23 +
  24 + /**
  25 + * 模型
  26 + */
  27 + private String model;
  28 + /**
  29 + * 温度
  30 + */
  31 + private Float temperature;
  32 + /**
  33 + * 最大 Token
  34 + */
  35 + private Integer maxTokens;
  36 + /**
  37 + * K 个候选
  38 + */
  39 + private Integer topK;
  40 +
  41 + @Override
  42 + public Float getTopP() {
  43 + return null;
  44 + }
  45 +
  46 + public static XingHuoChatOptions fromOptions(XingHuoChatOptions fromOptions) {
  47 + return XingHuoChatOptions.builder()
  48 + .model(fromOptions.getModel())
  49 + .temperature(fromOptions.getTemperature())
  50 + .maxTokens(fromOptions.getMaxTokens())
  51 + .topK(fromOptions.getTopK())
  52 + .build();
  53 + }
  54 +
  55 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoOptions.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
2   -
3   -import org.springframework.ai.chat.prompt.ChatOptions;
4   -import lombok.Data;
5   -import lombok.experimental.Accessors;
6   -
7   -/**
8   - * 讯飞星火
9   - * <p>
10   - * author: fansili
11   - * time: 2024/3/16 20:29
12   - */
13   -@Data
14   -@Accessors(chain = true)
15   -public class XingHuoOptions implements ChatOptions {
16   -
17   - // TODO @fan:这里 model 参数,然后使用 string
18   - /**
19   - * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
20   - * <p>
21   - * 指定访问的领域:
22   - * general指向V1.5版本;
23   - * generalv2指向V2版本;
24   - * generalv3指向V3版本;
25   - * generalv3.5指向V3.5版本;
26   - * 注意:不同的取值对应的url也不一样!
27   - */
28   - private XingHuoChatModel chatModel = XingHuoChatModel.XING_HUO_3_5;
29   - /**
30   - * 取值范围 (0,1] ,默认值0.5
31   - */
32   - private Float temperature;
33   - /**
34   - * V1.5取值为[1,4096]
35   - * V2.0、V3.0和V3.5取值为[1,8192],默认为2048。
36   - */
37   - private Integer maxTokens;
38   - /**
39   - * 取值为[1,6],默认为4
40   - */
41   - private Integer topK;
42   - /**
43   - * 需要保障用户下的唯一性,用于关联用户会话
44   - */
45   - private String chatId;
46   -
47   - @Override
48   - public Float getTemperature() {
49   - return this.temperature;
50   - }
51   -
52   -// @Override
53   -// public void setTemperature(Float temperature) {
54   -// this.temperature = temperature;
55   -// }
56   -
57   - @Override
58   - public Float getTopP() {
59   - return null;
60   - }
61   -
62   -// @Override
63   -// public void setTopP(Float topP) {
64   -//
65   -// }
66   -
67   - @Override
68   - public Integer getTopK() {
69   - return this.topK;
70   - }
71   -
72   -// @Override
73   -// public void setTopK(Integer topK) {
74   -// this.topK = topK;
75   -// }
76   -
77   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoApi.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
2   -
3   -import cn.hutool.http.HttpUtil;
4   -import cn.hutool.json.JSONUtil;
5   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
6   -import lombok.Data;
7   -import org.springframework.http.HttpStatusCode;
8   -import org.springframework.http.ResponseEntity;
9   -import org.springframework.web.reactive.function.client.WebClient;
10   -import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
11   -import reactor.core.publisher.Flux;
12   -import reactor.core.publisher.Mono;
13   -
14   -import javax.crypto.Mac;
15   -import javax.crypto.spec.SecretKeySpec;
16   -import java.net.URI;
17   -import java.nio.charset.Charset;
18   -import java.security.InvalidKeyException;
19   -import java.security.NoSuchAlgorithmException;
20   -import java.text.SimpleDateFormat;
21   -import java.util.*;
22   -import java.util.stream.Collectors;
23   -
24   -// TODO @fan:讯飞使用 spring websocket 接入,还是 okhttp?确认了,未使用的最好删除下,反正 git 也能找回 history
25   -/**
26   - * 讯飞星火 属性、api
27   - * <p>
28   - * 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
29   - * <p>
30   - * author: fansili
31   - * time: 2024/3/11 10:12
32   - */
33   -@Data
34   -public class XingHuoApi {
35   -
36   - private static final String DEFAULT_BASE_URL = "wss://spark-api.xf-yun.com";
37   -
38   - private String appId;
39   - private String appKey;
40   - private String secretKey;
41   - private WebClient webClient;
42   - // 创建 WebSocketClient 实例
43   - private ReactorNettyWebSocketClient socketClient = new ReactorNettyWebSocketClient();
44   -
45   - public XingHuoApi(String appId, String appKey, String secretKey) {
46   - this.appId = appId;
47   - this.appKey = appKey;
48   - this.secretKey = secretKey;
49   - }
50   -
51   - public ResponseEntity<XingHuoChatCompletion> chatCompletionEntity(XingHuoChatCompletionRequest request, XingHuoChatModel xingHuoChatModel) {
52   - String authUrl;
53   - try {
54   -// XingHuoChatModel useChatModel;
55   - authUrl = getAuthorizationUrl("spark-api.xf-yun.com", xingHuoChatModel.getUri());
56   - } catch (NoSuchAlgorithmException | InvalidKeyException e) {
57   - throw new RuntimeException(e);
58   - }
59   - // wss 请求的 URI
60   - URI uri = URI.create(authUrl);
61   - // 发起 wss 请求并处理响应
62   - Flux<XingHuoChatCompletion> messageFlux = Flux.create(sink -> {
63   - socketClient.execute(uri, session ->
64   - session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request))))
65   - .thenMany(session.receive()
66   - .map(WebSocketMessage -> {
67   - return JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class);
68   - })
69   - .doOnNext(sink::next) // 将接收到的消息推送到 Flux 中
70   - .doOnError(sink::error) // 处理错误
71   - .doOnTerminate(sink::complete)) // 完成时关闭 sink
72   - .then())
73   - .subscribe(); // 订阅以开始会话
74   - });
75   - // 阻塞获取所有结果
76   - List<XingHuoChatCompletion> responseList = messageFlux.collectList().block();
77   - // 拼接 content
78   - String responseContent = responseList.stream().map(item -> {
79   - // 获取 content
80   - return item.getPayload().getChoices().getText().stream().map(XingHuoChatCompletion.Text::getContent).collect(Collectors.joining());
81   - }).collect(Collectors.joining());
82   - // 将多个合并成一个
83   - XingHuoChatCompletion xingHuoChatCompletion = new XingHuoChatCompletion();
84   - xingHuoChatCompletion.setPayload(new XingHuoChatCompletion.Payload().setChoices(new XingHuoChatCompletion.Choices().setText(List.of(new XingHuoChatCompletion.Text().setContent(responseContent)))));
85   - return new ResponseEntity<>(xingHuoChatCompletion, HttpStatusCode.valueOf(200));
86   - }
87   -
88   -
89   - /**
90   - * 获取验证请求url
91   - *
92   - * @return
93   - */
94   - public String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException {
95   - // 获取鉴权时间 date
96   - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
97   - format.setTimeZone(TimeZone.getTimeZone("GMT"));
98   - String date = format.format(new Date());
99   -
100   - // 获取signature_origin字段
101   - StringBuilder builder = new StringBuilder("host: ").append(host).append("\n").
102   - append("date: ").append(date).append("\n").
103   - append("GET ").append(path).append(" HTTP/1.1");
104   -
105   - // 获得signatue
106   - Charset charset = Charset.forName("UTF-8");
107   - Mac mac = Mac.getInstance("hmacsha256");
108   - SecretKeySpec sp = new SecretKeySpec(secretKey.getBytes(charset), "hmacsha256");
109   - mac.init(sp);
110   - byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset));
111   - String signature = Base64.getEncoder().encodeToString(basebefore);
112   - //获得 authorization_origin
113   - String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", appKey, "hmac-sha256", "host date request-line", signature);
114   - //获得authorization
115   - String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset));
116   - // 获取httpUrl
117   - Map<String, Object> param = new HashMap<>();
118   - param.put("authorization", authorization);
119   - param.put("date", date);
120   - param.put("host", host);
121   -
122   - String toParams = HttpUtil.toParams(param);
123   - return "wss://" + host + path + "?" + toParams;
124   - }
125   -
126   - public Flux<XingHuoChatCompletion> chatCompletionStream(XingHuoChatCompletionRequest request, XingHuoChatModel xingHuoChatModel) {
127   - String authUrl;
128   - try {
129   - authUrl = getAuthorizationUrl("spark-api.xf-yun.com", xingHuoChatModel.getUri());
130   - } catch (NoSuchAlgorithmException | InvalidKeyException e) {
131   - throw new RuntimeException(e);
132   - }
133   - // wss 请求的 URI
134   - URI uri = URI.create(authUrl);
135   - // 发起 wss 请求并处理响应
136   - // 创建一个 Flux 来处理接收到的消息
137   - return Flux.create(sink -> {
138   - socketClient.execute(uri, session ->
139   - session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request))))
140   - .thenMany(session.receive()
141   - .map(WebSocketMessage -> JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class))
142   - .doOnNext(sink::next) // 将接收到的消息推送到 Flux 中
143   - .doOnError(sink::error) // 处理错误
144   - .doOnTerminate(sink::complete)) // 完成时关闭 sink
145   - .then())
146   - .subscribe(); // 订阅以开始会话
147   - });
148   - }
149   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletion.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
2   -
3   -import lombok.Data;
4   -import lombok.experimental.Accessors;
5   -
6   -import java.util.List;
7   -
8   -/**
9   - * author: fansili
10   - * time: 2024/3/11 10:20
11   - */
12   -@Data
13   -@Accessors(chain = true)
14   -public class XingHuoChatCompletion {
15   - private Header header;
16   - private Payload payload;
17   -
18   - @Data
19   - @Accessors(chain = true)
20   - public static class Header {
21   - private int code;
22   - private String message;
23   - private String sid;
24   - private int status;
25   - }
26   -
27   - @Data
28   - @Accessors(chain = true)
29   - public static class Payload {
30   - private Choices choices;
31   - }
32   -
33   - @Data
34   - @Accessors(chain = true)
35   - public static class Choices {
36   - private int status;
37   - private int seq;
38   - private List<Text> text;
39   - }
40   -
41   - @Data
42   - @Accessors(chain = true)
43   - public static class Text {
44   - private String content;
45   - private String role;
46   - private int index;
47   - }
48   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionMessage.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
2   -
3   -/**
4   - * author: fansili
5   - * time: 2024/3/11 10:20
6   - */
7   -public class XingHuoChatCompletionMessage {
8   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoChatCompletionRequest.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
2   -
3   -import lombok.Data;
4   -import lombok.Getter;
5   -import lombok.experimental.Accessors;
6   -
7   -import java.util.List;
8   -
9   -/**
10   - * 讯飞星火 request
11   - *
12   - * author: fansili
13   - * time: 2024/3/11 10:20
14   - */
15   -@Data
16   -@Accessors(chain = true)
17   -public class XingHuoChatCompletionRequest {
18   -
19   - private Header header;
20   - private Parameter parameter;
21   - private Payload payload;
22   -
23   - @Data
24   - @Accessors(chain = true)
25   - public static class Header {
26   - private String app_id;
27   - private String uid;
28   - }
29   -
30   - @Data
31   - @Accessors(chain = true)
32   - public static class Parameter {
33   - private Chat chat;
34   -
35   - @Data
36   - @Accessors(chain = true)
37   - public static class Chat {
38   - /**
39   - * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
40   - *
41   - * 指定访问的领域:
42   - * general指向V1.5版本;
43   - * generalv2指向V2版本;
44   - * generalv3指向V3版本;
45   - * generalv3.5指向V3.5版本;
46   - * 注意:不同的取值对应的url也不一样!
47   - */
48   - private String domain = "generalv3.5";
49   - /**
50   - * 取值范围 (0,1] ,默认值0.5
51   - */
52   - private Float temperature;
53   - /**
54   - * V1.5取值为[1,4096]
55   - * V2.0、V3.0和V3.5取值为[1,8192],默认为2048。
56   - */
57   - private Integer max_tokens;
58   - /**
59   - * 取值为[1,6],默认为4
60   - */
61   - private Integer top_k;
62   - /**
63   - * 需要保障用户下的唯一性,用于关联用户会话
64   - */
65   - private String chat_id;
66   - }
67   - }
68   -
69   - @Data
70   - @Accessors(chain = true)
71   - public static class Payload {
72   - private Message message;
73   -
74   - @Data
75   - @Accessors(chain = true)
76   - public static class Message {
77   - private List<Text> text;
78   -
79   -
80   - @Data
81   - @Accessors(chain = true)
82   - public static class Text {
83   - /**
84   - * 角色
85   - */
86   - private String role;
87   - /**
88   - * 消息内容
89   - */
90   - private String content;
91   - private Integer index;
92   -
93   - @Getter
94   - public static enum Role {
95   - SYSTEM("system"),
96   - USER("user"),
97   - ASSISTANT("assistant");
98   - private String name;
99   -
100   - private Role(String name) {
101   - this.name = name;
102   - }
103   - }
104   - }
105   - }
106   - }
107   -}
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;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
5   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
6   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
  5 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
7 6 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
8 7 import org.springframework.ai.chat.messages.*;
9 8 import org.springframework.ai.chat.prompt.ChatOptions;
... ... @@ -31,8 +30,7 @@ public class AiUtils {
31 30 // return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
32 31 return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
33 32 case XING_HUO:
34   - return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
35   - .setMaxTokens(maxTokens);
  33 + return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build();
36 34 case QIAN_WEN:
37 35 return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
38 36 default:
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java deleted
1   -package cn.iocoder.yudao.framework.ai.chat;
2   -
3   -import cn.hutool.http.HttpUtil;
4   -import cn.hutool.json.JSONUtil;
5   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion;
6   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest;
7   -import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
8   -import org.springframework.web.reactive.socket.client.WebSocketClient;
9   -import reactor.core.publisher.Flux;
10   -
11   -import javax.crypto.Mac;
12   -import javax.crypto.spec.SecretKeySpec;
13   -import java.net.MalformedURLException;
14   -import java.net.URI;
15   -import java.nio.charset.Charset;
16   -import java.security.InvalidKeyException;
17   -import java.security.NoSuchAlgorithmException;
18   -import java.text.SimpleDateFormat;
19   -import java.util.*;
20   -
21   -// TODO 芋艿:整理单测
22   -/**
23   - * author: fansili
24   - * time: 2024/3/13 20:47
25   - */
26   -public class XingHuoChatClientMainTests {
27   -
28   -
29   - private static final String HOST_URL = "http://spark-api.xf-yun.com/v3.5/chat";
30   - private static final String API_KEY = "cb6415c19d6162cda07b47316fcb0416";
31   - private static final String API_SECRET = "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh";
32   -
33   - public static void main(String[] args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException {
34   - String authUrl = getAuthorizationUrl("spark-api.xf-yun.com", "/v3.5/chat");
35   - System.err.println(authUrl);
36   -
37   - XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id("13c8cca6");
38   - XingHuoChatCompletionRequest.Parameter parameter
39   - = new XingHuoChatCompletionRequest.Parameter()
40   - .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain("generalv3.5"));
41   -
42   -
43   - XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text();
44   - text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName());
45   - text.setContent("世界上最好的开发语言是什么?");
46   - XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload()
47   - .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text)));
48   - XingHuoChatCompletionRequest request = new XingHuoChatCompletionRequest()
49   - .setHeader(header)
50   - .setParameter(parameter)
51   - .setPayload(payload);
52   -
53   - System.err.println(JSONUtil.toJsonPrettyStr(request));
54   -
55   -
56   - // 创建 WebSocketClient 实例
57   - WebSocketClient client = new ReactorNettyWebSocketClient();
58   -
59   - // wss 请求的 URI
60   - URI uri = URI.create(authUrl);
61   -
62   - // 发起 wss 请求并处理响应
63   - client.execute(uri, session ->
64   - // 使用会话发送消息,并接收回应
65   - session.send(Flux.just(session.textMessage(JSONUtil.toJsonStr(request))))
66   - .thenMany(session.receive()
67   - .map(WebSocketMessage -> {
68   - System.err.println(WebSocketMessage.getPayloadAsText());
69   - return JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class);
70   - })
71   - .log()) // 打印接收到的消息
72   - .then())
73   - .block(); // 等待操作完成或超时
74   -
75   - // 阻止退出
76   - Scanner scanner = new Scanner(System.in);
77   - scanner.nextLine();
78   - }
79   -
80   -
81   - /**
82   - * 获取验证请求url
83   - *
84   - * @return
85   - */
86   - public static String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException {
87   - // 获取鉴权时间 date
88   - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
89   - format.setTimeZone(TimeZone.getTimeZone("GMT"));
90   - String date = format.format(new Date());
91   -
92   - // 获取signature_origin字段
93   - StringBuilder builder = new StringBuilder("host: ").append(host).append("\n").
94   - append("date: ").append(date).append("\n").
95   - append("GET ").append(path).append(" HTTP/1.1");
96   -
97   - // 获得signatue
98   - Charset charset = Charset.forName("UTF-8");
99   - Mac mac = Mac.getInstance("hmacsha256");
100   - SecretKeySpec sp = new SecretKeySpec(API_SECRET.getBytes(charset), "hmacsha256");
101   - mac.init(sp);
102   - byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset));
103   - String signature = Base64.getEncoder().encodeToString(basebefore);
104   - //获得 authorization_origin
105   - String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", API_KEY, "hmac-sha256", "host date request-line", signature);
106   - //获得authorization
107   - String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset));
108   - // 获取httpUrl
109   - Map<String, Object> param = new HashMap<>();
110   - param.put("authorization", authorization);
111   - param.put("date", date);
112   - param.put("host", host);
113   -
114   - String toParams = HttpUtil.toParams(param);
115   - return "wss://" + host + path + "?" + toParams;
116   - }
117   -
118   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java
1 1 package cn.iocoder.yudao.framework.ai.chat;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
4   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
5   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
6   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
7   -import org.junit.Before;
8   -import org.junit.Test;
  4 +import org.junit.jupiter.api.Test;
9 5 import org.springframework.ai.chat.messages.Message;
10 6 import org.springframework.ai.chat.messages.SystemMessage;
11 7 import org.springframework.ai.chat.messages.UserMessage;
... ... @@ -15,58 +11,44 @@ import reactor.core.publisher.Flux;
15 11  
16 12 import java.util.ArrayList;
17 13 import java.util.List;
18   -import java.util.Scanner;
19   -import java.util.function.Consumer;
20 14  
21   -// TODO 芋艿:整理单测
22 15 /**
23   - * 讯飞星火 tests
24   - * <p>
25   - * author: fansili
26   - * time: 2024/3/11 11:00
  16 + * {@link XingHuoChatClient} 集成测试
  17 + *
  18 + * @author fansili
27 19 */
28 20 public class XingHuoChatClientTests {
29 21  
30   - private XingHuoChatClient xingHuoChatClient;
31   -
32   - @Before
33   - public void setup() {
34   - // 初始化 xingHuoChatClient
35   - xingHuoChatClient = new XingHuoChatClient(
36   - new XingHuoApi(
37   - "13c8cca6",
38   - "cb6415c19d6162cda07b47316fcb0416",
39   - "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"
40   - ),
41   - new XingHuoOptions().setChatModel(XingHuoChatModel.XING_HUO_3_5)
42   - );
43   - }
  22 + private final XingHuoChatClient client = new XingHuoChatClient(
  23 + "cb6415c19d6162cda07b47316fcb0416",
  24 + "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh");
44 25  
45 26 @Test
46   - public void callTest() {
  27 + public void testCall() {
  28 + // 准备参数
47 29 List<Message> messages = new ArrayList<>();
48 30 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
49   - messages.add(new UserMessage("长沙怎么样?"));
  31 + messages.add(new UserMessage("1 + 1 = ?"));
50 32  
51   - ChatResponse call = xingHuoChatClient.call(new Prompt(messages));
52   - System.err.println(call.getResult());
  33 + // 调用
  34 + ChatResponse response = client.call(new Prompt(messages));
  35 + // 打印结果
  36 + System.err.println(response);
53 37 }
54 38  
55 39 @Test
56   - public void streamTest() {
  40 + public void testStream() {
  41 + // 准备参数
57 42 List<Message> messages = new ArrayList<>();
58 43 messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
59   - messages.add(new UserMessage("长沙怎么样?"));
60   -
61   - Flux<ChatResponse> stream = xingHuoChatClient.stream(new Prompt(messages));
62   - stream.subscribe(new Consumer<ChatResponse>() {
63   - @Override
64   - public void accept(ChatResponse chatResponse) {
65   - System.err.print(chatResponse.getResult().getOutput().getContent());
66   - }
67   - });
68   - // 阻止退出
69   - Scanner scanner = new Scanner(System.in);
70   - scanner.nextLine();
  44 + messages.add(new UserMessage("1 + 1 = ?"));
  45 +
  46 + // 调用
  47 + Flux<ChatResponse> flux = client.stream(new Prompt(messages));
  48 + // 打印结果
  49 + List<ChatResponse> responses = flux.collectList().block();
  50 + assert responses != null;
  51 + responses.forEach(System.err::println);
71 52 }
  53 +
72 54 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java deleted
1   -package cn.iocoder.yudao.framework.ai.chat;
2   -
3   -import cn.hutool.http.HttpUtil;
4   -import cn.hutool.json.JSONUtil;
5   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
6   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion;
7   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest;
8   -import okhttp3.*;
9   -import org.jetbrains.annotations.NotNull;
10   -
11   -import javax.crypto.Mac;
12   -import javax.crypto.spec.SecretKeySpec;
13   -import java.net.MalformedURLException;
14   -import java.nio.charset.Charset;
15   -import java.security.InvalidKeyException;
16   -import java.security.NoSuchAlgorithmException;
17   -import java.text.SimpleDateFormat;
18   -import java.util.*;
19   -
20   -// TODO 芋艿:整理单测
21   -/**
22   - * 讯飞星火 tests
23   - * <p>
24   - * author: fansili
25   - * time: 2024/3/11 11:00
26   - */
27   -public class XingHuoOkHttpTests {
28   -
29   - private static final String HOST_URL = "http://spark-api.xf-yun.com/v3.5/chat";
30   - private static final String API_KEY = "cb6415c19d6162cda07b47316fcb0416";
31   - private static final String API_SECRET = "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh";
32   -
33   - private XingHuoChatClient xingHuoChatClient;
34   -
35   - public static void main(String[] args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException {
36   - String authUrl = getAuthorizationUrl("spark-api.xf-yun.com", "/v3.5/chat");
37   - System.err.println(authUrl);
38   -
39   - XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id("13c8cca6");
40   - XingHuoChatCompletionRequest.Parameter parameter
41   - = new XingHuoChatCompletionRequest.Parameter()
42   - .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain("generalv3.5"));
43   -
44   -
45   - XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text();
46   - text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName());
47   - text.setContent("世界上最好的开发语言是什么?");
48   - XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload()
49   - .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text)));
50   - XingHuoChatCompletionRequest request = new XingHuoChatCompletionRequest()
51   - .setHeader(header)
52   - .setParameter(parameter)
53   - .setPayload(payload);
54   -
55   - System.err.println(JSONUtil.toJsonPrettyStr(request));
56   -
57   - OkHttpClient client = new OkHttpClient();
58   - Request request2 = new Request.Builder()
59   - .url(authUrl) // 替换为你的 wss URL
60   - .build();
61   -
62   - WebSocketListener webSocketListener = new WebSocketListener() {
63   -
64   - @Override
65   - public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
66   - boolean send = webSocket.send(JSONUtil.toJsonStr(request));
67   - System.err.println("发送 -> " + send);
68   - System.err.println("链接成功!");
69   - }
70   -
71   - @Override
72   - public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
73   - super.onMessage(webSocket, text);
74   -// System.err.println(text);
75   - XingHuoChatCompletion response = JSONUtil.toBean(text, XingHuoChatCompletion.class);
76   - for (XingHuoChatCompletion.Text text1 : response.getPayload().getChoices().getText()) {
77   - System.err.print(text1.getContent());
78   - }
79   - }
80   - };
81   -
82   - WebSocket webSocket = client.newWebSocket(request2, webSocketListener);
83   -// webSocket.send(JSONUtil.toJsonStr(request));
84   -
85   -
86   - // Trigger shutdown of the dispatcher's executor so this process can exit cleanly.
87   - client.dispatcher().executorService().shutdown();
88   - // 阻止退出
89   - Scanner scanner = new Scanner(System.in);
90   - scanner.nextLine();
91   - }
92   -
93   -
94   - /**
95   - * 获取验证请求url
96   - *
97   - * @return
98   - */
99   - public static String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException {
100   - // 获取鉴权时间 date
101   - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
102   - format.setTimeZone(TimeZone.getTimeZone("GMT"));
103   - String date = format.format(new Date());
104   -
105   - // 获取signature_origin字段
106   - StringBuilder builder = new StringBuilder("host: ").append(host).append("\n").
107   - append("date: ").append(date).append("\n").
108   - append("GET ").append(path).append(" HTTP/1.1");
109   -
110   - // 获得signatue
111   - Charset charset = Charset.forName("UTF-8");
112   - Mac mac = Mac.getInstance("hmacsha256");
113   - SecretKeySpec sp = new SecretKeySpec(API_SECRET.getBytes(charset), "hmacsha256");
114   - mac.init(sp);
115   - byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset));
116   - String signature = Base64.getEncoder().encodeToString(basebefore);
117   - //获得 authorization_origin
118   - String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", API_KEY, "hmac-sha256", "host date request-line", signature);
119   - //获得authorization
120   - String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset));
121   - // 获取httpUrl
122   - Map<String, Object> param = new HashMap<>();
123   - param.put("authorization", authorization);
124   - param.put("date", date);
125   - param.put("host", host);
126   -
127   - String toParams = HttpUtil.toParams(param);
128   - return "wss://" + host + path + "?" + toParams;
129   - }
130   -
131   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java
1 1 package cn.iocoder.yudao.framework.ai.image;
2 2  
  3 +import org.junit.jupiter.api.BeforeEach;
  4 +import org.junit.jupiter.api.Test;
3 5 import org.springframework.ai.image.ImagePrompt;
4 6 import org.springframework.ai.image.ImageResponse;
5   -import org.junit.Before;
6   -import org.junit.Test;
7 7 import org.springframework.ai.openai.OpenAiImageModel;
8 8 import org.springframework.ai.openai.api.OpenAiImageApi;
9 9  
... ... @@ -25,7 +25,7 @@ public class OpenAiImageClientTests {
25 25  
26 26 private OpenAiImageModel openAiImageClient;
27 27  
28   - @Before
  28 + @BeforeEach
29 29 public void setup() {
30 30 // 初始化 openAiImageClient
31 31 this.openAiImageClient = new OpenAiImageModel(
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java
1 1 package cn.iocoder.yudao.framework.ai.music;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
4   -import org.junit.Before;
5   -import org.junit.Test;
  4 +import org.junit.jupiter.api.Test;
6 5  
7 6 import java.util.List;
8 7  
... ... @@ -13,14 +12,8 @@ import java.util.List;
13 12 */
14 13 public class SunoTests {
15 14  
16   - private SunoApi sunoApi;
17   -
18   - @Before
19   - public void setup() {
20   - String url = "https://suno-55ishh05u-status2xxs-projects.vercel.app";
21   -// String url = "http://127.0.0.1:3001";
22   - this.sunoApi = new SunoApi(url);
23   - }
  15 + private final SunoApi sunoApi = new SunoApi("https://suno-55ishh05u-status2xxs-projects.vercel.app");
  16 +// private final SunoApi sunoApi = new SunoApi("http://127.0.0.1:3001");
24 17  
25 18 @Test
26 19 public void selectById() {
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -162,11 +162,6 @@ spring:
162 162 base-url: https://api.gptsapi.net
163 163 stabilityai:
164 164 api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
165   - vertex:
166   - ai:
167   - gemini:
168   - project-id: 1 # TODO 芋艿:缺配置
169   - location: 2
170 165 qianfan: # 文心一言
171 166 api-key: x0cuLZ7XsaTCU08vuJWO87Lg
172 167 secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
... ... @@ -176,37 +171,24 @@ spring:
176 171 tongyi:
177 172 api-key: sk-Zsd81gZYg7
178 173  
179   -yudao.ai:
180   - xinghuo:
181   - enable: true
182   - aiPlatform: XING_HUO # TODO @fan:建议每个都独立配置属性类
183   - max-tokens: 1500
184   - temperature: 0.85
185   - topP: 0.8
186   - topK: 0
187   - appId: 13c8cca6
188   - appKey: cb6415c19d6162cda07b47316fcb0416
189   - secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh
190   - model: XING_HUO_3_5
191   - qianwen:
192   - enable: true
193   - aiPlatform: QIAN_WEN
194   - max-tokens: 1500
195   - temperature: 0.85
196   - topP: 0.8
197   - topK: 0
198   - api-key: sk-Zsd81gZYg7
199   - model: QWEN_TURBO
200   - midjourney:
201   - enable: true
202   -# base-url: https://api.holdai.top/mj-relax/mj
203   - base-url: https://api.holdai.top/mj
204   - api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf
205   - notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
206   - suno:
207   - enable: true
208   -# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
209   - base-url: http://127.0.0.1:3001
  174 +yudao:
  175 + ai:
  176 + xinghuo:
  177 + enable: true
  178 + appId: 13c8cca6
  179 + appKey: cb6415c19d6162cda07b47316fcb0416
  180 + secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh
  181 + model: generalv3.5
  182 + midjourney:
  183 + enable: true
  184 + # base-url: https://api.holdai.top/mj-relax/mj
  185 + base-url: https://api.holdai.top/mj
  186 + api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf
  187 + notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
  188 + suno:
  189 + enable: true
  190 + # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
  191 + base-url: http://127.0.0.1:3001
210 192  
211 193 --- #################### 芋道相关配置 ####################
212 194  
... ...