feat(ai): 完善AI对话
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
9
service-web/client/pnpm-lock.yaml
generated
9
service-web/client/pnpm-lock.yaml
generated
@@ -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':
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user