diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index aaba1191d..d7241b3ec 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -14,6 +14,7 @@ yudao-module-ai-api yudao-module-ai-biz + yudao-spring-boot-starter-ai diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index 8ca4bccc3..862f7fe8d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -100,6 +100,14 @@ 4.12.0 test + + io.projectreactor.netty + reactor-netty + + + cn.hutool + hutool-all + \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoApi.java new file mode 100644 index 000000000..58c21349b --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoApi.java @@ -0,0 +1,154 @@ +package cn.iocoder.yudao.framework.ai.chatxinghuo; + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletion; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletionRequest; +import lombok.Data; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URI; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 讯飞星火 属性、api + *

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

+ * author: fansili + * time: 2024/3/11 10:12 + */ +@Data +public class XingHuoApi { + + private static final String DEFAULT_BASE_URL = "wss://spark-api.xf-yun.com"; + + private String appId; + private String appKey; + private String secretKey; + private WebClient webClient; + private XingHuoChatModel useChatModel; + // 创建 WebSocketClient 实例 + private ReactorNettyWebSocketClient socketClient = new ReactorNettyWebSocketClient(); + + public XingHuoApi(String appId, String appKey, String secretKey, XingHuoChatModel useChatModel) { + this.appId = appId; + this.appKey = appKey; + this.secretKey = secretKey; + this.useChatModel = useChatModel; + + } + + public ResponseEntity chatCompletionEntity(XingHuoChatCompletionRequest request) { + String authUrl; + try { + authUrl = getAuthorizationUrl("spark-api.xf-yun.com", useChatModel.getUri()); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(e); + } + // wss 请求的 URI + URI uri = URI.create(authUrl); + // 发起 wss 请求并处理响应 + Flux messageFlux = Flux.create(sink -> { + socketClient.execute(uri, session -> + session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request)))) + .thenMany(session.receive() + .map(WebSocketMessage -> { + return JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class); + }) + .doOnNext(sink::next) // 将接收到的消息推送到 Flux 中 + .doOnError(sink::error) // 处理错误 + .doOnTerminate(sink::complete)) // 完成时关闭 sink + .then()) + .subscribe(); // 订阅以开始会话 + }); + // 阻塞获取所有结果 + List responseList = messageFlux.collectList().block(); + // 拼接 content + String responseContent = responseList.stream().map(item -> { + // 获取 content + return item.getPayload().getChoices().getText().stream().map(XingHuoChatCompletion.Text::getContent).collect(Collectors.joining()); + }).collect(Collectors.joining()); + // 将多个合并成一个 + XingHuoChatCompletion xingHuoChatCompletion = new XingHuoChatCompletion(); + xingHuoChatCompletion.setPayload(new XingHuoChatCompletion.Payload().setChoices(new XingHuoChatCompletion.Choices().setText(List.of(new XingHuoChatCompletion.Text().setContent(responseContent))))); + return new ResponseEntity<>(xingHuoChatCompletion, HttpStatusCode.valueOf(200)); + } + + + /** + * 获取验证请求url + * + * @return + */ + public String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException { + // 获取鉴权时间 date + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + String date = format.format(new Date()); + + // 获取signature_origin字段 + StringBuilder builder = new StringBuilder("host: ").append(host).append("\n"). + append("date: ").append(date).append("\n"). + append("GET ").append(path).append(" HTTP/1.1"); + + // 获得signatue + Charset charset = Charset.forName("UTF-8"); + Mac mac = Mac.getInstance("hmacsha256"); + SecretKeySpec sp = new SecretKeySpec(secretKey.getBytes(charset), "hmacsha256"); + mac.init(sp); + byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset)); + String signature = Base64.getEncoder().encodeToString(basebefore); + //获得 authorization_origin + String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", appKey, "hmac-sha256", "host date request-line", signature); + //获得authorization + String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset)); + // 获取httpUrl + Map param = new HashMap<>(); + param.put("authorization", authorization); + param.put("date", date); + param.put("host", host); + + String toParams = HttpUtil.toParams(param); + return "wss://" + host + path + "?" + toParams; + } + + public Flux chatCompletionStream(XingHuoChatCompletionRequest request) { + String authUrl; + try { + authUrl = getAuthorizationUrl("spark-api.xf-yun.com", useChatModel.getUri()); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException(e); + } + System.err.println(authUrl); + System.err.println(JSONUtil.toJsonPrettyStr(request)); + // wss 请求的 URI + URI uri = URI.create(authUrl); + // 发起 wss 请求并处理响应 + // 创建一个 Flux 来处理接收到的消息 + Flux messageFlux = Flux.create(sink -> { + socketClient.execute(uri, session -> + session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request)))) + .thenMany(session.receive() + .map(WebSocketMessage -> JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class)) + .doOnNext(sink::next) // 将接收到的消息推送到 Flux 中 + .doOnError(sink::error) // 处理错误 + .doOnTerminate(sink::complete)) // 完成时关闭 sink + .then()) + .subscribe(); // 订阅以开始会话 + }); + return messageFlux; + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatClient.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatClient.java new file mode 100644 index 000000000..a750e1c8f --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatClient.java @@ -0,0 +1,130 @@ +package cn.iocoder.yudao.framework.ai.chatxinghuo; + +import cn.iocoder.yudao.framework.ai.chat.ChatClient; +import cn.iocoder.yudao.framework.ai.chat.ChatResponse; +import cn.iocoder.yudao.framework.ai.chat.Generation; +import cn.iocoder.yudao.framework.ai.chat.StreamingChatClient; +import cn.iocoder.yudao.framework.ai.chat.prompt.Prompt; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletion; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletionMessage; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletionRequest; +import cn.iocoder.yudao.framework.ai.chatxinghuo.exception.XingHuoApiException; +import cn.iocoder.yudao.framework.ai.model.function.AbstractFunctionCallSupport; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.retry.support.RetryTemplate; +import reactor.core.publisher.Flux; + +import java.time.Duration; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 讯飞星火 client + *

+ * author: fansili + * time: 2024/3/11 10:19 + */ +@Slf4j +public class XingHuoChatClient extends AbstractFunctionCallSupport> + implements ChatClient, StreamingChatClient { + + private XingHuoApi xingHuoApi; + + public final RetryTemplate retryTemplate = RetryTemplate.builder() + // 最大重试次数 10 + .maxAttempts(10) + .retryOn(XingHuoApiException.class) + // 最大重试5次,第一次间隔3000ms,第二次3000ms * 2,第三次3000ms * 3,以此类推,最大间隔3 * 60000ms + .exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000)) + .withListener(new RetryListener() { + @Override + public void onError(RetryContext context, + RetryCallback callback, Throwable throwable) { + log.warn("重试异常:" + context.getRetryCount(), throwable); + } + + ; + }) + .build(); + + public XingHuoChatClient(XingHuoApi xingHuoApi) { + super(null); + this.xingHuoApi = xingHuoApi; + } + + @Override + public ChatResponse call(Prompt prompt) { + + return this.retryTemplate.execute(ctx -> { + // ctx 会有重试的信息 + // 创建 request 请求,stream模式需要供应商支持 + XingHuoChatCompletionRequest request = this.createRequest(prompt, false); + // 调用 callWithFunctionSupport 发送请求 + ResponseEntity response = this.callWithFunctionSupport(request); + // 获取结果封装 ChatResponse + return new ChatResponse(List.of(new Generation(response.getBody().getPayload().getChoices().getText().get(0).getContent()))); + }); + } + + private XingHuoChatCompletionRequest createRequest(Prompt prompt, boolean b) { + // 创建 header + XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id(xingHuoApi.getAppId()); + // 创建 params + XingHuoChatCompletionRequest.Parameter parameter = new XingHuoChatCompletionRequest.Parameter() + .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain(xingHuoApi.getUseChatModel().getValue())); + // 创建 payload text 信息 + XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text(); + text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName()); + text.setContent(prompt.getContents()); + // 创建 payload + XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload() + .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text))); + // 创建 request + return new XingHuoChatCompletionRequest() + .setHeader(header) + .setParameter(parameter) + .setPayload(payload); + } + + @Override + public Flux stream(Prompt prompt) { + // 创建 request 请求,stream模式需要供应商支持 + XingHuoChatCompletionRequest request = this.createRequest(prompt, false); + // 发送请求 + Flux response = this.xingHuoApi.chatCompletionStream(request); + return response.map(res -> { + String content = res.getPayload().getChoices().getText().stream() + .map(item -> item.getContent()).collect(Collectors.joining()); + return new ChatResponse(List.of(new Generation(content))); + }); + } + + @Override + protected XingHuoChatCompletionRequest doCreateToolResponseRequest(XingHuoChatCompletionRequest previousRequest, XingHuoChatCompletionMessage responseMessage, List conversationHistory) { + return null; + } + + @Override + protected List doGetUserMessages(XingHuoChatCompletionRequest request) { + return null; + } + + @Override + protected XingHuoChatCompletionMessage doGetToolResponseMessage(ResponseEntity response) { + return null; + } + + @Override + protected ResponseEntity doChatCompletion(XingHuoChatCompletionRequest request) { + return xingHuoApi.chatCompletionEntity(request); + } + + @Override + protected boolean isToolFunctionCall(ResponseEntity response) { + return false; + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatModel.java new file mode 100644 index 000000000..e00a622b0 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/XingHuoChatModel.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.framework.ai.chatxinghuo; + +import lombok.Getter; + +/** + * 讯飞星火 模型 + * + * 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E + * + * 1tokens 约等于1.5个中文汉字 或者 0.8个英文单词 + * 星火V1.5支持[搜索]内置插件;星火V2.0、V3.0和V3.5支持[搜索]、[天气]、[日期]、[诗词]、[字词]、[股票]六个内置插件 + * 星火V3.5 现已支持system、Function Call 功能。 + * + * author: fansili + * time: 2024/3/11 10:12 + */ +@Getter +public enum XingHuoChatModel { + +// 文档地址:https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E +// general指向V1.5版本; +// generalv2指向V2版本; +// generalv3指向V3版本; +// generalv3.5指向V3.5版本; + + XING_HUO_1_5("星火大模型1.5", "general", "/v1.1/chat"), + XING_HUO_2_0("星火大模型2.0", "generalv2", "/v2.1/chat"), + XING_HUO_3_0("星火大模型3.0", "generalv3", "/v3.1/chat"), + XING_HUO_3_5("星火大模型3.5", "generalv3.5", "/v3.5/chat"), + + ; + + XingHuoChatModel(String name, String value, String uri) { + this.name = name; + this.value = value; + this.uri = uri; + } + + private String name; + + private String value; + + private String uri; +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletion.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletion.java new file mode 100644 index 000000000..a0f965327 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletion.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.framework.ai.chatxinghuo.api; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * author: fansili + * time: 2024/3/11 10:20 + */ +@Data +@Accessors(chain = true) +public class XingHuoChatCompletion { + private Header header; + private Payload payload; + + @Data + @Accessors(chain = true) + public static class Header { + private int code; + private String message; + private String sid; + private int status; + } + + @Data + @Accessors(chain = true) + public static class Payload { + private Choices choices; + } + + @Data + @Accessors(chain = true) + public static class Choices { + private int status; + private int seq; + private List text; + } + + @Data + @Accessors(chain = true) + public static class Text { + private String content; + private String role; + private int index; + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionMessage.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionMessage.java new file mode 100644 index 000000000..faa3c8c73 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionMessage.java @@ -0,0 +1,8 @@ +package cn.iocoder.yudao.framework.ai.chatxinghuo.api; + +/** + * author: fansili + * time: 2024/3/11 10:20 + */ +public class XingHuoChatCompletionMessage { +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionRequest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionRequest.java new file mode 100644 index 000000000..009789cb3 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/api/XingHuoChatCompletionRequest.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.framework.ai.chatxinghuo.api; + +import lombok.Data; +import lombok.Getter; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 讯飞星火 request + * + * author: fansili + * time: 2024/3/11 10:20 + */ +@Data +@Accessors(chain = true) +public class XingHuoChatCompletionRequest { + + private Header header; + private Parameter parameter; + private Payload payload; + + @Data + @Accessors(chain = true) + public static class Header { + private String app_id; + private String uid; + } + + @Data + @Accessors(chain = true) + public static class Parameter { + private Chat chat; + + @Data + @Accessors(chain = true) + public static class Chat { + /** + * https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E + * + * 指定访问的领域: + * general指向V1.5版本; + * generalv2指向V2版本; + * generalv3指向V3版本; + * generalv3.5指向V3.5版本; + * 注意:不同的取值对应的url也不一样! + */ + private String domain = "general"; + private Double temperature = 0.5; + private Integer max_tokens = 2048; + } + } + + @Data + @Accessors(chain = true) + public static class Payload { + private Message message; + + @Data + @Accessors(chain = true) + public static class Message { + private List text; + + + @Data + @Accessors(chain = true) + public static class Text { + /** + * 角色 + */ + private String role; + /** + * 消息内容 + */ + private String content; + private Integer index; + + @Getter + public static enum Role { + SYSTEM("system"), + USER("user"), + ASSISTANT("assistant"); + private String name; + + private Role(String name) { + this.name = name; + } + } + } + } + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/exception/XingHuoApiException.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/exception/XingHuoApiException.java new file mode 100644 index 000000000..cde6147c7 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatxinghuo/exception/XingHuoApiException.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.framework.ai.chatxinghuo.exception; + +/** + * 讯飞星火 exception + * + * author: fansili + * time: 2024/3/11 10:22 + */ +public class XingHuoApiException extends RuntimeException { + + public XingHuoApiException(String message) { + super(message); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java new file mode 100644 index 000000000..674e4dc6d --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientMainTests.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletion; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletionRequest; +import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; +import org.springframework.web.reactive.socket.client.WebSocketClient; +import reactor.core.publisher.Flux; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * author: fansili + * time: 2024/3/13 20:47 + */ +public class XingHuoChatClientMainTests { + + + private static final String HOST_URL = "http://spark-api.xf-yun.com/v3.5/chat"; + private static final String API_KEY = "cb6415c19d6162cda07b47316fcb0416"; + private static final String API_SECRET = "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"; + + public static void main(String[] args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException { + String authUrl = getAuthorizationUrl("spark-api.xf-yun.com", "/v3.5/chat"); + System.err.println(authUrl); + + XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id("13c8cca6"); + XingHuoChatCompletionRequest.Parameter parameter + = new XingHuoChatCompletionRequest.Parameter() + .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain("generalv3.5")); + + + XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text(); + text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName()); + text.setContent("世界上最好的开发语言是什么?"); + XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload() + .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text))); + XingHuoChatCompletionRequest request = new XingHuoChatCompletionRequest() + .setHeader(header) + .setParameter(parameter) + .setPayload(payload); + + System.err.println(JSONUtil.toJsonPrettyStr(request)); + + + // 创建 WebSocketClient 实例 + WebSocketClient client = new ReactorNettyWebSocketClient(); + + // wss 请求的 URI + URI uri = URI.create(authUrl); + + // 发起 wss 请求并处理响应 + client.execute(uri, session -> + // 使用会话发送消息,并接收回应 + session.send(Flux.just(session.textMessage(JSONUtil.toJsonStr(request)))) + .thenMany(session.receive() + .map(WebSocketMessage -> { + System.err.println(WebSocketMessage.getPayloadAsText()); + return JSONUtil.toBean(WebSocketMessage.getPayloadAsText(), XingHuoChatCompletion.class); + }) + .log()) // 打印接收到的消息 + .then()) + .block(); // 等待操作完成或超时 + + // 阻止退出 + Scanner scanner = new Scanner(System.in); + scanner.nextLine(); + } + + + /** + * 获取验证请求url + * + * @return + */ + public static String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException { + // 获取鉴权时间 date + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + String date = format.format(new Date()); + + // 获取signature_origin字段 + StringBuilder builder = new StringBuilder("host: ").append(host).append("\n"). + append("date: ").append(date).append("\n"). + append("GET ").append(path).append(" HTTP/1.1"); + + // 获得signatue + Charset charset = Charset.forName("UTF-8"); + Mac mac = Mac.getInstance("hmacsha256"); + SecretKeySpec sp = new SecretKeySpec(API_SECRET.getBytes(charset), "hmacsha256"); + mac.init(sp); + byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset)); + String signature = Base64.getEncoder().encodeToString(basebefore); + //获得 authorization_origin + String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", API_KEY, "hmac-sha256", "host date request-line", signature); + //获得authorization + String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset)); + // 获取httpUrl + Map param = new HashMap<>(); + param.put("authorization", authorization); + param.put("date", date); + param.put("host", host); + + String toParams = HttpUtil.toParams(param); + return "wss://" + host + path + "?" + toParams; + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java new file mode 100644 index 000000000..da8bc9d5d --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.chat.prompt.Prompt; +import cn.iocoder.yudao.framework.ai.chatxinghuo.XingHuoApi; +import cn.iocoder.yudao.framework.ai.chatxinghuo.XingHuoChatClient; +import cn.iocoder.yudao.framework.ai.chatxinghuo.XingHuoChatModel; +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; + +import java.util.Scanner; +import java.util.function.Consumer; + +/** + * 讯飞星火 tests + *

+ * author: fansili + * time: 2024/3/11 11:00 + */ +public class XingHuoChatClientTests { + + private XingHuoChatClient xingHuoChatClient; + + @Before + public void setup() { + // 初始化 xingHuoChatClient + xingHuoChatClient = new XingHuoChatClient( + new XingHuoApi( + "13c8cca6", + "cb6415c19d6162cda07b47316fcb0416", + "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh", + XingHuoChatModel.XING_HUO_3_5 + ) + ); + } + + @Test + public void callTest() { + ChatResponse call = xingHuoChatClient.call(new Prompt("java和go那个性能更好!")); + System.err.println(call.getResult()); + } + + @Test + public void streamTest() { + Flux stream = xingHuoChatClient.stream(new Prompt("java和go那个性能更好!")); + stream.subscribe(new Consumer() { + @Override + public void accept(ChatResponse chatResponse) { + System.err.print(chatResponse.getResult().getOutput().getContent()); + } + }); + // 阻止退出 + Scanner scanner = new Scanner(System.in); + scanner.nextLine(); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java new file mode 100644 index 000000000..3f2903201 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoOkHttpTests.java @@ -0,0 +1,130 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.ai.chatxinghuo.XingHuoChatClient; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletion; +import cn.iocoder.yudao.framework.ai.chatxinghuo.api.XingHuoChatCompletionRequest; +import okhttp3.*; +import org.jetbrains.annotations.NotNull; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.MalformedURLException; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 讯飞星火 tests + *

+ * author: fansili + * time: 2024/3/11 11:00 + */ +public class XingHuoOkHttpTests { + + private static final String HOST_URL = "http://spark-api.xf-yun.com/v3.5/chat"; + private static final String API_KEY = "cb6415c19d6162cda07b47316fcb0416"; + private static final String API_SECRET = "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"; + + private XingHuoChatClient xingHuoChatClient; + + public static void main(String[] args) throws MalformedURLException, NoSuchAlgorithmException, InvalidKeyException { + String authUrl = getAuthorizationUrl("spark-api.xf-yun.com", "/v3.5/chat"); + System.err.println(authUrl); + + XingHuoChatCompletionRequest.Header header = new XingHuoChatCompletionRequest.Header().setApp_id("13c8cca6"); + XingHuoChatCompletionRequest.Parameter parameter + = new XingHuoChatCompletionRequest.Parameter() + .setChat(new XingHuoChatCompletionRequest.Parameter.Chat().setDomain("generalv3.5")); + + + XingHuoChatCompletionRequest.Payload.Message.Text text = new XingHuoChatCompletionRequest.Payload.Message.Text(); + text.setRole(XingHuoChatCompletionRequest.Payload.Message.Text.Role.USER.getName()); + text.setContent("世界上最好的开发语言是什么?"); + XingHuoChatCompletionRequest.Payload payload = new XingHuoChatCompletionRequest.Payload() + .setMessage(new XingHuoChatCompletionRequest.Payload.Message().setText(List.of(text))); + XingHuoChatCompletionRequest request = new XingHuoChatCompletionRequest() + .setHeader(header) + .setParameter(parameter) + .setPayload(payload); + + System.err.println(JSONUtil.toJsonPrettyStr(request)); + + OkHttpClient client = new OkHttpClient(); + Request request2 = new Request.Builder() + .url(authUrl) // 替换为你的 wss URL + .build(); + + WebSocketListener webSocketListener = new WebSocketListener() { + + @Override + public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { + boolean send = webSocket.send(JSONUtil.toJsonStr(request)); + System.err.println("发送 -> " + send); + System.err.println("链接成功!"); + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { + super.onMessage(webSocket, text); +// System.err.println(text); + XingHuoChatCompletion response = JSONUtil.toBean(text, XingHuoChatCompletion.class); + for (XingHuoChatCompletion.Text text1 : response.getPayload().getChoices().getText()) { + System.err.print(text1.getContent()); + } + } + }; + + WebSocket webSocket = client.newWebSocket(request2, webSocketListener); +// webSocket.send(JSONUtil.toJsonStr(request)); + + + // Trigger shutdown of the dispatcher's executor so this process can exit cleanly. + client.dispatcher().executorService().shutdown(); + // 阻止退出 + Scanner scanner = new Scanner(System.in); + scanner.nextLine(); + } + + + /** + * 获取验证请求url + * + * @return + */ + public static String getAuthorizationUrl(String host, String path) throws NoSuchAlgorithmException, InvalidKeyException { + // 获取鉴权时间 date + SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("GMT")); + String date = format.format(new Date()); + + // 获取signature_origin字段 + StringBuilder builder = new StringBuilder("host: ").append(host).append("\n"). + append("date: ").append(date).append("\n"). + append("GET ").append(path).append(" HTTP/1.1"); + + // 获得signatue + Charset charset = Charset.forName("UTF-8"); + Mac mac = Mac.getInstance("hmacsha256"); + SecretKeySpec sp = new SecretKeySpec(API_SECRET.getBytes(charset), "hmacsha256"); + mac.init(sp); + byte[] basebefore = mac.doFinal(builder.toString().getBytes(charset)); + String signature = Base64.getEncoder().encodeToString(basebefore); + //获得 authorization_origin + String authorization_origin = String.format("api_key=\"%s\",algorithm=\"%s\",headers=\"%s\",signature=\"%s\"", API_KEY, "hmac-sha256", "host date request-line", signature); + //获得authorization + String authorization = Base64.getEncoder().encodeToString(authorization_origin.getBytes(charset)); + // 获取httpUrl + Map param = new HashMap<>(); + param.put("authorization", authorization); + param.put("date", date); + param.put("host", host); + + String toParams = HttpUtil.toParams(param); + return "wss://" + host + path + "?" + toParams; + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatTests.java new file mode 100644 index 000000000..346ffedb1 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatTests.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.chat.prompt.Prompt; +import cn.iocoder.yudao.framework.ai.chatyiyan.YiYanApi; +import cn.iocoder.yudao.framework.ai.chatyiyan.YiYanChatClient; +import cn.iocoder.yudao.framework.ai.chatyiyan.YiYanChatModel; +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Flux; + +import java.util.Scanner; +import java.util.function.Consumer; + +/** + * chat 文心一言 + * + * author: fansili + * time: 2024/3/12 20:59 + */ +public class YiYanChatTests { + + private YiYanChatClient yiYanChatClient; + + @Before + public void setup() { + YiYanApi yiYanApi = new YiYanApi( + "x0cuLZ7XsaTCU08vuJWO87Lg", + "R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK", + YiYanChatModel.ERNIE4_3_5_8K, + 86400 + ); + yiYanChatClient = new YiYanChatClient(yiYanApi); + } + + @Test + public void callTest() { + ChatResponse call = yiYanChatClient.call(new Prompt("什么编程语言最好?")); + System.err.println(call.getResult()); + } + + @Test + public void streamTest() { + Flux fluxResponse = yiYanChatClient.stream(new Prompt("用java帮我写一个快排算法?")); + fluxResponse.subscribe(new Consumer() { + @Override + public void accept(ChatResponse chatResponse) { + System.err.print(chatResponse.getResult().getOutput().getContent()); + } + }); + // 阻止退出 + Scanner scanner = new Scanner(System.in); + scanner.nextLine(); + } +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/package-info.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/package-info.java new file mode 100644 index 000000000..1678ee0ae --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/package-info.java @@ -0,0 +1,5 @@ +/** + * author: fansili + * time: 2024/3/12 21:03 + */ +package cn.iocoder.yudao.framework.ai; \ No newline at end of file