Commit 2d36ec485832d02d14d0d92fd45cb561210738a4
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,11 +33,6 @@ | ||
| 33 | <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId> | 33 | <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId> |
| 34 | <version>${spring-ai.version}</version> | 34 | <version>${spring-ai.version}</version> |
| 35 | </dependency> | 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 | <dependency> | 37 | <dependency> |
| 43 | <groupId>cn.iocoder.boot</groupId> | 38 | <groupId>cn.iocoder.boot</groupId> |
| @@ -61,8 +56,8 @@ | @@ -61,8 +56,8 @@ | ||
| 61 | 56 | ||
| 62 | <!-- Test 测试相关 --> | 57 | <!-- Test 测试相关 --> |
| 63 | <dependency> | 58 | <dependency> |
| 64 | - <groupId>junit</groupId> | ||
| 65 | - <artifactId>junit</artifactId> | 59 | + <groupId>org.springframework.boot</groupId> |
| 60 | + <artifactId>spring-boot-starter-test</artifactId> | ||
| 66 | <scope>test</scope> | 61 | <scope>test</scope> |
| 67 | </dependency> | 62 | </dependency> |
| 68 | </dependencies> | 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,8 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl; | ||
| 5 | import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; | 5 | import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| 6 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | 6 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 7 | import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; | 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 | import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; | 9 | import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; |
| 11 | import lombok.extern.slf4j.Slf4j; | 10 | import lombok.extern.slf4j.Slf4j; |
| 12 | import org.springframework.boot.autoconfigure.AutoConfiguration; | 11 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| @@ -36,21 +35,14 @@ public class YudaoAiAutoConfiguration { | @@ -36,21 +35,14 @@ public class YudaoAiAutoConfiguration { | ||
| 36 | @Bean | 35 | @Bean |
| 37 | @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") | 36 | @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") |
| 38 | public XingHuoChatClient xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { | 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 | @Bean | 48 | @Bean |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java
| 1 | package cn.iocoder.yudao.framework.ai.config; | 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 | import lombok.Data; | 3 | import lombok.Data; |
| 6 | -import lombok.experimental.Accessors; | ||
| 7 | -import org.springframework.ai.autoconfigure.openai.OpenAiImageProperties; | ||
| 8 | import org.springframework.boot.context.properties.ConfigurationProperties; | 4 | import org.springframework.boot.context.properties.ConfigurationProperties; |
| 9 | 5 | ||
| 10 | /** | 6 | /** |
| 11 | - * ai 自动配置 | 7 | + * 芋道 AI 配置类 |
| 12 | * | 8 | * |
| 13 | * @author fansili | 9 | * @author fansili |
| 14 | - * @time 2024/4/12 16:29 | ||
| 15 | * @since 1.0 | 10 | * @since 1.0 |
| 16 | */ | 11 | */ |
| 17 | -@Data | ||
| 18 | -@Accessors(chain = true) | ||
| 19 | @ConfigurationProperties(prefix = "yudao.ai") | 12 | @ConfigurationProperties(prefix = "yudao.ai") |
| 13 | +@Data | ||
| 20 | public class YudaoAiProperties { | 14 | public class YudaoAiProperties { |
| 21 | 15 | ||
| 16 | + /** | ||
| 17 | + * 讯飞星火 | ||
| 18 | + */ | ||
| 22 | private XingHuoProperties xinghuo; | 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 | @Data | 31 | @Data |
| 47 | - public static class XingHuoProperties extends ChatProperties { | 32 | + public static class XingHuoProperties { |
| 48 | 33 | ||
| 34 | + private String enable; | ||
| 49 | private String appId; | 35 | private String appId; |
| 50 | private String appKey; | 36 | private String appKey; |
| 51 | private String secretKey; | 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,8 +47,9 @@ public class YudaoAiProperties { | ||
| 57 | public static class MidjourneyProperties { | 47 | public static class MidjourneyProperties { |
| 58 | 48 | ||
| 59 | private String enable; | 49 | private String enable; |
| 60 | - private String apiKey; | ||
| 61 | private String baseUrl; | 50 | private String baseUrl; |
| 51 | + | ||
| 52 | + private String apiKey; | ||
| 62 | private String notifyUrl; | 53 | private String notifyUrl; |
| 63 | 54 | ||
| 64 | } | 55 | } |
| @@ -68,9 +59,6 @@ public class YudaoAiProperties { | @@ -68,9 +59,6 @@ public class YudaoAiProperties { | ||
| 68 | 59 | ||
| 69 | private boolean enable = false; | 60 | private boolean enable = false; |
| 70 | 61 | ||
| 71 | - /** | ||
| 72 | - * API 服务的基本地址 | ||
| 73 | - */ | ||
| 74 | private String baseUrl; | 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,7 +12,6 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | ||
| 12 | import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; | 12 | import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| 13 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | 13 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; |
| 14 | import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; | 14 | import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; |
| 15 | -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi; | ||
| 16 | import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; | 15 | import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; |
| 17 | import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; | 16 | import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; |
| 18 | import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; | 17 | import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; |
| @@ -178,11 +177,9 @@ public class AiClientFactoryImpl implements AiClientFactory { | @@ -178,11 +177,9 @@ public class AiClientFactoryImpl implements AiClientFactory { | ||
| 178 | private static XingHuoChatClient buildXingHuoChatClient(String key) { | 177 | private static XingHuoChatClient buildXingHuoChatClient(String key) { |
| 179 | List<String> keys = StrUtil.split(key, '|'); | 178 | List<String> keys = StrUtil.split(key, '|'); |
| 180 | Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); | 179 | Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); |
| 181 | - String appId = keys.get(0); | ||
| 182 | String appKey = keys.get(1); | 180 | String appKey = keys.get(1); |
| 183 | String secretKey = keys.get(2); | 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 | package cn.iocoder.yudao.framework.ai.core.model.xinghuo; | 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 | import lombok.extern.slf4j.Slf4j; | 4 | import lombok.extern.slf4j.Slf4j; |
| 5 | +import org.springframework.ai.chat.metadata.ChatGenerationMetadata; | ||
| 10 | import org.springframework.ai.chat.model.ChatModel; | 6 | import org.springframework.ai.chat.model.ChatModel; |
| 11 | import org.springframework.ai.chat.model.ChatResponse; | 7 | import org.springframework.ai.chat.model.ChatResponse; |
| 12 | import org.springframework.ai.chat.model.Generation; | 8 | import org.springframework.ai.chat.model.Generation; |
| 13 | -import org.springframework.ai.chat.model.StreamingChatModel; | ||
| 14 | import org.springframework.ai.chat.prompt.ChatOptions; | 9 | import org.springframework.ai.chat.prompt.ChatOptions; |
| 15 | import org.springframework.ai.chat.prompt.Prompt; | 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 | import org.springframework.http.ResponseEntity; | 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 | import org.springframework.retry.support.RetryTemplate; | 17 | import org.springframework.retry.support.RetryTemplate; |
| 21 | import reactor.core.publisher.Flux; | 18 | import reactor.core.publisher.Flux; |
| 22 | 19 | ||
| 23 | -import java.time.Duration; | 20 | +import java.util.HashMap; |
| 24 | import java.util.List; | 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 | @Slf4j | 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 | @Override | 65 | @Override |
| 69 | public ChatResponse call(Prompt prompt) { | 66 | public ChatResponse call(Prompt prompt) { |
| 67 | + OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false); | ||
| 70 | return this.retryTemplate.execute(ctx -> { | 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 | @Override | 109 | @Override |
| 90 | public Flux<ChatResponse> stream(Prompt prompt) { | 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 | if (prompt.getOptions() != null) { | 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
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,8 +2,7 @@ package cn.iocoder.yudao.framework.ai.core.util; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.util.StrUtil; | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | 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 | import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; | 6 | import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; |
| 8 | import org.springframework.ai.chat.messages.*; | 7 | import org.springframework.ai.chat.messages.*; |
| 9 | import org.springframework.ai.chat.prompt.ChatOptions; | 8 | import org.springframework.ai.chat.prompt.ChatOptions; |
| @@ -31,8 +30,7 @@ public class AiUtils { | @@ -31,8 +30,7 @@ public class AiUtils { | ||
| 31 | // return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); | 30 | // return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); |
| 32 | return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build(); | 31 | return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build(); |
| 33 | case XING_HUO: | 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 | case QIAN_WEN: | 34 | case QIAN_WEN: |
| 37 | return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build(); | 35 | return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build(); |
| 38 | default: | 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 | package cn.iocoder.yudao.framework.ai.chat; | 1 | package cn.iocoder.yudao.framework.ai.chat; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient; | 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 | import org.springframework.ai.chat.messages.Message; | 5 | import org.springframework.ai.chat.messages.Message; |
| 10 | import org.springframework.ai.chat.messages.SystemMessage; | 6 | import org.springframework.ai.chat.messages.SystemMessage; |
| 11 | import org.springframework.ai.chat.messages.UserMessage; | 7 | import org.springframework.ai.chat.messages.UserMessage; |
| @@ -15,58 +11,44 @@ import reactor.core.publisher.Flux; | @@ -15,58 +11,44 @@ import reactor.core.publisher.Flux; | ||
| 15 | 11 | ||
| 16 | import java.util.ArrayList; | 12 | import java.util.ArrayList; |
| 17 | import java.util.List; | 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 | public class XingHuoChatClientTests { | 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 | @Test | 26 | @Test |
| 46 | - public void callTest() { | 27 | + public void testCall() { |
| 28 | + // 准备参数 | ||
| 47 | List<Message> messages = new ArrayList<>(); | 29 | List<Message> messages = new ArrayList<>(); |
| 48 | messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | 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 | @Test | 39 | @Test |
| 56 | - public void streamTest() { | 40 | + public void testStream() { |
| 41 | + // 准备参数 | ||
| 57 | List<Message> messages = new ArrayList<>(); | 42 | List<Message> messages = new ArrayList<>(); |
| 58 | messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | 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 | package cn.iocoder.yudao.framework.ai.image; | 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 | import org.springframework.ai.image.ImagePrompt; | 5 | import org.springframework.ai.image.ImagePrompt; |
| 4 | import org.springframework.ai.image.ImageResponse; | 6 | import org.springframework.ai.image.ImageResponse; |
| 5 | -import org.junit.Before; | ||
| 6 | -import org.junit.Test; | ||
| 7 | import org.springframework.ai.openai.OpenAiImageModel; | 7 | import org.springframework.ai.openai.OpenAiImageModel; |
| 8 | import org.springframework.ai.openai.api.OpenAiImageApi; | 8 | import org.springframework.ai.openai.api.OpenAiImageApi; |
| 9 | 9 | ||
| @@ -25,7 +25,7 @@ public class OpenAiImageClientTests { | @@ -25,7 +25,7 @@ public class OpenAiImageClientTests { | ||
| 25 | 25 | ||
| 26 | private OpenAiImageModel openAiImageClient; | 26 | private OpenAiImageModel openAiImageClient; |
| 27 | 27 | ||
| 28 | - @Before | 28 | + @BeforeEach |
| 29 | public void setup() { | 29 | public void setup() { |
| 30 | // 初始化 openAiImageClient | 30 | // 初始化 openAiImageClient |
| 31 | this.openAiImageClient = new OpenAiImageModel( | 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 | package cn.iocoder.yudao.framework.ai.music; | 1 | package cn.iocoder.yudao.framework.ai.music; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; | 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 | import java.util.List; | 6 | import java.util.List; |
| 8 | 7 | ||
| @@ -13,14 +12,8 @@ import java.util.List; | @@ -13,14 +12,8 @@ import java.util.List; | ||
| 13 | */ | 12 | */ |
| 14 | public class SunoTests { | 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 | @Test | 18 | @Test |
| 26 | public void selectById() { | 19 | public void selectById() { |
yudao-server/src/main/resources/application.yaml
| @@ -162,11 +162,6 @@ spring: | @@ -162,11 +162,6 @@ spring: | ||
| 162 | base-url: https://api.gptsapi.net | 162 | base-url: https://api.gptsapi.net |
| 163 | stabilityai: | 163 | stabilityai: |
| 164 | api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx | 164 | api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx |
| 165 | - vertex: | ||
| 166 | - ai: | ||
| 167 | - gemini: | ||
| 168 | - project-id: 1 # TODO 芋艿:缺配置 | ||
| 169 | - location: 2 | ||
| 170 | qianfan: # 文心一言 | 165 | qianfan: # 文心一言 |
| 171 | api-key: x0cuLZ7XsaTCU08vuJWO87Lg | 166 | api-key: x0cuLZ7XsaTCU08vuJWO87Lg |
| 172 | secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK | 167 | secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK |
| @@ -176,37 +171,24 @@ spring: | @@ -176,37 +171,24 @@ spring: | ||
| 176 | tongyi: | 171 | tongyi: |
| 177 | api-key: sk-Zsd81gZYg7 | 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 |