package com.xly.service; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSON; import com.xly.agent.ChatiAgent; import com.xly.agent.DynamicTableNl2SqlAiAgent; import com.xly.agent.ErpAiAgent; import com.xly.agent.SceneSelectorAiAgent; import com.xly.config.OperableChatMemoryProvider; import com.xly.constant.CommonConstant; import com.xly.constant.ReturnTypeCode; import com.xly.entity.*; import com.xly.exception.sqlexception.SqlGenerateException; import com.xly.mapper.ToolMetaMapper; import com.xly.runner.AppStartupRunner; import com.xly.tool.DynamicToolProvider; import com.xly.util.EnhancedErrorGuidance; import com.xly.util.InputPreprocessor; import com.xly.util.SqlValidateUtil; import com.xly.util.ValiDataUtil; import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.ChatMessageType; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.service.AiServices; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.*; @Service @RequiredArgsConstructor @Slf4j public class XlyErpService { //中文对话模型 private final OllamaChatModel chatModel; private final ChatLanguageModel chatiModel; private final ChatLanguageModel sqlChatModel; private final SceneSelectorAiAgent sceneSelectorAiAgent; private final UserSceneSessionService userSceneSessionService; private final DynamicToolProvider dynamicToolProvider; private final OperableChatMemoryProvider operableChatMemoryProvider; private final DynamicExeDbService dynamicExeDbService; private final ToolMetaMapper toolMetaMapper; //执行动态语句 执行异常的情况下 最多执行次数 private final Integer maxRetries = 5; //没有找到对应方法重走一次补偿次数 public final static Integer maxTollRetries = 1; /*** * @Author 钱豹 * @Date 19:18 2026/1/27 * @Param [userInput, userId, sUserType] * @return java.lang.String * @Description 问答 **/ public AiResponseDTO erpUserInput(String userInput, String userId , String sUserName , String sBrandsId , String sSubsidiaryId, String sUserType, String authorization) { String sceneName = StrUtil.EMPTY; String methodName = StrUtil.EMPTY; try { // 0. 预处理用户输入:去空格、转小写(方便匹配) String input= InputPreprocessor.preprocessWithCommons(userInput); // 1. 初始化用户场景会话(权限内场景) UserSceneSession session = userSceneSessionService.getUserSceneSession(userId,sUserName,sBrandsId,sSubsidiaryId,sUserType,authorization); session.setAuthorization(authorization); session.setSFunPrompts(null); sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():StrUtil.EMPTY; // 2. 特殊指令:重置场景(无论是否已选,都可重置) if (input.contains("重置") || input.contains("重新选择")) { //清除记忆缓存 operableChatMemoryProvider.clearSpecifiedMemory(userId); return AiResponseDTO.builder().aiText(resetUserScene(userId,session)).build(); } //聊天只能体 if (session.getCurrentScene() != null && Objects.equals(session.getCurrentScene().getSSceneNo(), "ChatZone")) { return getChatiAgent(input, session,StrUtil.EMPTY); } // 3. 未选场景:先展示场景选择界面,处理用户序号选择 if (!session.isSceneSelected() && ValiDataUtil.me().isPureNumber(input)){ // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示) return handleSceneSelect(userId, input, session); } // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续 ErpAiAgent aiAgent = createErpAiAgent(userId, input, session); // 没有选择到场景,进闲聊模式 if (aiAgent == null){ return getChatiAgent (input, session,StrUtil.EMPTY); } // List chatMessage = operableChatMemoryProvider.getCurrentChatMessages(session.getUserId()); String sResponMessage = aiAgent.chat(userId, input); // List chatMessage2 = operableChatMemoryProvider.getCurrentChatMessages(session.getUserId()); String sResponMessageOld = StrUtil.EMPTY; // 调用方法,参数缺失部分提示,就直接使用方法返回的 if(session.getCurrentTool() != null && session.getSFunPrompts()!=null ){ // 缺失的参数明细 sResponMessage = session.getSFunPrompts(); } if (session.getCurrentTool()== null){ sResponMessageOld = sResponMessage; sResponMessage = StrUtil.EMPTY; } //5.执行工具方法后,清除记忆 if(session.getBCleanMemory()){ doCleanUserMemory(session,userId); } // 6.找到方法并且本方法带表结构描述时,需要调用 自然语言转SQL智能体 if((ObjectUtil.isNotEmpty(session.getCurrentTool()) && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo())) ){ sResponMessage = getDynamicTableSql(session, input, userId, userInput,0,StrUtil.EMPTY,StrUtil.EMPTY,"0",StrUtil.EMPTY); } //如果返回空的进入闲聊模式 if (ObjectUtil.isEmpty(sResponMessage)){ return getChatiAgent (input, session,sResponMessageOld); } if (session.getCurrentScene()!= null ){ return AiResponseDTO.builder().aiText(sResponMessage) .sSceneName(session.getCurrentScene().getSSceneName()) .sMethodName((ObjectUtil.isEmpty(session.getCurrentTool()))?StrUtil.EMPTY:session.getCurrentTool().getSMethodName()) .sReturnType(ReturnTypeCode.MAKEDOWN.getCode()) .build(); }else { return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText("当前场景:没有选择 退回当前场景 请输入 "+ CommonConstant.RESET + sResponMessage).sReturnType(ReturnTypeCode.HTML.getCode()).build(); } } catch (Exception e) { return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText("系统异常:" + e.getMessage() + ",请稍后重试!").sReturnType(ReturnTypeCode.HTML.getCode()).build(); } } public AiResponseDTO cleanMemory( String userId , String sUserName , String sBrandsId , String sSubsidiaryId, String sUserType, String authorization) { UserSceneSession session = userSceneSessionService.getUserSceneSession(userId,sUserName,sBrandsId,sSubsidiaryId,sUserType,authorization); operableChatMemoryProvider.clearSpecifiedMemory(userId); session.setCurrentTool(null); session.setBCleanMemory(false); UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():StrUtil.EMPTY; return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(StrUtil.EMPTY).systemText("清除记忆成功!").sReturnType(ReturnTypeCode.HTML.getCode()).build(); } /*** * @Author 钱豹 * @Date 18:38 2026/2/5 * @Param [session, input, userId, userInput] * @return java.lang.String * @Description 获取执行动态SQL **/ private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput,Integer attempt,String errorSql,String errorMessage,String iErroCount,String historySqlList ){ String resultExplain = "信息模糊,请提供更具体的问题或指令"; try{ while (attempt < maxRetries) { try{ attempt = attempt+1; return getDynamicTableSqlExec(session, input, userId, userInput,errorSql,errorMessage,iErroCount,historySqlList); }catch (Exception e){ String erroMsg = e.getMessage(); String errorSqlOld = StrUtil.EMPTY; if(erroMsg.contains(EnhancedErrorGuidance.splitString) && erroMsg.split(EnhancedErrorGuidance.splitString).length>1){ errorSqlOld = erroMsg.split(EnhancedErrorGuidance.splitString)[1]; errorSqlOld = StrUtil.replace(errorSqlOld,";",""); if(StrUtil.isNotEmpty(historySqlList)){ historySqlList = historySqlList+"/"+errorSqlOld; }else{ historySqlList = errorSqlOld; } } String errorMessageOld = erroMsg.split(EnhancedErrorGuidance.splitString)[0]; if (attempt == maxRetries) { return resultExplain+"查询的SQL语句:"+historySqlList; } else { return getDynamicTableSql( session, input, userId, userInput, attempt,errorSqlOld,errorMessageOld,attempt.toString(),historySqlList); } } } }catch (Exception e){ }finally { doCleanUserMemory(session,userId); } return resultExplain; } /*** * @Author 钱豹 * @Date 19:59 2026/3/4 * @Param [session, sUserId] * @return void * @Description 删除用户记忆 方法 **/ private void doCleanUserMemory(UserSceneSession session,String userId){ operableChatMemoryProvider.clearSpecifiedMemory(userId); session.setCurrentTool(null); session.setSceneSelected(false); UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); session.setBCleanMemory(false); } /*** * @Author 钱豹 * @Date 19:49 2026/3/4 * @Param [session, input, userId, userInput] * @return java.lang.String * @Description 执行动态sSql **/ private String getDynamicTableSqlExec(UserSceneSession session,String input,String userId,String userInput,String errorSql,String errorMessage,String iErroCount,String historySqlList){ // 1. 构建自然语言转SQLAgent, DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session); String tableNames = session.getCurrentTool().getSInputTabelName(); // "订单表:viw_salsalesorder,客户信息表:elecustomer,结算方式表:sispayment,产品表(无单价,无金额,无数量):viw_product_sort,销售人员表:viw_sissalesman_depart"; String tableStruct = session.getCurrentTool().getSStructureMemo(); String sDataNow = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_TIME_FORMAT); log.info("当前时间:"+sDataNow); String rawSql = StrUtil.EMPTY; if(ObjectUtil.isEmpty(errorSql) && ObjectUtil.isEmpty(errorMessage)){ rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,sDataNow,userInput); }else{ rawSql = aiDynamicTableNl2SqlAiAgent.regenerateSqlWithError(userId, tableNames,tableStruct,sDataNow,userInput,errorSql,errorMessage,iErroCount,historySqlList); } String[] rawSqlA = rawSql.split(";"); if(rawSqlA.length>1){ rawSql = rawSqlA[rawSqlA.length-1]; } if (rawSql == null || rawSql.trim().isEmpty()) { throw new SqlGenerateException("SQL EMPTY"); } // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略) String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql); SqlValidateUtil.validateMysqlSql(cleanSql); // 4. 执行SQL获取结构化结果 // Map params = new HashMap<>(); List> sqlResult = new ArrayList<>(); try{ sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql); }catch (Exception e){ throw new SqlGenerateException(e.getMessage()+" OLDSQL "+cleanSql); } // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务) String resultJson = JSON.toJSONString(sqlResult); return aiDynamicTableNl2SqlAiAgent.explainSqlResult( userId, userInput, cleanSql, tableStruct, resultJson ); } /*** * @Author 钱豹 * @Date 11:22 2026/1/31 * @Param * @return * @Description 动态参数补齐处理 **/ private String dotoolExecutionRequests(AiMessage aiMessage){ String textTs = aiMessage.text(); if(aiMessage.hasToolExecutionRequests()){ List toolExecutionRequests = aiMessage.toolExecutionRequests(); toolExecutionRequests.forEach(toolRequests->{ String arguments = toolRequests.arguments(); log.info(arguments); }); } return textTs; } /*** * 存入全部场景 * @Author 钱豹 * @Date 19:06 2026/1/26 * @Param [sUserId, sUserType] * @return java.lang.String * 页面刷新/首次进入时调用:初始化用户场景会话,直接返回场景选择引导词 * 前端页面加载完成后,无需用户输入,直接调用该方法即可显示引导词 * @param sUserId 用户ID(前端传入,如user-001) sUserType 角色状态 * @return 场景选择引导词(即原buildSceneSelectHint生成的文案) */ public AiResponseDTO initSceneGuide(String systemText,String sUserId,String sUserName,String sBrandsId,String sSubsidiaryId,String sUserType,String authorization) { try { UserSceneSession userSceneSession = userSceneSessionService.getUserSceneSession( sUserId,sUserName,sBrandsId,sSubsidiaryId,sUserType,authorization); systemText = userSceneSession.buildSceneSelectHint(); } catch (Exception e) { systemText = "

抱歉,你暂无任何业务场景的访问权限,请联系管理员开通!

"; } return AiResponseDTO.builder().aiText(StrUtil.EMPTY).systemText(systemText) .build(); } // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== private DynamicTableNl2SqlAiAgent createDynamicTableNl2SqlAiAgent(String userId, String userInput, UserSceneSession session) { // 4. 获取/创建用DynamicTableNl2SqlAiAgent DynamicTableNl2SqlAiAgent aiAgent = UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.get(userId); if(ObjectUtil.isEmpty(aiAgent)){ aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class) .chatLanguageModel(sqlChatModel) .chatMemoryProvider(operableChatMemoryProvider) .toolProvider(dynamicToolProvider) .build(); UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent); } return aiAgent; } // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) { // 1. 已选场景:强制绑定该场景工具 if (session.isSceneSelected() && session.getCurrentScene() != null) { dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); } else { // 2. 未选场景:大模型根据输入返加相应的场景 SceneDto sceneDto = parseSceneByLlm(userId, userInput, session); if (sceneDto != null) { session.setCurrentScene(sceneDto); session.setSceneSelected(true); UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); }else {return null;} } // 4. 获取/创建用Agent ErpAiAgent aiAgent = UserSceneSessionService.ERP_AGENT_CACHE.get(userId); if(ObjectUtil.isEmpty(aiAgent)){ aiAgent = AiServices.builder(ErpAiAgent.class) .chatLanguageModel(chatModel) .chatMemoryProvider(operableChatMemoryProvider) .toolProvider(dynamicToolProvider) // .toolChoice(ChatCompletionToolChoice.ofRequired()) // 👈 必须调用一个工具 .build(); UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent); // 初始化AiService 以防止热加载太慢 找不到相应的方法 aiAgent.chat(userId, "initAiService"); log.info("用户{}Agent构建完成,已选场景:{},场景ID{}", userId, session.isSceneSelected() ? session.getCurrentScene().getSSceneName() : "未选(全场景匹配)", dynamicToolProvider.sSceneIdMap.get(userId)); } return aiAgent; } /** * 大模型意图解析核心方法(获取场景) * @param userId 用户ID * @param userInput 用户输入 * @param session 用户会话 * @return 匹配的BusinessScene,null表示解析失败 */ private SceneDto parseSceneByLlm(String userId, String userInput, UserSceneSession session) { try { List metasAll = session.getAuthTool(); // toolMetaMapper.findAll(); // 1. 构建大模型意图解析请求 String authScenesDesc =session.buildAuthScenesForLlm(metasAll); // 2. 调用大模型解析意图,LangChain4j自动将大模型输出映射为SceneIntentParseResp // {{authScenesDesc}} SceneIntentParseResp parseResp = sceneSelectorAiAgent.parseSceneIntent(userInput,authScenesDesc); // authScenesDesc // 3. 解析结果处理 if (parseResp == null || parseResp.getSceneCode() == null || "NO_MATCH".equals(parseResp.getSceneCode())) { log.warn("用户{}大模型未匹配到任何场景,输入:{}", userId, userInput); return null; } // 4. 将场景编码转换为BusinessScene枚举 String sSceneNo = parseResp.getScene(); return AppStartupRunner.getAiAgentByCode(sSceneNo); } catch (Exception e) { log.error("用户{}大模型意图解析失败,输入:{}", userId, userInput, e); return null; } } /*** * @Author 钱豹 * @Date 19:28 2026/1/26 * @Param [userId, session] * @return java.lang.String * @Description 重置用户场景选择:恢复为未选状态,清空当前场景,重新展示选择界面 **/ private String resetUserScene(String userId, UserSceneSession session) { session.setSceneSelected(false); session.setBCleanMemory(false); session.setCurrentTool(null); session.setCurrentScene(null); session.setCurrentRowData(null); UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); // 清空Agent缓存 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint(); } /** * 处理用户场景选择:输入序号→匹配场景→更新会话状态 */ private AiResponseDTO handleSceneSelect(String userId, String userInput, UserSceneSession session) { // 1. 尝试根据序号匹配场景 boolean selectSuccess = session.selectSceneByInput(userInput); String sceneName = StrUtil.EMPTY; String methodName = StrUtil.EMPTY; if (selectSuccess) { // 2. 选择成功:更新缓存,返回成功提示 UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); // 清空该用户原有Agent缓存(重新构建绑定新场景的Agent) UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); //清除记忆缓存 operableChatMemoryProvider.clearSpecifiedMemory(userId); String aiText = "智能体选择成功! 现在可以问她相关问题(如" + String.join("、", session.getCurrentScene().getSSceneContext()) + ")"; sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSControlName():StrUtil.EMPTY; return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(aiText).sSceneName(session.getCurrentScene().getSSceneName()).build(); } else { // 3. 选择失败:重新展示场景选择提示 return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(session.buildSceneSelectHint()).build(); } } /*** * @Author 钱豹 * @Date 13:32 2026/2/6 * @Param [input, session] * @return java.lang.String * @Description 获取智普通智能体 **/ private AiResponseDTO getChatiAgent (String input,UserSceneSession session,String sResponMessageOld){ String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():"随便聊聊"; if(ObjectUtil.isNotEmpty(sResponMessageOld)){ return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sResponMessageOld).systemText(StrUtil.EMPTY).sReturnType(ReturnTypeCode.HTML.getCode()).build(); } ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId()); if(ObjectUtil.isEmpty(chatiAgent)){ chatiAgent = AiServices.builder(ChatiAgent.class) .chatLanguageModel(chatiModel) .chatMemoryProvider(operableChatMemoryProvider) .build(); UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent); } String sChatMessage = chatiAgent.chat(session.getUserId(), input); return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sChatMessage).systemText(StrUtil.EMPTY).sReturnType(ReturnTypeCode.HTML.getCode()).build(); } /*** * @Author 钱豹 * @Date 12:14 2026/3/8 * @Param [chatMessage, memoryId] * @return void * @Description 随便聊聊移除最后一个AI 返回信息 跟系统询问的随便聊聊 **/ private void removeMssageSbll(List chatMessage,String memoryId){ if(chatMessage!=null){ // operableChatMemoryProvider.deleteSingleMessage(memoryId,chatMessage.get(chatMessage.size()-1)); for(int i=chatMessage.size()-1;i>0;i--){ ChatMessage data = chatMessage.get(i); ChatMessageType sType = data.type(); if(ChatMessageType.SYSTEM.equals(sType)){ operableChatMemoryProvider.deleteSingleMessage(memoryId,data); return; } } } } }