diff --git a/modules/ai/src/main/java/com/bytedesk/ai/springai/dashscope/SpringAIDashscopeController.java b/modules/ai/src/main/java/com/bytedesk/ai/springai/dashscope/SpringAIDashscopeController.java index d37e655af5..3cce2b7d85 100644 --- a/modules/ai/src/main/java/com/bytedesk/ai/springai/dashscope/SpringAIDashscopeController.java +++ b/modules/ai/src/main/java/com/bytedesk/ai/springai/dashscope/SpringAIDashscopeController.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2025-02-17 11:39:17 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2025-03-07 16:40:36 + * @LastEditTime: 2025-03-07 16:53:02 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -13,30 +13,42 @@ */ package com.bytedesk.ai.springai.dashscope; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import com.bytedesk.core.annotation.UserIp; import com.bytedesk.core.utils.JsonResult; +import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Flux; +import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY; +import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY; + + /** * 阿里通义千问接口 */ @@ -49,33 +61,39 @@ public class SpringAIDashscopeController { private final SpringAIDashscopeService springAIDashscopeService; private final DashScopeChatModel bytedeskDashScopeChatModel; + private final Optional imageService; private final ExecutorService executorService = Executors.newCachedThreadPool(); + + @Qualifier("bytedeskDashScopeChatClient") + private final ChatClient bytedeskDashScopeChatClient; /** - * 方式1:同步调用 - * http://127.0.0.1:9003/springai/dashscope/chat/sync?message=hello + * ChatClient 简单调用 + * http://127.0.0.1:9003/springai/dashscope/simple/chat?query= */ - @GetMapping("/chat/sync") - public ResponseEntity> chatSync( - @RequestParam(value = "message", defaultValue = "Tell me a joke") String message) { - String response = springAIDashscopeService.processPromptSync(message); - return ResponseEntity.ok(JsonResult.success(response)); + @GetMapping("/simple/chat") + public ResponseEntity simpleChat( + @RequestParam(value = "query", defaultValue = "你好,很高兴认识你,能简单介绍一下自己吗?") String query) { + + String result = bytedeskDashScopeChatClient.prompt(query).call().content(); + + return ResponseEntity.ok(JsonResult.success(result)); } /** - * 方式2:异步流式调用 - * http://127.0.0.1:9003/springai/dashscope/chat/stream?message=hello + * ChatClient 流式调用 + * http://127.0.0.1:9003/springai/dashscope/stream/chat?query= */ - @GetMapping("/chat/stream") - public Flux chatStream( - @RequestParam(value = "message", defaultValue = "Tell me a joke") String message, HttpServletResponse response) { - // 避免返回乱码 + @GetMapping("/stream/chat") + public Flux streamChat(@RequestParam(value = "query", defaultValue = "你好,很高兴认识你,能简单介绍一下自己吗?") String query, + HttpServletResponse response) { + response.setCharacterEncoding("UTF-8"); - // 设置返回类型 - Prompt prompt = new Prompt(new UserMessage(message)); - return bytedeskDashScopeChatModel.stream(prompt); + + return bytedeskDashScopeChatClient.prompt(query).stream().content(); } + /** * 方式3:SSE调用 * http://127.0.0.1:9003/springai/dashscope/chat/sse?message=hello @@ -135,4 +153,120 @@ public class SpringAIDashscopeController { executorService.shutdown(); } } + + /** + * ChatClient 使用自定义的 Advisor 实现功能增强. + * eg: + * http://127.0.0.1:9003/springai/dashscope/advisor/chat/123?query=你好,我叫牧生,之后的会话中都带上我的名字 + * 你好,牧生!很高兴认识你。在接下来的对话中,我会记得带上你的名字。有什么想聊的吗? + * http://127.0.0.1:9003/springai/dashscope/advisor/chat/123?query=我叫什么名字? + * 你叫牧生呀。有什么事情想要分享或者讨论吗,牧生? + */ + @GetMapping("/advisor/chat/{id}") + public Flux advisorChat( + HttpServletResponse response, + @PathVariable String id, + @RequestParam String query) { + + response.setCharacterEncoding("UTF-8"); + + return this.bytedeskDashScopeChatClient.prompt(query) + .advisors( + a -> a + .param(CHAT_MEMORY_CONVERSATION_ID_KEY, id) + .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)) + .stream().content(); + } + + // http://127.0.0.1:9003/springai/image/image2text + @UserIp + @PostMapping("/image2text") + @Operation(summary = "DashScope Image Recognition") + public Flux> image2text(@RequestParam("image") MultipartFile image) { + + if (image.isEmpty()) { + return Flux.just(JsonResult.error("No image file provided")); + } + + if (imageService.isPresent()) { + return imageService.get().image2Text(image).map(JsonResult::success); + } else { + return Flux.just(JsonResult.error("Image service not enabled")); + } + + } + + // http://127.0.0.1:9003/springai/image/text2Image?prompt=A beautiful sunset + // over a calm ocean + @UserIp + @GetMapping("/text2Image") + @Operation(summary = "DashScope Image Generation") + public JsonResult text2Image( + @RequestParam(value = "prompt", defaultValue = "A beautiful sunset over a calm ocean") String prompt, + HttpServletResponse response) { + + if (prompt == null || prompt.isEmpty()) { + return JsonResult.error("Prompt is required"); + } + + if (imageService.isPresent()) { + imageService.get().text2Image(prompt, response); + } else { + return JsonResult.error("Image service not enabled"); + } + + return JsonResult.success(); + } + + /** + * audio2text + * http://127.0.0.1:9003/springai/audio/audio2text + * 用于将音频转换为文本输出 + */ + // @UserIp + // @PostMapping("/audio2text") + // @Operation(summary = "DashScope Audio Transcription") + // public Flux> audioToText(@RequestParam("audio") MultipartFile audio) { + + // if (audio.isEmpty()) { + // return Flux.just(JsonResult.error("No audio file provided")); + // } + + // if (audioService.isPresent()) { + // return audioService.get().audio2text(audio).map(JsonResult::success); + // } else { + // return Flux.just(JsonResult.error("Audio service not enabled")); + // } + // } + + /** + * text2audio + * http://127.0.0.1:9003/springai/audio/text2audio?prompt=Hello, how are you? + * 用于将文本转换为语音输出 + */ + // @UserIp + // @GetMapping("/text2audio") + // @Operation(summary = "DashScope Speech Synthesis") + // public JsonResult textToAudio( + // @RequestParam(value = "prompt", defaultValue = "Hello, how are you?") String prompt) { + + // if (prompt == null || prompt.isEmpty()) { + // return JsonResult.error("Prompt is required"); + // } + + // if (audioService.isPresent()) { + // byte[] audioData = audioService.get().text2audio(prompt); + + // // 测试验证音频数据是否为空 + // try (FileOutputStream fos = new FileOutputStream("audio.wav")) { + // fos.write(audioData); + // } catch (IOException e) { + // return JsonResult.error("Failed to save audio file: " + e.getMessage()); + // } + + // return JsonResult.success(audioData); + // } else { + // return JsonResult.error("Audio service not enabled"); + // } + // } } \ No newline at end of file diff --git a/modules/ai/src/main/java/com/bytedesk/ai/springai/dashscope/SpringAIDashscopeService.java b/modules/ai/src/main/java/com/bytedesk/ai/springai/dashscope/SpringAIDashscopeService.java index c9576e3fd1..1aa197d206 100644 --- a/modules/ai/src/main/java/com/bytedesk/ai/springai/dashscope/SpringAIDashscopeService.java +++ b/modules/ai/src/main/java/com/bytedesk/ai/springai/dashscope/SpringAIDashscopeService.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2025-02-28 17:56:26 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2025-03-07 15:37:58 + * @LastEditTime: 2025-03-07 17:00:40 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -16,6 +16,7 @@ package com.bytedesk.ai.springai.dashscope; import java.util.List; import java.util.Optional; +import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; @@ -24,7 +25,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.bytedesk.ai.springai.base.BaseSpringAIService; import com.bytedesk.ai.springai.spring.SpringAIVectorService; import com.bytedesk.core.message.IMessageSendService; @@ -38,55 +38,52 @@ import lombok.extern.slf4j.Slf4j; @ConditionalOnProperty(name = "spring.ai.dashscope.chat.enabled", havingValue = "true", matchIfMissing = false) public class SpringAIDashscopeService extends BaseSpringAIService { - private final Optional bytedeskDashScopeChatModel; + // private final Optional bytedeskDashScopeChatModel; + // private final ChatClient bytedeskDashScopeChatClient; + @Qualifier("bytedeskDashScopeChatClient") + private final ChatClient bytedeskDashScopeChatClient; public SpringAIDashscopeService( - @Qualifier("bytedeskDashScopeChatModel") Optional bytedeskDashScopeChatModel, + @Qualifier("bytedeskDashScopeChatClient") ChatClient bytedeskDashScopeChatClient, Optional springAIVectorService, IMessageSendService messageSendService) { super(springAIVectorService, messageSendService); - this.bytedeskDashScopeChatModel = bytedeskDashScopeChatModel; + // this.bytedeskDashScopeChatModel = bytedeskDashScopeChatModel; + this.bytedeskDashScopeChatClient = bytedeskDashScopeChatClient; } @Override protected void processPrompt(Prompt prompt, MessageProtobuf messageProtobuf) { - bytedeskDashScopeChatModel.ifPresent(model -> model.stream(prompt).subscribe( - response -> { - if (response != null) { - log.info("DashScope API response metadata: {}", response.getMetadata()); - List generations = response.getResults(); - for (Generation generation : generations) { - AssistantMessage assistantMessage = generation.getOutput(); - String textContent = assistantMessage.getText(); - - messageProtobuf.setType(MessageTypeEnum.STREAM); - messageProtobuf.setContent(textContent); - messageSendService.sendProtobufMessage(messageProtobuf); - } - } - }, - error -> { - log.error("DashScope API error: ", error); - messageProtobuf.setType(MessageTypeEnum.ERROR); - messageProtobuf.setContent("服务暂时不可用,请稍后重试"); - messageSendService.sendProtobufMessage(messageProtobuf); - }, - () -> log.info("Chat stream completed") - )); + bytedeskDashScopeChatClient.prompt(prompt.toString()) + .stream() + .content() + .subscribe( + content -> { + messageProtobuf.setType(MessageTypeEnum.STREAM); + messageProtobuf.setContent(content); + messageSendService.sendProtobufMessage(messageProtobuf); + }, + error -> { + log.error("DashScope API error: ", error); + messageProtobuf.setType(MessageTypeEnum.ERROR); + messageProtobuf.setContent("服务暂时不可用,请稍后重试"); + messageSendService.sendProtobufMessage(messageProtobuf); + }, + () -> log.info("Chat stream completed") + ); } @Override protected String generateFaqPairs(String prompt) { - return bytedeskDashScopeChatModel.map(model -> model.call(prompt)).orElse(""); + return bytedeskDashScopeChatClient.prompt(prompt).call().content(); } @Override protected String processPromptSync(String message) { try { - return bytedeskDashScopeChatModel.map(model -> model.call(message)) - .orElse("DashScope service is not available"); + return bytedeskDashScopeChatClient.prompt(message).call().content(); } catch (Exception e) { log.error("DashScope API sync error: ", e); return "服务暂时不可用,请稍后重试"; @@ -95,25 +92,17 @@ public class SpringAIDashscopeService extends BaseSpringAIService { @Override protected void processPromptSSE(String message, SseEmitter emitter) { - bytedeskDashScopeChatModel.ifPresentOrElse( - model -> { - Prompt prompt = new Prompt(message); - model.stream(prompt).subscribe( - response -> { + try { + bytedeskDashScopeChatClient.prompt(message) + .stream() + .content() + .subscribe( + content -> { try { - if (response != null) { - List generations = response.getResults(); - for (Generation generation : generations) { - AssistantMessage assistantMessage = generation.getOutput(); - String textContent = assistantMessage.getText(); - - // 发送SSE事件 - emitter.send(SseEmitter.event() - .data(textContent) - .id(String.valueOf(System.currentTimeMillis())) - .name("message")); - } - } + emitter.send(SseEmitter.event() + .data(content) + .id(String.valueOf(System.currentTimeMillis())) + .name("message")); } catch (Exception e) { log.error("Error sending SSE event", e); emitter.completeWithError(e); @@ -138,17 +127,9 @@ public class SpringAIDashscopeService extends BaseSpringAIService { } } ); - }, - () -> { - try { - emitter.send(SseEmitter.event() - .data("DashScope service is not available") - .name("error")); - emitter.complete(); - } catch (Exception e) { - emitter.completeWithError(e); - } - } - ); + } catch (Exception e) { + log.error("Error starting SSE stream", e); + emitter.completeWithError(e); + } } } diff --git a/modules/ai/src/main/java/com/bytedesk/ai/springai/spring/SpringAIConfig.java b/modules/ai/src/main/java/com/bytedesk/ai/springai/spring/SpringAIConfig.java index 617a89051d..c6ab35a3f2 100644 --- a/modules/ai/src/main/java/com/bytedesk/ai/springai/spring/SpringAIConfig.java +++ b/modules/ai/src/main/java/com/bytedesk/ai/springai/spring/SpringAIConfig.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2025-02-12 12:09:13 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2025-03-07 07:39:32 + * @LastEditTime: 2025-03-07 17:17:43 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -34,14 +34,6 @@ public class SpringAIConfig { private final Optional ollamaChatModel; - // @Bean("defaultChatClientBuilder") - // ChatClient.Builder defaultChatClientBuilder() { - // if (ollamaChatModel.isPresent()) { - // return ChatClient.builder(ollamaChatModel.get()); - // } - // return null; - // } - // https://docs.spring.io/spring-ai/reference/api/chatclient.html @Primary @Bean("defaultChatClient") diff --git a/modules/ai/src/main/java/com/bytedesk/ai/springai/spring/SpringAIVectorService.java b/modules/ai/src/main/java/com/bytedesk/ai/springai/spring/SpringAIVectorService.java index 7079b3f9fb..cb72398449 100644 --- a/modules/ai/src/main/java/com/bytedesk/ai/springai/spring/SpringAIVectorService.java +++ b/modules/ai/src/main/java/com/bytedesk/ai/springai/spring/SpringAIVectorService.java @@ -2,7 +2,7 @@ * @Author: jackning 270580156@qq.com * @Date: 2024-07-27 21:27:01 * @LastEditors: jackning 270580156@qq.com - * @LastEditTime: 2025-03-06 16:52:59 + * @LastEditTime: 2025-03-07 17:11:31 * @Description: bytedesk.com https://github.com/Bytedesk/bytedesk * Please be aware of the BSL license restrictions before installing Bytedesk IM – * selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license. @@ -42,7 +42,6 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.HashMap; -import com.alibaba.fastjson2.JSON; import com.bytedesk.ai.springai.spring.event.VectorSplitEvent; import com.bytedesk.ai.utils.reader.WebDocumentReader; import com.bytedesk.core.config.BytedeskEventPublisher;