refactor(chat):重构AI助手功能并集成文档读取能力

- 移除原有的手机号识别与消息发送逻辑
- 删除RabbitMQ和邮件相关配置及代码
- 引入PDF、HTML、JSON等多种文档读取器
- 集成向量存储与检索功能支持问答
- 更新Spring AI依赖并调整内存存储方式
- 添加新的工具类用于保存文档到向量库- 修改提示词模板去除强制附加句规则
- 调整Cassandra和PgVector相关配置项- 新增多种文件格式读取组件实现类
This commit is contained in:
2025-10-31 20:48:28 +08:00
parent 29be26207f
commit 5ee2a0f11c
18 changed files with 363 additions and 355 deletions

49
pom.xml
View File

@@ -58,14 +58,6 @@
<artifactId>jasypt-spring-boot-starter</artifactId> <artifactId>jasypt-spring-boot-starter</artifactId>
<version>${jasypt-starter-version}</version> <version>${jasypt-starter-version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
@@ -83,18 +75,47 @@
<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> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-boot-starter-mail</artifactId> <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency> </dependency>
<!-- 向量 Advisor -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<!-- Cassandra -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-cassandra</artifactId>
</dependency>
<!-- 读取 Markdown -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-markdown-document-reader</artifactId>
</dependency>
<!-- 读取 HTML -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-jsoup-document-reader</artifactId>
</dependency>
<!-- 读取 PDF -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<!-- Tika -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@@ -1,15 +1,20 @@
package com.hanserwei.chat.config; package com.hanserwei.chat.config;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.memory.redis.BaseRedisChatMemoryRepository; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
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 com.hanserwei.chat.tools.SaveDocumentsTools;
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.client.advisor.vectorstore.QuestionAnswerAdvisor;
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.ai.chat.memory.repository.cassandra.CassandraChatMemoryRepository;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.aot.hint.annotation.RegisterReflection;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -17,49 +22,46 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
public class ChatClientConfiguration { public class ChatClientConfiguration {
@Value("${spring.ai.memory.redis.host}")
private String redisHost;
@Value("${spring.ai.memory.redis.port}")
private int redisPort;
@Value("${spring.ai.memory.redis.password}")
private String redisPassword;
@Value("${spring.ai.memory.redis.timeout}")
private int redisTimeout;
@Resource @Resource
private DashScopeChatModel dashScopeChatModel; private DashScopeChatModel dashScopeChatModel;
@Resource @Resource
private AiDBTools aiDBTools; private AiDBTools aiDBTools;
@Resource
private DashScopeEmbeddingModel dashScopeEmbeddingModel;
@Resource
private CassandraChatMemoryRepository chatMemoryRepository;
@Resource
private VectorStore vectorStore;
@Value("classpath:prompt/aiAssistant.st") @Value("classpath:prompt/aiAssistant.st")
private org.springframework.core.io.Resource aiAssistantResource; private org.springframework.core.io.Resource aiAssistantResource;
@Bean @Bean
public BaseRedisChatMemoryRepository redisChatMemoryRepository() { public ChatMemory chatMemory() {
// 构建RedissonRedisChatMemoryRepository实例 return MessageWindowChatMemory.builder()
return LettuceRedisChatMemoryRepository.builder() .maxMessages(50)
.host(redisHost) .chatMemoryRepository(chatMemoryRepository)
.port(redisPort)
.password(redisPassword)
.timeout(redisTimeout)
.build(); .build();
} }
@Bean @Bean
public ChatMemory chatMemory(BaseRedisChatMemoryRepository chatMemoryRepository) { public SimpleVectorStore simpleVectorStore() {
return MessageWindowChatMemory return SimpleVectorStore.builder(dashScopeEmbeddingModel)
.builder() .build();
.maxMessages(100000)
.chatMemoryRepository(chatMemoryRepository).build();
} }
@Bean @Bean
public ChatClient dashScopeChatClient(ChatMemory chatMemory, SendMQMessageTools sendMQMessageTools) { public ChatClient dashScopeChatClient(ChatMemory chatMemory,
SaveDocumentsTools saveDocumentsTools
) {
PromptChatMemoryAdvisor chatMemoryAdvisor = PromptChatMemoryAdvisor.builder(chatMemory).build();
SimpleLoggerAdvisor simpleLoggerAdvisor = new SimpleLoggerAdvisor();
QuestionAnswerAdvisor questionAnswerAdvisor = new QuestionAnswerAdvisor(vectorStore);
return ChatClient.builder(dashScopeChatModel) return ChatClient.builder(dashScopeChatModel)
.defaultTools(aiDBTools, sendMQMessageTools) .defaultTools(aiDBTools, saveDocumentsTools)
.defaultSystem(aiAssistantResource) .defaultSystem(aiAssistantResource)
.defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build()) .defaultAdvisors(chatMemoryAdvisor,simpleLoggerAdvisor,questionAnswerAdvisor)
.build(); .build();
} }
} }

