feat(ai): 完善AI对话

This commit is contained in:
v-zhangjc9
2025-05-16 19:00:26 +08:00
parent 8fbc665abf
commit be976290b6
7 changed files with 116 additions and 51 deletions

View File

@@ -1,11 +1,17 @@
package com.lanyuanxiaoyao.service.ai.chat.controller; 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 java.io.IOException;
import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient; 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.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -26,26 +32,35 @@ public class ChatController {
private final ChatClient chatClient; private final ChatClient chatClient;
public ChatController(ChatClient.Builder builder) { 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<MessageVO> messages) {
return chatClient.prompt() 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 @ResponseBody
public String chatSync(@RequestBody String message) { public String chatSync(@RequestBody ImmutableList<MessageVO> messages) {
return buildRequest(message) return buildRequest(messages)
.call() .call()
.content(); .content();
} }
@PostMapping(value = "async", consumes = "text/plain;charset=utf-8") @PostMapping("async")
public SseEmitter chatAsync(@RequestBody String message) { public SseEmitter chatAsync(@RequestBody ImmutableList<MessageVO> messages) {
SseEmitter emitter = new SseEmitter(); SseEmitter emitter = new SseEmitter();
buildRequest(message) buildRequest(messages)
.stream() .stream()
.content() .content()
.subscribe( .subscribe(

View File

@@ -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 + '\'' +
'}';
}
}

View File

@@ -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);
}
}

View File

@@ -2,35 +2,14 @@ spring:
application: application:
name: service-ai-chat name: service-ai-chat
profiles: profiles:
include: common,metrics,forest include: random-port,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=)
ai: ai:
openai: openai:
base-url: http://132.121.206.65:10086 base-url: http://132.121.206.65:10086
api-key: '*XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj<MYQd$&@$sY7CgqNyea4giJi4' api-key: ENC(K+Hff9QGC+fcyi510VIDd9CaeK/IN5WBJ9rlkUsHEdDgIidW+stHHJlsK0lLPUXXREha+ToQZqqDXJrqSE+GUKCXklFhelD8bRHFXBIeP/ZzT2cxhzgKUXgjw3S0Qw2R)
chat: chat:
options: options:
model: 'Qwen3-1.7' model: 'Qwen3-1.7'
jasypt: mvc:
encryptor: async:
password: 'r#(R,P"Dp^A47>WSn:Wn].gs/+"v:q_Q*An~zF*g-@j@jtSTv5H/,S-3:R?r9R}.' request-timeout: 300000
server:
port: 8080

View File

@@ -12,6 +12,7 @@
"@ant-design/icons": "^6.0.0", "@ant-design/icons": "^6.0.0",
"@ant-design/pro-components": "^2.8.7", "@ant-design/pro-components": "^2.8.7",
"@ant-design/x": "^1.2.0", "@ant-design/x": "^1.2.0",
"@echofly/fetch-event-source": "^3.0.2",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@tinyflow-ai/react": "^0.1.6", "@tinyflow-ai/react": "^0.1.6",
"amis": "^6.12.0", "amis": "^6.12.0",

View File

@@ -17,6 +17,9 @@ importers:
'@ant-design/x': '@ant-design/x':
specifier: ^1.2.0 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) 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': '@fortawesome/fontawesome-free':
specifier: ^6.7.2 specifier: ^6.7.2
version: 6.7.2 version: 6.7.2
@@ -257,6 +260,10 @@ packages:
peerDependencies: peerDependencies:
react: '>=16.8.0' 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': '@emotion/hash@0.8.0':
resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
@@ -3517,6 +3524,8 @@ snapshots:
react: 18.3.1 react: 18.3.1
tslib: 2.8.1 tslib: 2.8.1
'@echofly/fetch-event-source@3.0.2': {}
'@emotion/hash@0.8.0': {} '@emotion/hash@0.8.0': {}
'@emotion/is-prop-valid@1.2.2': '@emotion/is-prop-valid@1.2.2':

View File

@@ -1,5 +1,6 @@
import {ClearOutlined, UserOutlined} from '@ant-design/icons' import {ClearOutlined, UserOutlined} from '@ant-design/icons'
import {Bubble, Sender, useXAgent, useXChat, Welcome} from '@ant-design/x' 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 {Button, Divider, Flex, Switch, Tooltip, Typography} from 'antd'
import markdownIt from 'markdown-it' import markdownIt from 'markdown-it'
import {useRef, useState} from 'react' import {useRef, useState} from 'react'
@@ -30,7 +31,7 @@ const ConversationDiv = styled.div`
border-left: 3px solid; border-left: 3px solid;
padding-left: 5px; padding-left: 5px;
margin-bottom: 10px; margin-bottom: 10px;
//white-space: pre-line; white-space: pre-line;
} }
} }
@@ -40,11 +41,6 @@ const ConversationDiv = styled.div`
padding-right: 30px; padding-right: 30px;
} }
` `
const llmConfig = {
base: 'http://132.121.206.65:10086',
model: 'Qwen3-1.7',
secret: 'Bearer *XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj<MYQd$&@$sY7CgqNyea4giJi4',
}
function Conversation() { function Conversation() {
const abortController = useRef<AbortController | null>(null) const abortController = useRef<AbortController | null>(null)
@@ -52,20 +48,34 @@ function Conversation() {
const [think, setThink] = useState<boolean>(true) const [think, setThink] = useState<boolean>(true)
const [agent] = useXAgent<{ role: string, content: string }>({ const [agent] = useXAgent<{ role: string, content: string }>({
baseURL: `${llmConfig.base}/v1/chat/completions`, request: async (info, callbacks) => {
model: llmConfig.model, await fetchEventSource('http://127.0.0.1:8080/chat/async', {
dangerouslyApiKey: llmConfig.secret, 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({ const {onRequest, messages, setMessages} = useXChat({
agent, agent,
transformMessage: ({originMessage, chunk}) => { transformMessage: ({originMessage, chunk}) => {
let text = '' let text = ''
try { try {
if (chunk?.data && !chunk?.data.includes('DONE')) { if (chunk?.data) {
const message = JSON.parse(chunk?.data) text = chunk.data
text = !message?.choices?.[0].delta?.content
? ''
: message?.choices?.[0].delta?.content
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -128,7 +138,6 @@ function Conversation() {
</div>)} </div>)}
<div className="conversation-sender"> <div className="conversation-sender">
<Sender <Sender
loading={agent.isRequesting()}
value={input} value={input}
onChange={setInput} onChange={setInput}
onSubmit={message => { onSubmit={message => {