XlyErpService.java 19.9 KB
package com.xly.service;

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.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.model.chat.ChatLanguageModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.service.AiServices;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.python.antlr.ast.Str;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
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;



    /***
     * @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);
            }

            // 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);
            }
            String sResponMessage = aiAgent.chat(userId, input);
//            调用方法,参数缺失部分提示,就直接使用方法返回的
            if(session.getCurrentTool() != null
                    && session.getSFunPrompts()!=null
            ){ // 缺失的参数明细
                sResponMessage = session.getSFunPrompts();
            }
            if (session.getCurrentTool()== null){
                sResponMessage = StrUtil.EMPTY;
            }
            //5.执行工具方法后,清除记忆
            if(session.getBCleanMemory()){
                operableChatMemoryProvider.clearSpecifiedMemory(userId);
                session.setCurrentTool(null);
                session.setBCleanMemory(false);
            }
//          6.找到方法并且本方法带表结构描述时,需要调用 自然语言转SQL智能体
            if((ObjectUtil.isNotEmpty(session.getCurrentTool())
                    && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName())
                    && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo()))
            ){
                sResponMessage = getDynamicTableSql(session, input, userId, userInput);
            }
            //如果返回空的进入闲聊模式
            if (ObjectUtil.isEmpty(sResponMessage)){
                return getChatiAgent (input, session);
            }
            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);
        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){
        String resultExplain = StrUtil.EMPTY;
        try{
            // 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 rawSql =  aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,userInput);
            if (rawSql == null || rawSql.trim().isEmpty()) {
                throw new SqlGenerateException("AI服务生成SQL失败,返回结果为空");
            }
            // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略)
            String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql);
            SqlValidateUtil.validateMysqlSql(cleanSql);
            log.info("SQL清理并强校验通过,可执行SQL:{}", cleanSql);
            // 4. 执行SQL获取结构化结果
//                Map<String,Object> params = new HashMap<>();
            List<Map<String, Object>> sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql);
            // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务)
            String resultJson = JSON.toJSONString(sqlResult);
            resultExplain = aiDynamicTableNl2SqlAiAgent.explainSqlResult(
                    userId,
                    userInput,
                    cleanSql,
                    tableStruct,
                    resultJson
            );
        }catch (Exception e){
            session.setCurrentTool(null);
            resultExplain = "动态SQL执行错误,请提供更具体的问题或指令";
        }
        log.info("动态表结构NL2SQL流程执行完成");
        return resultExplain;
    }




    /***
     * @Author 钱豹
     * @Date 11:22 2026/1/31
     * @Param
     * @return
     * @Description 动态参数补齐处理
     **/
    private String dotoolExecutionRequests(AiMessage aiMessage){
        String textTs = aiMessage.text();
        if(aiMessage.hasToolExecutionRequests()){
            List<ToolExecutionRequest> 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 = "<p style='color:red;'>抱歉,你暂无任何业务场景的访问权限,请联系管理员开通!</p>";
        }
        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)
                    .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<ToolMeta> 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.getSceneCode();
            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){
        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);
        String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY;
        String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():"随便聊聊";
        return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sChatMessage).systemText(StrUtil.EMPTY).sReturnType(ReturnTypeCode.HTML.getCode()).build();
    }

}