View File

@@ -1,37 +0,0 @@
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();
}
}

View File

@@ -1,30 +0,0 @@
package com.hanserwei.chat.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置 RedisTemplate 的连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

View File

@@ -1,47 +0,0 @@
package com.hanserwei.chat.consumer;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class RabbitMQConsumer {
@Resource
private JavaMailSender mailSender;
@RabbitListener(queues = "chat.queue")
public void receiveMessage(Object message) {
String messageText = extractMessage(message);
log.info("Received message text:\n{}", messageText);
SimpleMailMessage mailMessage = new SimpleMailMessage();
// 配置发送者邮箱
mailMessage.setFrom("2628273921@qq.com");
// 配置接受者邮箱
mailMessage.setTo("ssw010723@gmail.com");
// 配置邮件主题
mailMessage.setSubject("主题:及时联系客户");
// 配置邮件内容
mailMessage.setText(messageText);
// 发送邮件
mailSender.send(mailMessage);
}
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);
};
}
}

View File

@@ -2,21 +2,19 @@ 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.reader.MyPdfReader;
import com.hanserwei.chat.utils.ConversationContext; import com.hanserwei.chat.utils.ConversationContext;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; 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.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
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.List;
import java.util.regex.Pattern;
@Slf4j @Slf4j
@RestController @RestController
@@ -26,15 +24,14 @@ public class AiChatController {
@Resource @Resource
private ChatClient dashScopeChatClient; private ChatClient dashScopeChatClient;
@Resource @Resource
private SendMQMessageTools sendMQMessageTools; private MyPdfReader myPdfReader;
@Resource
private static final Pattern PHONE_PATTERN = Pattern.compile("(?<!\\d)(1[3-9]\\d{9})(?!\\d)"); private VectorStore vectorStore;
@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()); log.info("会话ID{}", chatMessageDTO.getConversionId());
ConversationContext.setConversationId(chatMessageDTO.getConversionId()); ConversationContext.setConversationId(chatMessageDTO.getConversionId());
triggerSendMessageIfPhonePresent(chatMessageDTO);
return dashScopeChatClient.prompt() return dashScopeChatClient.prompt()
.user(chatMessageDTO.getMessage()) .user(chatMessageDTO.getMessage())
@@ -48,21 +45,10 @@ public class AiChatController {
.doFinally(signalType -> ConversationContext.clear()); .doFinally(signalType -> ConversationContext.clear());
} }
private void triggerSendMessageIfPhonePresent(ChatMessageDTO chatMessageDTO) { @GetMapping("/readpdf")
String message = chatMessageDTO.getMessage(); public String readPdf() {
if (message == null || message.isEmpty()) { List<Document> docsFromPdf = myPdfReader.getDocsFromPdf();
return; vectorStore.add(docsFromPdf);
} return "ok!";
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));
} }
} }

View File

@@ -1,29 +0,0 @@
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());
}
}
}

View File

@@ -0,0 +1,34 @@
package com.hanserwei.chat.reader;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.jsoup.JsoupDocumentReader;
import org.springframework.ai.reader.jsoup.config.JsoupDocumentReaderConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class MyHtmlReader {
@Value("classpath:/document/my-page.html")
private Resource resource;
public List<Document> loadHtml() {
// JsoupDocumentReader 阅读器配置类
JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder()
.selector("article p") // 提取 <article> 标签内的 p 段落
.charset("UTF-8") // 使用 UTF-8 编码
.includeLinkUrls(true) // 在元数据中包含链接 URL绝对链接
.metadataTags(List.of("author", "date")) // 提取 author 和 date 元标签
.additionalMetadata("source", "my-page.html") // 添加自定义元数据
.build();
// 新建 JsoupDocumentReader 阅读器
JsoupDocumentReader reader = new JsoupDocumentReader(resource, config);
// 读取并转换为 Document 文档集合
return reader.get();
}
}

View File

