package com.ruoyi.ai.controller; import com.ruoyi.ai.assistant.FinancialAgent; import com.ruoyi.ai.assistant.FinancialIntentExecutor; import com.ruoyi.ai.bean.ChatForm; import com.ruoyi.ai.context.AiSessionUserContext; import com.ruoyi.ai.service.AiChatSessionService; 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.RestController; import reactor.core.publisher.Flux; import java.time.LocalDate; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.List; @Tag(name = "财务智能体") @RestController @RequestMapping("/financial-ai") public class FinancialAiController extends BaseController { private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai"); private final FinancialAgent financialAgent; private final FinancialIntentExecutor financialIntentExecutor; private final AiSessionUserContext aiSessionUserContext; private final MongoChatMemoryStore mongoChatMemoryStore; private final AiChatSessionService aiChatSessionService; public FinancialAiController(FinancialAgent financialAgent, FinancialIntentExecutor financialIntentExecutor, AiSessionUserContext aiSessionUserContext, MongoChatMemoryStore mongoChatMemoryStore, AiChatSessionService aiChatSessionService) { this.financialAgent = financialAgent; this.financialIntentExecutor = financialIntentExecutor; this.aiSessionUserContext = aiSessionUserContext; this.mongoChatMemoryStore = mongoChatMemoryStore; this.aiChatSessionService = aiChatSessionService; } @Operation(summary = "财务智能体对话") @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8") public Flux chat(@RequestBody ChatForm chatForm) { 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 = financialIntentExecutor.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); } if (isBusinessDataIntent(userMessage)) { String noGuessResponse = "未识别到可执行的数据查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、客户、供应商或单号后再查询。"; mongoChatMemoryStore.appendMessages( memoryId, List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse)) ); aiChatSessionService.refreshSessionStats(memoryId, loginUser); return Flux.just(noGuessResponse); } return financialAgent.chat(memoryId, userMessage, currentDateForPrompt()) .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser)) .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, 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())); } private String currentDateForPrompt() { return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT); } private boolean isBusinessDataIntent(String message) { if (!StringUtils.hasText(message)) { return false; } String text = message.trim(); return containsAny(text, "查询", "查看", "统计", "分析", "建议", "成本核算", "产品成本", "工序成本", "订单利润", "亏损订单", "低利润", "库存资金", "库存积压", "呆滞库存", "现金流", "回款风险", "付款压力", "资金缺口", "应收", "应付", "异常预警", "经营异常", "风险预警", "驾驶舱", "经营看板", "经营总览", "日报", "周报", "经营报告", "分析报告", "业财融合", "口径", "指标解释"); } private boolean containsAny(String text, String... keywords) { for (String keyword : keywords) { if (text.contains(keyword)) { return true; } } return false; } }