feat(chat): 实现AI客服助手与手机号码识别功能
- 新增AI助手提示词模板,定义角色、目标与交互规则 - 实现手机号自动识别并触发消息发送工具- 添加RabbitMQ配置与消息收发组件 - 集成SendMessage工具支持用户留资通知 - 引入会话上下文管理工具类ConversationContext - 升级聊天客户端配置,加载系统提示词与默认工具 - 增加数据库操作工具日志记录 - 添加Spring AMQP与Jackson依赖支持消息队列通信
This commit is contained in:
8
pom.xml
8
pom.xml
@@ -83,6 +83,14 @@
|
|||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
|||||||
import com.alibaba.cloud.ai.memory.redis.BaseRedisChatMemoryRepository;
|
import com.alibaba.cloud.ai.memory.redis.BaseRedisChatMemoryRepository;
|
||||||
import com.alibaba.cloud.ai.memory.redis.LettuceRedisChatMemoryRepository;
|
import com.alibaba.cloud.ai.memory.redis.LettuceRedisChatMemoryRepository;
|
||||||
import com.hanserwei.chat.tools.AiDBTools;
|
import com.hanserwei.chat.tools.AiDBTools;
|
||||||
|
import com.hanserwei.chat.tools.SendMQMessageTools;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
|
import org.springframework.ai.chat.client.advisor.PromptChatMemoryAdvisor;
|
||||||
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
|
|
||||||
import org.springframework.ai.chat.memory.ChatMemory;
|
import org.springframework.ai.chat.memory.ChatMemory;
|
||||||
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
|
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -32,6 +32,9 @@ public class ChatClientConfiguration {
|
|||||||
@Resource
|
@Resource
|
||||||
private AiDBTools aiDBTools;
|
private AiDBTools aiDBTools;
|
||||||
|
|
||||||
|
@Value("classpath:prompt/aiAssistant.st")
|
||||||
|
private org.springframework.core.io.Resource aiAssistantResource;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public BaseRedisChatMemoryRepository redisChatMemoryRepository() {
|
public BaseRedisChatMemoryRepository redisChatMemoryRepository() {
|
||||||
// 构建RedissonRedisChatMemoryRepository实例
|
// 构建RedissonRedisChatMemoryRepository实例
|
||||||
@@ -52,10 +55,11 @@ public class ChatClientConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ChatClient dashScopeChatClient(ChatMemory chatMemory) {
|
public ChatClient dashScopeChatClient(ChatMemory chatMemory, SendMQMessageTools sendMQMessageTools) {
|
||||||
return ChatClient.builder(dashScopeChatModel)
|
return ChatClient.builder(dashScopeChatModel)
|
||||||
.defaultTools(aiDBTools)
|
.defaultTools(aiDBTools, sendMQMessageTools)
|
||||||
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build(), new SimpleLoggerAdvisor())
|
.defaultSystem(aiAssistantResource)
|
||||||
|
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.hanserwei.chat.config;
|
||||||
|
|
||||||
|
import org.springframework.amqp.core.Binding;
|
||||||
|
import org.springframework.amqp.core.BindingBuilder;
|
||||||
|
import org.springframework.amqp.core.DirectExchange;
|
||||||
|
import org.springframework.amqp.core.Queue;
|
||||||
|
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
|
||||||
|
import org.springframework.amqp.support.converter.MessageConverter;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RabbitMQConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DirectExchange directExchange() {
|
||||||
|
return new DirectExchange("chat.exchange");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Queue queue() {
|
||||||
|
return new Queue("chat.queue");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Binding binding() {
|
||||||
|
return BindingBuilder.bind(queue())
|
||||||
|
.to(directExchange())
|
||||||
|
.with("chat.routing.key");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MessageConverter messageConverter() {
|
||||||
|
return new Jackson2JsonMessageConverter();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.hanserwei.chat.consumer;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.amqp.core.Message;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RabbitMQConsumer {
|
||||||
|
|
||||||
|
@RabbitListener(queues = "chat.queue")
|
||||||
|
public void receiveMessage(Object message) {
|
||||||
|
String messageText = extractMessage(message);
|
||||||
|
log.info("Received message text:\n{}", messageText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractMessage(Object message) {
|
||||||
|
return switch (message) {
|
||||||
|
case null -> "";
|
||||||
|
case String str -> str;
|
||||||
|
case byte[] body -> new String(body, StandardCharsets.UTF_8);
|
||||||
|
case Message amqpMessage -> new String(amqpMessage.getBody(), StandardCharsets.UTF_8);
|
||||||
|
default -> String.valueOf(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ package com.hanserwei.chat.controller;
|
|||||||
|
|
||||||
import com.hanserwei.chat.model.dto.ChatMessageDTO;
|
import com.hanserwei.chat.model.dto.ChatMessageDTO;
|
||||||
import com.hanserwei.chat.model.vo.AIResponse;
|
import com.hanserwei.chat.model.vo.AIResponse;
|
||||||
|
import com.hanserwei.chat.tools.SendMQMessageTools;
|
||||||
|
import com.hanserwei.chat.utils.ConversationContext;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
import org.springframework.ai.chat.client.ChatClient;
|
||||||
import org.springframework.ai.chat.memory.ChatMemory;
|
import org.springframework.ai.chat.memory.ChatMemory;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -12,15 +15,26 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/ai")
|
@RequestMapping("/ai")
|
||||||
public class AiChatController {
|
public class AiChatController {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ChatClient dashScopeChatClient;
|
private ChatClient dashScopeChatClient;
|
||||||
|
@Resource
|
||||||
|
private SendMQMessageTools sendMQMessageTools;
|
||||||
|
|
||||||
|
private static final Pattern PHONE_PATTERN = Pattern.compile("(?<!\\d)(1[3-9]\\d{9})(?!\\d)");
|
||||||
|
|
||||||
@PostMapping(path = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
@PostMapping(path = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
public Flux<AIResponse> chatWithAi(@RequestBody ChatMessageDTO chatMessageDTO) {
|
public Flux<AIResponse> chatWithAi(@RequestBody ChatMessageDTO chatMessageDTO) {
|
||||||
|
log.info("会话ID:{}", chatMessageDTO.getConversionId());
|
||||||
|
ConversationContext.setConversationId(chatMessageDTO.getConversionId());
|
||||||
|
triggerSendMessageIfPhonePresent(chatMessageDTO);
|
||||||
|
|
||||||
return dashScopeChatClient.prompt()
|
return dashScopeChatClient.prompt()
|
||||||
.user(chatMessageDTO.getMessage())
|
.user(chatMessageDTO.getMessage())
|
||||||
@@ -29,8 +43,26 @@ public class AiChatController {
|
|||||||
.chatResponse()
|
.chatResponse()
|
||||||
.mapNotNull(chatResponse -> AIResponse.builder()
|
.mapNotNull(chatResponse -> AIResponse.builder()
|
||||||
.v(chatResponse.getResult().getOutput().getText())
|
.v(chatResponse.getResult().getOutput().getText())
|
||||||
.build());
|
.build())
|
||||||
|
.contextWrite(ctx -> ConversationContext.withConversationId(chatMessageDTO.getConversionId()))
|
||||||
|
.doFinally(signalType -> ConversationContext.clear());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void triggerSendMessageIfPhonePresent(ChatMessageDTO chatMessageDTO) {
|
||||||
|
String message = chatMessageDTO.getMessage();
|
||||||
|
if (message == null || message.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher matcher = PHONE_PATTERN.matcher(message);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String phoneNumber = matcher.group(1);
|
||||||
|
log.info("检测到手机号:{},会话ID:{}", phoneNumber, chatMessageDTO.getConversionId());
|
||||||
|
sendMQMessageTools.sendMQMessage(phoneNumber)
|
||||||
|
.doOnError(error -> log.error("触发发送消息工具失败,手机号:{},错误:{}", phoneNumber, error.getMessage(), error))
|
||||||
|
.subscribe(result -> log.info("已触发发送消息工具,手机号:{},结果:{}", phoneNumber, result));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.hanserwei.chat.publisher;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RabbitMQPublisher {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RabbitTemplate rabbitTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*
|
||||||
|
* @param exchange 交换机
|
||||||
|
* @param routingKey 路由键
|
||||||
|
* @param message 消息
|
||||||
|
*/
|
||||||
|
public void send(String exchange, String routingKey, Object message) {
|
||||||
|
try {
|
||||||
|
rabbitTemplate.convertAndSend(exchange, routingKey, message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("RabbitMQ发送消息失败:{}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,14 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.hanserwei.chat.domain.dataobject.User;
|
import com.hanserwei.chat.domain.dataobject.User;
|
||||||
import com.hanserwei.chat.service.UserService;
|
import com.hanserwei.chat.service.UserService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.tool.annotation.Tool;
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
import org.springframework.ai.tool.annotation.ToolParam;
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class AiDBTools {
|
public class AiDBTools {
|
||||||
|
|
||||||
@@ -18,21 +20,25 @@ public class AiDBTools {
|
|||||||
|
|
||||||
@Tool(name = "findAll", description = "查询所有用户")
|
@Tool(name = "findAll", description = "查询所有用户")
|
||||||
public List<User> findAll() {
|
public List<User> findAll() {
|
||||||
|
log.info("AiDBTools: findAll");
|
||||||
return userService.list();
|
return userService.list();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Tool(name = "findAllByIdIn", description = "根据id列表查询用户")
|
@Tool(name = "findAllByIdIn", description = "根据id列表查询用户")
|
||||||
public List<User> findAllByIdIn(@ToolParam(description = "用户id列表") List<Long> ids) {
|
public List<User> findAllByIdIn(@ToolParam(description = "用户id列表") List<Long> ids) {
|
||||||
|
log.info("AiDBTools: findAllByIdIn");
|
||||||
return userService.listByIds(ids);
|
return userService.listByIds(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Tool(name = "findById", description = "根据id查询用户")
|
@Tool(name = "findById", description = "根据id查询用户")
|
||||||
public User findById(Long id) {
|
public User findById(Long id) {
|
||||||
|
log.info("AiDBTools: findById");
|
||||||
return userService.getById(id);
|
return userService.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Tool(name = "findByName", description = "根据名称查询用户")
|
@Tool(name = "findByName", description = "根据名称查询用户")
|
||||||
public User findByName(String name) {
|
public User findByName(String name) {
|
||||||
|
log.info("AiDBTools: findByName");
|
||||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
||||||
.eq(User::getName, name);
|
.eq(User::getName, name);
|
||||||
return userService.getOne(queryWrapper);
|
return userService.getOne(queryWrapper);
|
||||||
@@ -40,6 +46,7 @@ public class AiDBTools {
|
|||||||
|
|
||||||
@Tool(name = "findByNameLike", description = "根据名称模糊查询用户")
|
@Tool(name = "findByNameLike", description = "根据名称模糊查询用户")
|
||||||
public List<User> findByNameLike(String name) {
|
public List<User> findByNameLike(String name) {
|
||||||
|
log.info("AiDBTools: findByNameLike");
|
||||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
||||||
.like(User::getName, name);
|
.like(User::getName, name);
|
||||||
return userService.list(queryWrapper);
|
return userService.list(queryWrapper);
|
||||||
@@ -47,6 +54,7 @@ public class AiDBTools {
|
|||||||
|
|
||||||
@Tool(name = "findByAge", description = "根据年龄查询用户")
|
@Tool(name = "findByAge", description = "根据年龄查询用户")
|
||||||
public List<User> findByAge(Integer age) {
|
public List<User> findByAge(Integer age) {
|
||||||
|
log.info("AiDBTools: findByAge");
|
||||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
||||||
.eq(User::getAge, age);
|
.eq(User::getAge, age);
|
||||||
return userService.list(queryWrapper);
|
return userService.list(queryWrapper);
|
||||||
@@ -54,6 +62,7 @@ public class AiDBTools {
|
|||||||
|
|
||||||
@Tool(name = "findByAgeBetween", description = "根据年龄范围查询用户")
|
@Tool(name = "findByAgeBetween", description = "根据年龄范围查询用户")
|
||||||
public List<User> findByAgeBetween(Integer start, Integer end) {
|
public List<User> findByAgeBetween(Integer start, Integer end) {
|
||||||
|
log.info("AiDBTools: findByAgeBetween");
|
||||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(User.class)
|
||||||
.between(User::getAge, start, end);
|
.between(User::getAge, start, end);
|
||||||
return userService.list(queryWrapper);
|
return userService.list(queryWrapper);
|
||||||
@@ -68,6 +77,7 @@ public class AiDBTools {
|
|||||||
name (String), email (String), 和 age (Integer)。
|
name (String), email (String), 和 age (Integer)。
|
||||||
""")
|
""")
|
||||||
public void insert(@ToolParam(description = "用户对象") User user) {
|
public void insert(@ToolParam(description = "用户对象") User user) {
|
||||||
|
log.info("AiDBTools: insert");
|
||||||
userService.save(user);
|
userService.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +87,13 @@ public class AiDBTools {
|
|||||||
并携带要修改的字段,例如 name (String), email (String), 或 age (Integer)。
|
并携带要修改的字段,例如 name (String), email (String), 或 age (Integer)。
|
||||||
""")
|
""")
|
||||||
public void update(@ToolParam(description = "用户对象") User user) {
|
public void update(@ToolParam(description = "用户对象") User user) {
|
||||||
|
log.info("AiDBTools: update");
|
||||||
userService.updateById(user);
|
userService.updateById(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Tool(name = "delete", description = "删除用户")
|
@Tool(name = "delete", description = "删除用户")
|
||||||
public void delete(Long id) {
|
public void delete(Long id) {
|
||||||
|
log.info("AiDBTools: delete");
|
||||||
userService.removeById(id);
|
userService.removeById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.hanserwei.chat.tools;
|
||||||
|
|
||||||
|
import com.hanserwei.chat.publisher.RabbitMQPublisher;
|
||||||
|
import com.hanserwei.chat.utils.ConversationContext;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.ai.chat.memory.ChatMemory;
|
||||||
|
import org.springframework.ai.chat.messages.AbstractMessage;
|
||||||
|
import org.springframework.ai.chat.messages.Message;
|
||||||
|
import org.springframework.ai.chat.messages.MessageType;
|
||||||
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.ai.tool.annotation.ToolParam;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SendMQMessageTools {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RabbitMQPublisher rabbitMQPublisher;
|
||||||
|
@Resource
|
||||||
|
private ChatMemory chatMemory;
|
||||||
|
|
||||||
|
private static final int HISTORY_LIMIT = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送RabbitMQ消息
|
||||||
|
*
|
||||||
|
* @param phoneNumber 手机号码
|
||||||
|
*/
|
||||||
|
@Tool(name = "SendMessage",
|
||||||
|
description = "当用户留下手机号码时,立即调用此工具发送消息通知管理员。手机号码应该是11位数字。")
|
||||||
|
public Mono<String> sendMQMessage(
|
||||||
|
@ToolParam(description = "用户提供的手机号码,必须是11位数字") String phoneNumber
|
||||||
|
) {
|
||||||
|
return ConversationContext.getConversationIdMono()
|
||||||
|
.defaultIfEmpty(ConversationContext.getConversationId())
|
||||||
|
.map(conversationId -> {
|
||||||
|
String exchange = "chat.exchange";
|
||||||
|
String routingKey = "chat.routing.key";
|
||||||
|
String recentConversation = buildRecentConversation(conversationId);
|
||||||
|
String message = "用户留了手机号:" + phoneNumber + ",会话ID:" + conversationId + recentConversation;
|
||||||
|
|
||||||
|
log.info("SendMQMessageTools 发送消息: {}", message);
|
||||||
|
rabbitMQPublisher.send(exchange, routingKey, message);
|
||||||
|
|
||||||
|
// 确保日志能被记录
|
||||||
|
log.info("SendMQMessageTools 消息发送完成");
|
||||||
|
return "消息发送成功";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildRecentConversation(Long conversationId) {
|
||||||
|
if (conversationId == null) {
|
||||||
|
return "。最近对话:暂无记录";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Message> history = null;
|
||||||
|
try {
|
||||||
|
history = chatMemory.get(String.valueOf(conversationId));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("获取会话{}历史记录失败: {}", conversationId, ex.getMessage(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history == null || history.isEmpty()) {
|
||||||
|
return "。最近对话:暂无记录";
|
||||||
|
}
|
||||||
|
|
||||||
|
int skip = Math.max(0, history.size() - HISTORY_LIMIT);
|
||||||
|
String recent = history.stream()
|
||||||
|
.skip(skip)
|
||||||
|
.map(SendMQMessageTools::formatMessage)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.joining("\n"));
|
||||||
|
|
||||||
|
if (recent.isEmpty()) {
|
||||||
|
return "。最近对话:暂无记录";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "。最近对话:\n" + recent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatMessage(Message message) {
|
||||||
|
if (message == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType messageType = message.getMessageType();
|
||||||
|
String role = messageType != null ? messageType.name() : "UNKNOWN";
|
||||||
|
String text = (message instanceof AbstractMessage abstractMessage)
|
||||||
|
? abstractMessage.getText()
|
||||||
|
: message.toString();
|
||||||
|
|
||||||
|
if (text == null || text.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return role + ": " + text.strip();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.hanserwei.chat.utils;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.context.Context;
|
||||||
|
import reactor.util.context.ContextView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话上下文工具类,用于在线程本地存储中保存会话ID
|
||||||
|
* 同时支持Reactor Context,以支持响应式编程中的上下文传递
|
||||||
|
*/
|
||||||
|
public class ConversationContext {
|
||||||
|
|
||||||
|
private static final ThreadLocal<Long> CONVERSATION_ID_HOLDER = new ThreadLocal<>();
|
||||||
|
|
||||||
|
// Reactor Context key
|
||||||
|
private static final String CONVERSATION_ID_KEY = "conversationId";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前线程的会话ID
|
||||||
|
* @param conversationId 会话ID
|
||||||
|
*/
|
||||||
|
public static void setConversationId(Long conversationId) {
|
||||||
|
CONVERSATION_ID_HOLDER.set(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前线程的会话ID
|
||||||
|
* @return 会话ID
|
||||||
|
*/
|
||||||
|
public static Long getConversationId() {
|
||||||
|
// 首先尝试从ThreadLocal获取
|
||||||
|
return CONVERSATION_ID_HOLDER.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从Reactor Context中获取会话ID
|
||||||
|
* @return 包含会话ID的Mono
|
||||||
|
*/
|
||||||
|
public static Mono<Long> getConversationIdMono() {
|
||||||
|
return Mono.deferContextual(ctx -> {
|
||||||
|
if (ctx.hasKey(CONVERSATION_ID_KEY)) {
|
||||||
|
return Mono.just(ctx.get(CONVERSATION_ID_KEY));
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在Reactor Context中设置会话ID
|
||||||
|
* @param conversationId 会话ID
|
||||||
|
* @return Context
|
||||||
|
*/
|
||||||
|
public static Context withConversationId(Long conversationId) {
|
||||||
|
return Context.of(CONVERSATION_ID_KEY, conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理当前线程的会话ID
|
||||||
|
*/
|
||||||
|
public static void clear() {
|
||||||
|
CONVERSATION_ID_HOLDER.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,15 @@ spring:
|
|||||||
max-wait: 10000
|
max-wait: 10000
|
||||||
min-idle: 10
|
min-idle: 10
|
||||||
time-between-eviction-runs: 10000
|
time-between-eviction-runs: 10000
|
||||||
|
rabbitmq:
|
||||||
|
host: localhost
|
||||||
|
port: 5672
|
||||||
|
username: admin
|
||||||
|
password: admin123
|
||||||
|
virtual-host: /snailsAi
|
||||||
|
listener:
|
||||||
|
simple:
|
||||||
|
prefetch: 1
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: org.postgresql.Driver
|
driver-class-name: org.postgresql.Driver
|
||||||
url: jdbc:postgresql://localhost:5432/postgres?serverTimezone=Asia/Shanghai
|
url: jdbc:postgresql://localhost:5432/postgres?serverTimezone=Asia/Shanghai
|
||||||
|
|||||||
23
snails-chat/src/main/resources/prompt/aiAssistant.st
Normal file
23
snails-chat/src/main/resources/prompt/aiAssistant.st
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
### 🎯 **角色与目标**
|
||||||
|
|
||||||
|
你是一个**专业、友好且信息准确**的智能客服AI助手,负责解答用户关于HanserUniverse公司的常见问题。你的核心目标是**高效解决疑问**,并在每次互动后**引导用户留下手机号码进行下一步的深度咨询**。
|
||||||
|
|
||||||
|
公司核心业务: 我们是一家专注于智能家居产品研发与销售的科技公司。
|
||||||
|
|
||||||
|
使命: 致力于通过新技术 / 优质服务 / 专业内容提升用户的生活质量/学习效率/工作效率。
|
||||||
|
### 📜 **基本指令与约束**
|
||||||
|
|
||||||
|
1. **回答模式:** 始终以**清晰、简洁、礼貌**的方式回答用户的问题。
|
||||||
|
2. **强制性附加句:** 无论你回答了什么问题,在**每个回答的末尾**,你必须**完全、准确地附加**以下这句话:
|
||||||
|
> **"如果想要了解更多信息可以留下电话号码,会有专门的客服人员与您联系。"**
|
||||||
|
3. **手机号识别与工具调用:**
|
||||||
|
* 当用户输入的文本中包含一个**有效的手机号码**时(通常是11位数字,识别时请保持一定的容错性),你必须**立即调用SendMessage工具**,并将手机号作为参数传递。
|
||||||
|
* **给用户的反馈:** 工具调用成功后,给用户回复一个简短的确认信息,例如:"感谢您留下手机号码,我们已收到您的信息,专业的客服人员将在24小时内与您联系,请保持手机畅通。"
|
||||||
|
|
||||||
|
### 💡 **工作流程示例**
|
||||||
|
|
||||||
|
| 步骤 | 用户输入 | AI助手的行为 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **1 (常规问题)** | "你们产品的价格是多少?" | 1. 回答价格信息。 <br> 2. **附加句:** "如果想要了解更多信息可以留下电话号码,会有专门的客服人员与您联系。" |
|
||||||
|
| **2 (留号码)** | "好的,这是我的手机号:13800001234" | 1. **识别**到手机号。 <br> 2. **调用工具:** `SendMessage` <br> 3. **回复确认:** "感谢您留下手机号码,我们已收到您的信息,专业的客服人员将在24小时内与您联系,请保持手机畅通。" |
|
||||||
|
| **3 (追问)** | "你们的退货政策呢?" | 1. 回答退货政策。 <br> 2. **附加句:** "如果想要了解更多信息可以留下电话号码,会有专门的客服人员与您联系。" |
|
||||||
Reference in New Issue
Block a user