@@ -0,0 +1,27 @@
package com.hanserwei.chat.reader;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.JsonReader;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class MyJsonReader {
@Value("classpath:/document/tv.json")
private Resource resource;
/**
* 读取 Json 文件
* @return
*/
public List<Document> loadJson() {
// 创建 JsonReader 阅读器实例,配置需要读取的字段
JsonReader jsonReader = new JsonReader(resource, "description", "content", "title");
// 执行读取操作,并转换为 Document 对象集合
return jsonReader.get();
}
}

View File

@@ -0,0 +1,33 @@
package com.hanserwei.chat.reader;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.markdown.MarkdownDocumentReader;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class MyMarkdownReader {
@Value("classpath:/document/code.md")
private Resource resource;
public List<Document> loadMarkdown() {
// MarkdownDocumentReader 阅读器配置类
MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
.withHorizontalRuleCreateDocument(true) // 遇到水平线 ---,则创建新文档
.withIncludeCodeBlock(false) // 排除代码块(代码块生成单独文档)
.withIncludeBlockquote(false) // 排除块引用(块引用生成单独文档)
.withAdditionalMetadata("filename", "code.md") // 添加自定义元数据,如文件名称
.build();
// 新建 MarkdownDocumentReader 阅读器
MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
// 读取并转换为 Document 文档集合
return reader.get();
}
}

View File

@@ -0,0 +1,28 @@
package com.hanserwei.chat.reader;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class MyPdfReader {
public List<Document> getDocsFromPdf() {
// 新建 PagePdfDocumentReader 阅读器
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/document/profile.pdf", // PDF 文件路径
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0) // 设置页面顶边距为0
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0) // 设置删除顶部文本行数为0
.build())
.withPagesPerDocument(1) // 设置每个文档包含1页
.build());
// 读取并转换为 Document 文档集合
return pdfReader.read();
}
}

View File

@@ -0,0 +1,48 @@
package com.hanserwei.chat.reader;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class MyTextReader {
@Value("classpath:/document/manual.txt")
private Resource resource;
/**
* 读取 Txt 文档
* @return 读取的文档集合
*/
public List<Document> loadText() {
// 创建 TextReader 对象,用于读取指定资源 (resource) 的文本内容
TextReader textReader = new TextReader(resource);
// 添加自定义元数据,如文件名称
textReader.getCustomMetadata()
.put("filename", "manual.txt");
// 读取并转换为 Document 文档集合
return textReader.read();
}
/**
* 读取 Txt 文档并分块拆分
* @return 文档分块集合
*/
public List<Document> loadTextAndSplit() {
// 创建 TextReader 对象,用于读取指定资源 (resource) 的文本内容
TextReader textReader = new TextReader(resource);
// 将资源内容解析为 Document 对象集合
List<Document> documents = textReader.get();
// 使用 TokenTextSplitter 对文档列表进行分块处理
// 返回拆分后的文档分块集合
return new TokenTextSplitter().apply(documents);
}
}

View File

@@ -0,0 +1,29 @@
package com.hanserwei.chat.reader;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class MyTikaPptReader {
@Value("classpath:/document/XX牌云感变频空调说明书.pptx")
private Resource resource;
public List<Document> loadPpt() {
// 新建 TikaDocumentReader 阅读器
TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(resource);
// 读取并转换为 Document 文档集合
List<Document> documents = tikaDocumentReader.get();
// 文档分块
// 使用自定义设置
TokenTextSplitter splitter = new TokenTextSplitter(1000, 400, 10, 5000, true);
return splitter.apply(documents);
}
}

View File

@@ -0,0 +1,28 @@
package com.hanserwei.chat.reader;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class MyTikaWordReader {
@Value("classpath:/document/55f79946a0964b89bc7ab9b55e4a49ff.docx")
private Resource resource;
public List<Document> loadWord() {
// 新建 TikaDocumentReader 阅读器
TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(resource);
// 读取并转换为 Document 文档集合
List<Document> documents = tikaDocumentReader.get();
// 文档分块
TokenTextSplitter splitter = new TokenTextSplitter(); // 不设置任何构造参数,表示使用默认设置
return splitter.apply(documents);
}
}

View File

