diff --git a/src/main/java/com/wok/supportbot/app/AssistantApp.java b/src/main/java/com/wok/supportbot/app/AssistantApp.java index 9d7ee12..a899162 100644 --- a/src/main/java/com/wok/supportbot/app/AssistantApp.java +++ b/src/main/java/com/wok/supportbot/app/AssistantApp.java @@ -3,6 +3,10 @@ package com.wok.supportbot.app; import com.wok.supportbot.advisor.MyLoggerAdvisor; import com.wok.supportbot.advisor.ReReadingAdvisor; import com.wok.supportbot.chatmemory.DatabaseChatMemory; +import com.wok.supportbot.rag.preretrieval.CompressionQueryRewriter; +import com.wok.supportbot.rag.preretrieval.MultiQueryExpanderRewriter; +import com.wok.supportbot.rag.preretrieval.RewriteQueryRewriter; +import com.wok.supportbot.rag.preretrieval.TranslationQueryRewriter; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; @@ -22,6 +26,7 @@ import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; @@ -41,7 +46,7 @@ import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvis public class AssistantApp { @Resource - private VectorStore vectorStore; + private VectorStore pgVectorVectorStore; private final ChatClient chatClient; @@ -90,6 +95,33 @@ public class AssistantApp { return chatResponse.getResult().getOutput().getText(); } + /** + * AI 基础对话(支持多轮对话记忆,SSE 流式传输) + * + * @param message + * @param chatId + * @return + */ + public Flux doChatByStream(String message, String chatId) { + return chatClient + .prompt() + .user(message) + .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) + .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) + .stream() + .content(); + } + + // AI 恋爱知识库问答功能 + @Resource + RewriteQueryRewriter rewriteQueryRewriter; + @Resource + CompressionQueryRewriter compressionQueryRewriter; + @Resource + MultiQueryExpanderRewriter multiQueryExpanderRewriter; + @Resource + TranslationQueryRewriter translationQueryRewriter; + /** * 和 RAG 知识库进行对话 @@ -99,13 +131,17 @@ public class AssistantApp { * @return */ public String doChatWithRag(String message, String chatId) { + // 在预检索阶段,系统接收用户的原始查询,通过查询转换和查询扩展等方法对其进行优化,输出增强的用户查询。 + // String rewrittenMessage = translationQueryRewriter.doQueryRewrite(message); + String rewrittenMessage = rewriteQueryRewriter.doQueryRewrite(message); + ChatResponse chatResponse = chatClient .prompt() - .user(message) + .user(rewrittenMessage) .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) // 应用 RAG 知识库问答 - .advisors(QuestionAnswerAdvisor.builder(vectorStore) + .advisors(QuestionAnswerAdvisor.builder(pgVectorVectorStore) // 相似度阈值为 0.0,并返回最相关的前 4 个结果 .searchRequest(SearchRequest.builder().similarityThreshold(0.0).topK(4).build()) .build()) @@ -121,7 +157,7 @@ public class AssistantApp { private MultiQueryExpander multiQueryExpander; /** - * 和 RAG 知识库进行对话 + * 和 RAG 知识库进行对话(另外一种使用方式) * * @param message * @param chatId @@ -129,10 +165,11 @@ public class AssistantApp { */ public String doChatWithRagEnhance(String message, String chatId) { Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder() + // todo 不生效 //.queryTransformers(queryTransformers) //.queryExpander(multiQueryExpander) .documentRetriever(VectorStoreDocumentRetriever.builder() - .vectorStore(vectorStore) + .vectorStore(pgVectorVectorStore) .similarityThreshold(0.5) .topK(4) .build()) diff --git a/src/main/java/com/wok/supportbot/app/ProductInfoApp.java b/src/main/java/com/wok/supportbot/app/ProductInfoApp.java index ef8996e..c04526f 100644 --- a/src/main/java/com/wok/supportbot/app/ProductInfoApp.java +++ b/src/main/java/com/wok/supportbot/app/ProductInfoApp.java @@ -45,16 +45,13 @@ public class ProductInfoApp { /** * 商品信息结构化抽取 * @param rawContent 爬取的商品网页内容 - * @param chatId 对话ID * @return 结构化的商品信息对象 */ - public ProductInfo extractProductInfo(String rawContent, String chatId) { + public ProductInfo extractProductInfo(String rawContent) { ProductInfo productInfo = chatClient .prompt() .system(SYSTEM_PROMPT) .user(rawContent) - .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) - .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) .call() .entity(ProductInfo.class); log.info("Extracted product info: {}", productInfo); diff --git a/src/main/java/com/wok/supportbot/config/CorsConfig.java b/src/main/java/com/wok/supportbot/config/CorsConfig.java new file mode 100644 index 0000000..01b3aaa --- /dev/null +++ b/src/main/java/com/wok/supportbot/config/CorsConfig.java @@ -0,0 +1,25 @@ +package com.wok.supportbot.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 全局跨域配置 + */ +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + // 覆盖所有请求 + registry.addMapping("/**") + // 允许发送 Cookie + .allowCredentials(true) + // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突) + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .exposedHeaders("*"); + } +} diff --git a/src/main/java/com/wok/supportbot/controller/AiController.java b/src/main/java/com/wok/supportbot/controller/AiController.java new file mode 100644 index 0000000..cb44a4f --- /dev/null +++ b/src/main/java/com/wok/supportbot/controller/AiController.java @@ -0,0 +1,89 @@ +package com.wok.supportbot.controller; + +import com.wok.supportbot.app.AssistantApp; +import jakarta.annotation.Resource; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerSentEvent; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import reactor.core.publisher.Flux; + +import java.io.IOException; + + + +public class AiController { + + @Resource + private AssistantApp assistantApp; + + /** + * 同步调用 AI 智能客服应用 + * + * @param message + * @param chatId + * @return + */ + @GetMapping("/assistant_app/chat/sync") + public String doChatWithAssistantAppSync(String message, String chatId) { + return assistantApp.doChat(message, chatId); + } + + /** + * SSE 流式调用 AI 智能客服应用 + * 返回Flux 响应式؜对象,并且添加 SSE 对应的 MediaType + * + * @param message + * @param chatId + * @return + */ + @GetMapping(value = "/assistant_app/chat/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux doChatWithLoveAppSSE(String message, String chatId) { + return assistantApp.doChatByStream(message, chatId); + } + + /** + * SSE 流式调用 AI 智能客服应用 + * 返回 Flux 对象,并且؜设置泛型为 ServerSentEvent。使用这种方式可以省略 MediaType + * + * @param message + * @param chatId + * @return + */ + @GetMapping(value = "/assistant_app/chat/server_sent_event") + public Flux> doChatWithAssistantAppServerSentEvent(String message, String chatId) { + return assistantApp.doChatByStream(message, chatId) + .map(chunk -> ServerSentEvent.builder() + .data(chunk) + .build()); + } + + /** + * SSE 流式调用 AI 智能客服应用 + * 使用 SSEEmiter,؜通过 send 方法持续向 SseEmitter 发送消息 + * + * @param message + * @param chatId + * @return + */ + @GetMapping(value = "/assistant_app/chat/sse_emitter") + public SseEmitter doChatWithAssistantAppServerSseEmitter(String message, String chatId) { + // 创建一个超时时间较长的 SseEmitter + SseEmitter sseEmitter = new SseEmitter(180000L); // 3 分钟超时 + // 获取 Flux 响应式数据流并且直接通过订阅推送给 SseEmitter + assistantApp.doChatByStream(message, chatId) + .subscribe(chunk -> { + try { + sseEmitter.send(chunk); + } catch (IOException e) { + sseEmitter.completeWithError(e); + } + }, sseEmitter::completeWithError, sseEmitter::complete); + // 返回 + return sseEmitter; + } +} diff --git a/src/main/java/com/wok/supportbot/controller/HealthController.java b/src/main/java/com/wok/supportbot/controller/HealthController.java deleted file mode 100644 index 334e839..0000000 --- a/src/main/java/com/wok/supportbot/controller/HealthController.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.wok.supportbot.controller; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/health") -public class HealthController { - - @GetMapping - public String healthCheck() { - return "ok"; - } -} diff --git a/src/main/java/com/wok/supportbot/config/QueryExpanderConfig.java b/src/main/java/com/wok/supportbot/rag/config/QueryExpanderConfig.java similarity index 94% rename from src/main/java/com/wok/supportbot/config/QueryExpanderConfig.java rename to src/main/java/com/wok/supportbot/rag/config/QueryExpanderConfig.java index de35e86..d78ef2b 100644 --- a/src/main/java/com/wok/supportbot/config/QueryExpanderConfig.java +++ b/src/main/java/com/wok/supportbot/rag/config/QueryExpanderConfig.java @@ -1,4 +1,4 @@ -package com.wok.supportbot.config; +package com.wok.supportbot.rag.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; diff --git a/src/main/java/com/wok/supportbot/config/QueryTransformerConfig.java b/src/main/java/com/wok/supportbot/rag/config/QueryTransformerConfig.java similarity index 83% rename from src/main/java/com/wok/supportbot/config/QueryTransformerConfig.java rename to src/main/java/com/wok/supportbot/rag/config/QueryTransformerConfig.java index d02526f..79645b2 100644 --- a/src/main/java/com/wok/supportbot/config/QueryTransformerConfig.java +++ b/src/main/java/com/wok/supportbot/rag/config/QueryTransformerConfig.java @@ -1,19 +1,14 @@ -package com.wok.supportbot.config; +package com.wok.supportbot.rag.config; import org.springframework.ai.chat.client.ChatClient; -import org.springframework.ai.chat.client.advisor.RetrievalAugmentationAdvisor; import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.rag.preretrieval.query.expansion.MultiQueryExpander; import org.springframework.ai.rag.preretrieval.query.transformation.CompressionQueryTransformer; import org.springframework.ai.rag.preretrieval.query.transformation.QueryTransformer; import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer; import org.springframework.ai.rag.preretrieval.query.transformation.TranslationQueryTransformer; -import org.springframework.ai.rag.retrieval.search.DocumentRetriever; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.util.List; - @Configuration public class QueryTransformerConfig { diff --git a/src/main/java/com/wok/supportbot/load/InMemoryVectorStoreConfig.java b/src/main/java/com/wok/supportbot/rag/load/InMemoryVectorStoreConfig.java similarity index 76% rename from src/main/java/com/wok/supportbot/load/InMemoryVectorStoreConfig.java rename to src/main/java/com/wok/supportbot/rag/load/InMemoryVectorStoreConfig.java index a40aac0..3693b13 100644 --- a/src/main/java/com/wok/supportbot/load/InMemoryVectorStoreConfig.java +++ b/src/main/java/com/wok/supportbot/rag/load/InMemoryVectorStoreConfig.java @@ -1,16 +1,11 @@ -package com.wok.supportbot.load; +package com.wok.supportbot.rag.load; -import com.wok.supportbot.extract.MarkdownDocumentLoader; -import jakarta.annotation.Resource; -import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.util.List; - /** * 向量数据库配置(初始化基于内存的向量数据库 Bean) */ diff --git a/src/main/java/com/wok/supportbot/load/PgVectorStoreConfig.java b/src/main/java/com/wok/supportbot/rag/load/PgVectorStoreConfig.java similarity index 89% rename from src/main/java/com/wok/supportbot/load/PgVectorStoreConfig.java rename to src/main/java/com/wok/supportbot/rag/load/PgVectorStoreConfig.java index d44466f..4c6f522 100644 --- a/src/main/java/com/wok/supportbot/load/PgVectorStoreConfig.java +++ b/src/main/java/com/wok/supportbot/rag/load/PgVectorStoreConfig.java @@ -1,8 +1,5 @@ -package com.wok.supportbot.load; +package com.wok.supportbot.rag.load; -import com.wok.supportbot.extract.MarkdownDocumentLoader; -import jakarta.annotation.Resource; -import org.springframework.ai.document.Document; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.pgvector.PgVectorStore; @@ -11,8 +8,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; -import java.util.List; - import static org.springframework.ai.vectorstore.pgvector.PgVectorStore.PgDistanceType.COSINE_DISTANCE; import static org.springframework.ai.vectorstore.pgvector.PgVectorStore.PgIndexType.HNSW; /** diff --git a/src/main/java/com/wok/supportbot/preretrieval/CompressionQueryRewriter.java b/src/main/java/com/wok/supportbot/rag/preretrieval/CompressionQueryRewriter.java similarity index 89% rename from src/main/java/com/wok/supportbot/preretrieval/CompressionQueryRewriter.java rename to src/main/java/com/wok/supportbot/rag/preretrieval/CompressionQueryRewriter.java index 160a01a..74b3dae 100644 --- a/src/main/java/com/wok/supportbot/preretrieval/CompressionQueryRewriter.java +++ b/src/main/java/com/wok/supportbot/rag/preretrieval/CompressionQueryRewriter.java @@ -1,10 +1,8 @@ -package com.wok.supportbot.preretrieval; +package com.wok.supportbot.rag.preretrieval; 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.ai.chat.model.ChatModel; import org.springframework.ai.rag.Query; import org.springframework.ai.rag.preretrieval.query.transformation.CompressionQueryTransformer; diff --git a/src/main/java/com/wok/supportbot/preretrieval/MultiQueryExpanderRewriter.java b/src/main/java/com/wok/supportbot/rag/preretrieval/MultiQueryExpanderRewriter.java similarity index 96% rename from src/main/java/com/wok/supportbot/preretrieval/MultiQueryExpanderRewriter.java rename to src/main/java/com/wok/supportbot/rag/preretrieval/MultiQueryExpanderRewriter.java index fcc86ba..57036d9 100644 --- a/src/main/java/com/wok/supportbot/preretrieval/MultiQueryExpanderRewriter.java +++ b/src/main/java/com/wok/supportbot/rag/preretrieval/MultiQueryExpanderRewriter.java @@ -1,4 +1,4 @@ -package com.wok.supportbot.preretrieval; +package com.wok.supportbot.rag.preretrieval; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; diff --git a/src/main/java/com/wok/supportbot/preretrieval/RewriteQueryRewriter.java b/src/main/java/com/wok/supportbot/rag/preretrieval/RewriteQueryRewriter.java similarity index 96% rename from src/main/java/com/wok/supportbot/preretrieval/RewriteQueryRewriter.java rename to src/main/java/com/wok/supportbot/rag/preretrieval/RewriteQueryRewriter.java index 66f4de1..28fcd8e 100644 --- a/src/main/java/com/wok/supportbot/preretrieval/RewriteQueryRewriter.java +++ b/src/main/java/com/wok/supportbot/rag/preretrieval/RewriteQueryRewriter.java @@ -1,4 +1,4 @@ -package com.wok.supportbot.preretrieval; +package com.wok.supportbot.rag.preretrieval; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; diff --git a/src/main/java/com/wok/supportbot/preretrieval/TranslationQueryRewriter.java b/src/main/java/com/wok/supportbot/rag/preretrieval/TranslationQueryRewriter.java similarity index 96% rename from src/main/java/com/wok/supportbot/preretrieval/TranslationQueryRewriter.java rename to src/main/java/com/wok/supportbot/rag/preretrieval/TranslationQueryRewriter.java index 84df263..548386d 100644 --- a/src/main/java/com/wok/supportbot/preretrieval/TranslationQueryRewriter.java +++ b/src/main/java/com/wok/supportbot/rag/preretrieval/TranslationQueryRewriter.java @@ -1,4 +1,4 @@ -package com.wok.supportbot.preretrieval; +package com.wok.supportbot.rag.preretrieval; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; diff --git a/src/test/java/com/wok/supportbot/PgVectorVectorStoreConfigTest.java b/src/test/java/com/wok/supportbot/PgVectorVectorStoreConfigTest.java index 5038fdc..4cbbb2a 100644 --- a/src/test/java/com/wok/supportbot/PgVectorVectorStoreConfigTest.java +++ b/src/test/java/com/wok/supportbot/PgVectorVectorStoreConfigTest.java @@ -1,6 +1,5 @@ package com.wok.supportbot; -import com.wok.supportbot.preretrieval.RewriteQueryRewriter; import jakarta.annotation.Resource; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/wok/supportbot/QueryTransformerTests.java b/src/test/java/com/wok/supportbot/QueryTransformerTests.java index e40eb6f..e476f15 100644 --- a/src/test/java/com/wok/supportbot/QueryTransformerTests.java +++ b/src/test/java/com/wok/supportbot/QueryTransformerTests.java @@ -1,15 +1,13 @@ package com.wok.supportbot; -import com.wok.supportbot.preretrieval.CompressionQueryRewriter; -import com.wok.supportbot.preretrieval.MultiQueryExpanderRewriter; -import com.wok.supportbot.preretrieval.RewriteQueryRewriter; -import com.wok.supportbot.preretrieval.TranslationQueryRewriter; -import jakarta.annotation.Resource; +import com.wok.supportbot.rag.preretrieval.CompressionQueryRewriter; +import com.wok.supportbot.rag.preretrieval.MultiQueryExpanderRewriter; +import com.wok.supportbot.rag.preretrieval.RewriteQueryRewriter; +import com.wok.supportbot.rag.preretrieval.TranslationQueryRewriter; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.rag.Query; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; diff --git a/src/test/java/com/wok/supportbot/SupportBotApplicationTests.java b/src/test/java/com/wok/supportbot/SupportBotApplicationTests.java index 87bae76..5e41877 100644 --- a/src/test/java/com/wok/supportbot/SupportBotApplicationTests.java +++ b/src/test/java/com/wok/supportbot/SupportBotApplicationTests.java @@ -44,12 +44,8 @@ class SupportBotApplicationTests { String rawContent = "这是商品标题:智能手表Pro 2025," + "描述:这款智能手表支持心率监测和GPS," + "价格:299美元,评分:4.7星,评论数:1567,品牌:TechBrand,分类:电子产品。"; - - // 生成随机聊天ID,模拟独立会话 - String chatId = UUID.randomUUID().toString(); - // 调用方法 - ProductInfo productInfo = productInfoApp.extractProductInfo(rawContent, chatId); + ProductInfo productInfo = productInfoApp.extractProductInfo(rawContent); // 断言结果不为空 Assertions.assertNotNull(productInfo);