diff --git a/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/controller/ChatController.java b/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/controller/ChatController.java index c3b3d15..cb78f95 100644 --- a/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/controller/ChatController.java +++ b/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/controller/ChatController.java @@ -1,11 +1,17 @@ package com.lanyuanxiaoyao.service.ai.chat.controller; +import cn.hutool.core.util.StrUtil; +import com.lanyuanxiaoyao.service.ai.chat.entity.MessageVO; +import com.lanyuanxiaoyao.service.ai.chat.tools.DatetimeTools; import java.io.IOException; +import org.eclipse.collections.api.list.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -26,26 +32,35 @@ public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { - this.chatClient = builder.build(); + this.chatClient = builder + .defaultSystem("始终在中文语境下进行对话") + .build(); } - private ChatClient.ChatClientRequestSpec buildRequest(String message) { + private ChatClient.ChatClientRequestSpec buildRequest(ImmutableList messages) { return chatClient.prompt() - .user(message); + .messages( + messages + .collect(message -> StrUtil.equals(message.getRole(), "assistant") + ? new AssistantMessage(message.getContent()) + : new UserMessage(message.getContent())) + .collect(message -> (Message) message) + .toList() + ); } - @PostMapping(value = "sync", consumes = "text/plain;charset=utf-8") + @PostMapping("sync") @ResponseBody - public String chatSync(@RequestBody String message) { - return buildRequest(message) + public String chatSync(@RequestBody ImmutableList messages) { + return buildRequest(messages) .call() .content(); } - @PostMapping(value = "async", consumes = "text/plain;charset=utf-8") - public SseEmitter chatAsync(@RequestBody String message) { + @PostMapping("async") + public SseEmitter chatAsync(@RequestBody ImmutableList messages) { SseEmitter emitter = new SseEmitter(); - buildRequest(message) + buildRequest(messages) .stream() .content() .subscribe( diff --git a/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/entity/MessageVO.java b/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/entity/MessageVO.java new file mode 100644 index 0000000..0272189 --- /dev/null +++ b/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/entity/MessageVO.java @@ -0,0 +1,34 @@ +package com.lanyuanxiaoyao.service.ai.chat.entity; + +/** + * @author lanyuanxiaoyao + * @version 20250516 + */ +public class MessageVO { + private String role; + private String content; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + @Override + public String toString() { + return "MessageVO{" + + "role='" + role + '\'' + + ", content='" + content + '\'' + + '}'; + } +} diff --git a/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/tools/DatetimeTools.java b/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/tools/DatetimeTools.java new file mode 100644 index 0000000..e436df4 --- /dev/null +++ b/service-ai/service-ai-chat/src/main/java/com/lanyuanxiaoyao/service/ai/chat/tools/DatetimeTools.java @@ -0,0 +1,18 @@ +package com.lanyuanxiaoyao.service.ai.chat.tools; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import org.springframework.ai.tool.annotation.Tool; + +/** + * @author lanyuanxiaoyao + * @version 20250516 + */ +public class DatetimeTools { + private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Tool(description = "获取当前时间") + public String getCurrentTime() { + return LocalDateTime.now().format(formatter); + } +} diff --git a/service-ai/service-ai-chat/src/main/resources/application.yml b/service-ai/service-ai-chat/src/main/resources/application.yml index b828a5e..723c522 100644 --- a/service-ai/service-ai-chat/src/main/resources/application.yml +++ b/service-ai/service-ai-chat/src/main/resources/application.yml @@ -2,35 +2,14 @@ spring: application: name: service-ai-chat profiles: - include: common,metrics,forest - cloud: - zookeeper: - enabled: true - connect-string: b1m2.hdp.dc:2181,b1m3.hdp.dc:2181,b1m4.hdp.dc:2181,b1m5.hdp.dc:2181,b1m6.hdp.dc:2181 - discovery: - enabled: ${spring.cloud.zookeeper.enabled} - root: /hudi-services - instance-id: ${spring.application.name}-127.0.0.1-${random.uuid}-20250514 - metadata: - discovery: zookeeper - ip: 127.0.0.1 - hostname: localhost - hostname_full: localhost - start_time: 20250514112750 - security: - meta: - authority: ENC(GXKnbq1LS11U2HaONspvH+D/TkIx13aWTaokdkzaF7HSvq6Z0Rv1+JUWFnYopVXu) - username: ENC(moIO5mO39V1Z+RDwROK9JXY4GfM8ZjDgM6Si7wRZ1MPVjbhTpmLz3lz28rAiw7c2LeCmizfJzHkEXIwGlB280g==) - darkcode: ENC(0jzpQ7T6S+P7bZrENgYsUoLhlqGvw7DA2MN3BRqEOwq7plhtg72vuuiPQNnr3DaYz0CpyTvxInhpx11W3VZ1trD6NINh7O3LN70ZqO5pWXk=) + include: random-port,common,metrics,forest ai: openai: base-url: http://132.121.206.65:10086 - api-key: '*XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTjWSn:Wn].gs/+"v:q_Q*An~zF*g-@j@jtSTv5H/,S-3:R?r9R}.' -server: - port: 8080 \ No newline at end of file + mvc: + async: + request-timeout: 300000 diff --git a/service-web/client/package.json b/service-web/client/package.json index 1284afd..74d3ed3 100644 --- a/service-web/client/package.json +++ b/service-web/client/package.json @@ -12,6 +12,7 @@ "@ant-design/icons": "^6.0.0", "@ant-design/pro-components": "^2.8.7", "@ant-design/x": "^1.2.0", + "@echofly/fetch-event-source": "^3.0.2", "@fortawesome/fontawesome-free": "^6.7.2", "@tinyflow-ai/react": "^0.1.6", "amis": "^6.12.0", diff --git a/service-web/client/pnpm-lock.yaml b/service-web/client/pnpm-lock.yaml index 90e6868..ffb61e4 100644 --- a/service-web/client/pnpm-lock.yaml +++ b/service-web/client/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@ant-design/x': specifier: ^1.2.0 version: 1.2.0(antd@5.25.0(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@echofly/fetch-event-source': + specifier: ^3.0.2 + version: 3.0.2 '@fortawesome/fontawesome-free': specifier: ^6.7.2 version: 6.7.2 @@ -257,6 +260,10 @@ packages: peerDependencies: react: '>=16.8.0' + '@echofly/fetch-event-source@3.0.2': + resolution: {integrity: sha512-woQtppGXKXGOPLgmHRoPyNflOXaE2o+0L7a/6jsrnj3y2YDltVbc0FfeuSugv7iijlI5tIHtJUcGbxf4zPFzxg==} + engines: {node: '>=16.15'} + '@emotion/hash@0.8.0': resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} @@ -3517,6 +3524,8 @@ snapshots: react: 18.3.1 tslib: 2.8.1 + '@echofly/fetch-event-source@3.0.2': {} + '@emotion/hash@0.8.0': {} '@emotion/is-prop-valid@1.2.2': diff --git a/service-web/client/src/pages/ai/Conversation.tsx b/service-web/client/src/pages/ai/Conversation.tsx index 4752d28..a728538 100644 --- a/service-web/client/src/pages/ai/Conversation.tsx +++ b/service-web/client/src/pages/ai/Conversation.tsx @@ -1,5 +1,6 @@ import {ClearOutlined, UserOutlined} from '@ant-design/icons' import {Bubble, Sender, useXAgent, useXChat, Welcome} from '@ant-design/x' +import {fetchEventSource} from '@echofly/fetch-event-source' import {Button, Divider, Flex, Switch, Tooltip, Typography} from 'antd' import markdownIt from 'markdown-it' import {useRef, useState} from 'react' @@ -30,7 +31,7 @@ const ConversationDiv = styled.div` border-left: 3px solid; padding-left: 5px; margin-bottom: 10px; - //white-space: pre-line; + white-space: pre-line; } } @@ -40,11 +41,6 @@ const ConversationDiv = styled.div` padding-right: 30px; } ` -const llmConfig = { - base: 'http://132.121.206.65:10086', - model: 'Qwen3-1.7', - secret: 'Bearer *XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj(null) @@ -52,20 +48,34 @@ function Conversation() { const [think, setThink] = useState(true) const [agent] = useXAgent<{ role: string, content: string }>({ - baseURL: `${llmConfig.base}/v1/chat/completions`, - model: llmConfig.model, - dangerouslyApiKey: llmConfig.secret, + request: async (info, callbacks) => { + await fetchEventSource('http://127.0.0.1:8080/chat/async', { + method: 'POST', + headers: { + 'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(info.messages), + signal: abortController.current?.signal, + onmessage: ev => { + console.log(ev) + callbacks.onUpdate({ + id: ev.id, + event: 'delta', + data: ev.data, + }) + }, + onclose: () => callbacks.onSuccess([]), + }) + }, }) const {onRequest, messages, setMessages} = useXChat({ agent, transformMessage: ({originMessage, chunk}) => { let text = '' try { - if (chunk?.data && !chunk?.data.includes('DONE')) { - const message = JSON.parse(chunk?.data) - text = !message?.choices?.[0].delta?.content - ? '' - : message?.choices?.[0].delta?.content + if (chunk?.data) { + text = chunk.data } } catch (error) { console.error(error) @@ -128,7 +138,6 @@ function Conversation() { )}
{