@@ -0,0 +1,33 @@
package com.hanserwei.chat.tools;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.List;
@Slf4j
@Component
public class SaveDocumentsTools {
@Resource
private SimpleVectorStore simpleVectorStore;
@Tool(name = "SaveDocuments",
description = "保存文档为本地的向量化知识库。")
public boolean saveDocuments(
@ToolParam(description = "文档内容") String documents
) {
simpleVectorStore.add(List.of(new Document(documents)));
//把内存中的向量数据,持久化到磁盘
File file = new File("/home/hanserwei/IdeaProjects/snails-ai-backend/snails-chat/src/main/resources/documents/vector.json");
simpleVectorStore.save(file);
return true;
}
}

View File

@@ -1,105 +0,0 @@
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();
}
}

View File

@@ -9,18 +9,13 @@ spring:
name: snails-ai name: snails-ai
banner: banner:
location: config/banner.txt location: config/banner.txt
cassandra:
contact-points: 127.0.0.1 # Cassandra 集群节点地址(可配置多个,用逗号分隔)
port: 9042 # 端口号
local-datacenter: datacenter1 # 必须与集群配置的数据中心名称一致(大小写敏感)
jackson: jackson:
serialization: serialization:
write-dates-as-timestamps: false write-dates-as-timestamps: false
mail:
host: ${MAIL_HOST:smtp.qq.com}
port: ${MAIL_PORT:587}
username: ${MAIL_USERNAME:2628273921@qq.com}
password: ENC(ARrAyZNZhbaG6tebogv6WSQbtCO+Vq93NfSA6tMAiD0tTogujERVwEGBECakH0LUhYq9oTaXgfw7tonxNAFEwg==)
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
mail.smtp.starttls.required: true
data: data:
redis: redis:
host: localhost host: localhost
@@ -35,15 +30,6 @@ 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
@@ -56,18 +42,31 @@ spring:
connection-timeout: 5000 # 获取连接超时 5 秒 connection-timeout: 5000 # 获取连接超时 5 秒
max-lifetime: 28800000 # 8 小时(确保在数据库连接超时前被回收) max-lifetime: 28800000 # 8 小时(确保在数据库连接超时前被回收)
ai: ai:
memory: vectorstore:
redis: pgvector:
host: localhost initialize-schema: true
port: 6379 table-name: snails_ai_vector
timeout: 5000 index-type: hnsw
password: redis dimensions: 1024
chat:
memory:
repository:
cassandra:
keyspace: snails_ai
table: t_ai_chat_memory # 表名
time-to-live: 1095d # 数据的自动过期时间1095天 ≈ 3年
initialize-schema: true # 自动初始化表结构
dashscope: dashscope:
api-key: ENC(cMgcKZkFllyE88DIbGwLKot9Vg02co+gsmY8L8o4/o3UjhcmqO4lJzFU35Sx0n+qFG8pDL0wBjoWrT8X6BuRw9vNlQhY1LgRWHaF9S1zzyM=) api-key: ENC(cMgcKZkFllyE88DIbGwLKot9Vg02co+gsmY8L8o4/o3UjhcmqO4lJzFU35Sx0n+qFG8pDL0wBjoWrT8X6BuRw9vNlQhY1LgRWHaF9S1zzyM=)
chat: chat:
options: options:
model: qwen-plus model: qwen-plus
temperature: 0.5 temperature: 0.5
embedding:
options:
model: text-embedding-v4
dimensions: 1024
read-timeout: 60000
mybatis-plus: mybatis-plus:
configuration: configuration:
map-underscore-to-camel-case: true map-underscore-to-camel-case: true

View File

@@ -5,19 +5,7 @@
使 / / // 使 / / //
### 📜 **** ### 📜 ****
1. **** **** 1. **** ****
2. **** ********
> **"如果想要了解更多信息可以留下电话号码,会有专门的客服人员与您联系。"**
3. ****
* ****11位数字**SendMessage**
* **** "感谢您留下手机号码我们已收到您的信息专业的客服人员将在24小时内与您联系请保持手机畅通。"
### 💡 ****
| 步骤 | | AI助手的行为 |
| :--- | :--- | :--- |
| **1 ()** | "你们产品的价格是多少?" | 1. <br> 2. **** "如果想要了解更多信息可以留下电话号码,会有专门的客服人员与您联系。" |
| **2 ()** | "好的这是我的手机号13800001234" | 1. **** <br> 2. **** `SendMessage` <br> 3. **** "感谢您留下手机号码我们已收到您的信息专业的客服人员将在24小时内与您联系请保持手机畅通。" |
| **3 ()** | "你们的退货政策呢?" | 1. 退 <br> 2. **** "如果想要了解更多信息可以留下电话号码,会有专门的客服人员与您联系。" |