| | |
| | | |
| | | import com.ruoyi.ai.assistant.ApproveTodoAgent; |
| | | import com.ruoyi.ai.assistant.ApproveTodoIntentExecutor; |
| | | import com.ruoyi.ai.assistant.FileAnalyzeAgent; |
| | | import com.ruoyi.ai.bean.ChatForm; |
| | | import com.ruoyi.ai.context.AiSessionUserContext; |
| | | import com.ruoyi.ai.service.AiChatSessionService; |
| | | import com.ruoyi.ai.service.AiFileTextExtractor; |
| | | import com.ruoyi.ai.store.MongoChatMemoryStore; |
| | | import com.ruoyi.common.utils.SecurityUtils; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.framework.security.LoginUser; |
| | | import com.ruoyi.framework.web.controller.BaseController; |
| | | import com.ruoyi.framework.web.domain.AjaxResult; |
| | | import dev.langchain4j.data.message.AiMessage; |
| | | import dev.langchain4j.data.message.UserMessage; |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import io.swagger.v3.oas.annotations.tags.Tag; |
| | | import org.springframework.web.bind.annotation.DeleteMapping; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | import reactor.core.publisher.Flux; |
| | | |
| | | import java.io.IOException; |
| | | import java.util.List; |
| | | import java.util.NoSuchElementException; |
| | | import java.util.UUID; |
| | | |
| | | @Tag(name = "协同办公助手") |
| | | @RestController |
| | | @RequestMapping("/xiaozhi") |
| | | public class XiaozhiController { |
| | | public class XiaozhiController extends BaseController { |
| | | |
| | | private static final String FILE_ANALYZE_MEMORY_PREFIX = "file-analyze::"; |
| | | |
| | | private final ApproveTodoAgent approveTodoAgent; |
| | | private final ApproveTodoIntentExecutor approveTodoIntentExecutor; |
| | | private final FileAnalyzeAgent fileAnalyzeAgent; |
| | | private final AiSessionUserContext aiSessionUserContext; |
| | | private final MongoChatMemoryStore mongoChatMemoryStore; |
| | | private final AiFileTextExtractor aiFileTextExtractor; |
| | | private final AiChatSessionService aiChatSessionService; |
| | | |
| | | public XiaozhiController(ApproveTodoAgent approveTodoAgent, ApproveTodoIntentExecutor approveTodoIntentExecutor) { |
| | | public XiaozhiController(ApproveTodoAgent approveTodoAgent, |
| | | ApproveTodoIntentExecutor approveTodoIntentExecutor, |
| | | FileAnalyzeAgent fileAnalyzeAgent, |
| | | AiSessionUserContext aiSessionUserContext, |
| | | MongoChatMemoryStore mongoChatMemoryStore, |
| | | AiFileTextExtractor aiFileTextExtractor, |
| | | AiChatSessionService aiChatSessionService) { |
| | | this.approveTodoAgent = approveTodoAgent; |
| | | this.approveTodoIntentExecutor = approveTodoIntentExecutor; |
| | | this.fileAnalyzeAgent = fileAnalyzeAgent; |
| | | this.aiSessionUserContext = aiSessionUserContext; |
| | | this.mongoChatMemoryStore = mongoChatMemoryStore; |
| | | this.aiFileTextExtractor = aiFileTextExtractor; |
| | | this.aiChatSessionService = aiChatSessionService; |
| | | } |
| | | |
| | | @Operation(summary = "对话") |
| | | @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8") |
| | | public Flux<String> chat(@RequestBody ChatForm chatForm) { |
| | | String directResponse = approveTodoIntentExecutor.tryExecute(chatForm.getMessage()); |
| | | if (directResponse != null) { |
| | | if (!StringUtils.hasText(chatForm.getMemoryId())) { |
| | | return Flux.just("memoryId不能为空"); |
| | | } |
| | | if (!StringUtils.hasText(chatForm.getMessage())) { |
| | | return Flux.just("message不能为空"); |
| | | } |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | String memoryId = chatForm.getMemoryId(); |
| | | String userMessage = chatForm.getMessage(); |
| | | |
| | | aiSessionUserContext.bind(memoryId, loginUser); |
| | | aiChatSessionService.touchSession(memoryId, loginUser, userMessage); |
| | | |
| | | String directResponse = approveTodoIntentExecutor.tryExecute(memoryId, userMessage); |
| | | if (StringUtils.isNotEmpty(directResponse)) { |
| | | mongoChatMemoryStore.appendMessages( |
| | | memoryId, |
| | | List.of(UserMessage.from(userMessage), AiMessage.from(directResponse)) |
| | | ); |
| | | aiChatSessionService.refreshSessionStats(memoryId, loginUser); |
| | | return Flux.just(directResponse); |
| | | } |
| | | return approveTodoAgent.chat(chatForm.getMemoryId(), chatForm.getMessage()); |
| | | |
| | | return approveTodoAgent.chat(memoryId, userMessage) |
| | | .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser)) |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser)); |
| | | } |
| | | |
| | | @Operation(summary = "上传文件分析") |
| | | @PostMapping(value = "/analyze-file", consumes = "multipart/form-data", produces = "text/stream;charset=utf-8") |
| | | public Flux<String> analyzeFile(@RequestParam("file") MultipartFile file, |
| | | @RequestParam(value = "message", required = false) String message, |
| | | @RequestParam(value = "memoryId", required = false) String memoryId) { |
| | | String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString(); |
| | | String finalMemoryId = rawMemoryId.startsWith(FILE_ANALYZE_MEMORY_PREFIX) |
| | | ? rawMemoryId |
| | | : FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId; |
| | | |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | aiSessionUserContext.bind(finalMemoryId, loginUser); |
| | | |
| | | String finalMessage = StringUtils.hasText(message) ? message : "请分析这个文件的核心内容并给出总结"; |
| | | String fileText; |
| | | try { |
| | | fileText = aiFileTextExtractor.extractText(file); |
| | | } catch (IllegalArgumentException ex) { |
| | | return Flux.just(ex.getMessage()); |
| | | } catch (IOException ex) { |
| | | return Flux.just("文件读取失败"); |
| | | } |
| | | |
| | | if (!StringUtils.hasText(fileText)) { |
| | | return Flux.just("未提取到有效文件内容"); |
| | | } |
| | | |
| | | String limitedContent = fileText.length() > 12000 ? fileText.substring(0, 12000) : fileText; |
| | | String userPrompt = "你将分析用户上传的文件内容。\n" |
| | | + "文件名: " + file.getOriginalFilename() + "\n" |
| | | + "用户问题: " + finalMessage + "\n" |
| | | + "文件内容如下:\n" |
| | | + limitedContent; |
| | | |
| | | aiChatSessionService.touchSession(finalMemoryId, loginUser, "文件分析: " + finalMessage); |
| | | |
| | | return Flux.defer(() -> fileAnalyzeAgent.chat(finalMemoryId, userPrompt)) |
| | | .onErrorResume(NoSuchElementException.class, ex -> { |
| | | // DashScope 在历史消息兼容场景下可能抛 NoSuchElementException,清空后重试一次 |
| | | mongoChatMemoryStore.deleteMessages(finalMemoryId); |
| | | return fileAnalyzeAgent.chat(finalMemoryId, userPrompt); |
| | | }) |
| | | .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)) |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)); |
| | | } |
| | | |
| | | @Operation(summary = "会话列表") |
| | | @GetMapping("/history/sessions") |
| | | public AjaxResult listSessions() { |
| | | return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser())); |
| | | } |
| | | |
| | | @Operation(summary = "会话消息") |
| | | @GetMapping("/history/messages/{memoryId}") |
| | | public AjaxResult listMessages(@PathVariable String memoryId) { |
| | | return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser())); |
| | | } |
| | | |
| | | @Operation(summary = "删除会话") |
| | | @DeleteMapping("/history/{memoryId}") |
| | | public AjaxResult deleteSession(@PathVariable String memoryId) { |
| | | aiSessionUserContext.remove(memoryId); |
| | | return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser())); |
| | | } |
| | | } |