From 81a572e65ebc5788208e462747e91f797fea2536 Mon Sep 17 00:00:00 2001 From: qianbao Date: Tue, 3 Mar 2026 10:39:58 +0800 Subject: [PATCH] 1111 --- pom.xml | 1 + src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java | 9 +++++---- src/main/java/com/xly/entity/UserSceneSession.java | 6 ++++++ src/main/java/com/xly/service/XlyErpService.java | 2 ++ src/main/java/com/xly/tool/DynamicToolProvider.java | 165 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------- src/main/java/com/xly/tts/service/PythonTtsProxyService.java | 1 - src/main/java/com/xly/util/AIModelDataFormatter.java | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main/java/com/xly/util/UserChoseIntentParser.java | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/main/resources/templates/chat.html | 6 +++--- 9 files changed, 231 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/xly/util/AIModelDataFormatter.java create mode 100644 src/main/java/com/xly/util/UserChoseIntentParser.java diff --git a/pom.xml b/pom.xml index 3b22e6d..2ba3470 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,7 @@ org.springframework.boot spring-boot-starter-webflux + org.springframework.boot spring-boot-starter-thymeleaf diff --git a/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java b/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java index da4a81c..ec78a8f 100644 --- a/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java +++ b/src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java @@ -23,10 +23,11 @@ public interface DynamicTableNl2SqlAiAgent { 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; 3. 编写规范: 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); - 3.2 SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件 - 3.3 SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用 - 3.4 SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断,用示例格式判断,示例:ifnull(customername,'')<>'' - 3.5 SQL所有的显示字段的别名中,不能出现空格,如: tCreateDate as earliest 订单日期,正确的应是 tCreateDate as earliest订单日期 + 3.2 SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件; + 3.3 SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用; + 3.4 SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断,用示例格式判断,示例:ifnull(customername,'')<>''; + 3.5 SQL所有的查询条件,如果是日期类型的字段,均需要加不为空判断,用示例格式判断,示例:tmakedate is not Null; + 3.6 SQL所有的显示字段的别名中,不能出现空格,如: tCreateDate as earliest 订单日期,正确的应是 tCreateDate as earliest订单日期 4. 安全约束:禁止生成任何DDL/DML语句(DROP/ALTER/INSERT/UPDATE/DELETE等),禁止使用子查询、存储过程、自定义函数、临时表; 5. 精准性: 5.1 严格按用户需求+传入的表结构生成,仅使用指定字段/表,无多余字段、无无效表关联、无冗余过滤条件; diff --git a/src/main/java/com/xly/entity/UserSceneSession.java b/src/main/java/com/xly/entity/UserSceneSession.java index d08a292..b200072 100644 --- a/src/main/java/com/xly/entity/UserSceneSession.java +++ b/src/main/java/com/xly/entity/UserSceneSession.java @@ -41,6 +41,12 @@ public class UserSceneSession { private SceneDto currentScene; private ToolMeta currentTool; + + /*** + * 当前未清返回的数据集 + **/ + private Map> currentRowData; + /*** * @Author 钱豹 * @Date 10:07 2026/1/31 diff --git a/src/main/java/com/xly/service/XlyErpService.java b/src/main/java/com/xly/service/XlyErpService.java index 350bff9..b27c2ea 100644 --- a/src/main/java/com/xly/service/XlyErpService.java +++ b/src/main/java/com/xly/service/XlyErpService.java @@ -108,6 +108,7 @@ public class XlyErpService { //5.执行工具方法后,清除记忆 if(session.getBCleanMemory()){ operableChatMemoryProvider.clearSpecifiedMemory(userId); + session.setCurrentTool(null); session.setBCleanMemory(false); } // 6.找到方法并且本方法带表结构描述时,需要调用 自然语言转SQL智能体 @@ -318,6 +319,7 @@ public class XlyErpService { 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); diff --git a/src/main/java/com/xly/tool/DynamicToolProvider.java b/src/main/java/com/xly/tool/DynamicToolProvider.java index 0f8d74e..2c02422 100644 --- a/src/main/java/com/xly/tool/DynamicToolProvider.java +++ b/src/main/java/com/xly/tool/DynamicToolProvider.java @@ -20,10 +20,7 @@ import com.xly.mapper.ParamRuleMapper; import com.xly.mapper.ToolMetaMapper; import com.xly.service.DynamicExeDbService; import com.xly.service.UserSceneSessionService; -import com.xly.util.DeepCopyUtils; -import com.xly.util.HttpsRequestUtil; -import com.xly.util.JsonUtils; -import com.xly.util.OkHttpUtil; +import com.xly.util.*; import dev.langchain4j.agent.tool.*; import dev.langchain4j.data.message.ChatMessage; @@ -233,7 +230,15 @@ public class DynamicToolProvider implements ToolProvider { StringBuffer sl = new StringBuffer(); if(ObjectUtil.isNotEmpty(meta.getStoolDesc())){ - stoolDesc.append("MethodNo:").append(meta.getSMethodNo()).append(",核心工作内容:【").append(meta.getSMethodName()).append("】").append(meta.getStoolDesc()); + stoolDesc.append("MethodNo:").append(meta.getSMethodNo()).append(",核心工作内容:【").append(meta.getSMethodName()); +// if (meta.getIBizType()==4){ +// stoolDesc.append(",").append("并选择数据后执行["+meta.getSControlName()+"]操作"); +// } + stoolDesc.append("】").append(meta.getStoolDesc()); + if (meta.getIBizType()==4){ + stoolDesc.append(",").append("并选择数据后执行 "+meta.getSControlName()+" 操作"); +// .append("1.全部数据生成多个单据 回复【全部确认】;2.全部数据生成一个单据 回复【合并确认】;3.按自然语义描述生成一个单据 如"1,3行确认""); + } } if("boxQuote".equals(meta.getSMethodNo())){ log.info(meta.getSParamRules()); @@ -558,6 +563,14 @@ public class DynamicToolProvider implements ToolProvider { // {"0":"查询","1":"执行"} 查询不需要确认 Boolean isConfirmed = isConfirmed(input) || input.contains("生成") || input.contains("确认"); + //判断是否生成数据 + List> sRowData = new ArrayList<>(); + String sHandleType = "merge"; + if(4== meta.getIBizType() && ObjectUtil.isNotEmpty(session.getCurrentRowData())){ + Map sRowDataMap = UserChoseIntentParser.getSelectedRows( input, session.getCurrentRowData()); + sRowData = (List>) sRowDataMap.get("sRowData"); + sHandleType = sRowDataMap.get("sHandleType").toString(); + } if((isConfirmed || 0== meta.getIActionType()) && 5!= meta.getIBizType()){ // 确认后必填项校验 List missingAfter = checkConfirmAfterParam(args, paramRuleData); @@ -575,9 +588,9 @@ public class DynamicToolProvider implements ToolProvider { askconfirmMsg = buildConfirmUserMessage(meta, args); }else if(4== meta.getIBizType() || meta.getIBizType()==5){ askconfirmMsg = doGetFromData( meta,args,session); - session.setSFunPrompts(askconfirmMsg); - operableChatMemoryProvider.get(memoryId).add(UserMessage.from("SYSTEM: 等待用户确认或选择部分数据操作")); - return String.valueOf(successResult(toolExecutionRequest,askconfirmMsg)); +// session.setSFunPrompts(askconfirmMsg); +// operableChatMemoryProvider.get(memoryId).add(UserMessage.from("SYSTEM: 等待用户确认或选择部分数据操作")); + return executeWithConfirmation(toolExecutionRequest,askconfirmMsg,operableChatMemoryProvider.get(memoryId), session, meta).text(); }else{ askconfirmMsg =getDefMessage(args,meta.getSControlName(),meta); } @@ -863,64 +876,64 @@ public class DynamicToolProvider implements ToolProvider { **/ private String executeToolAfter(ToolMeta meta, Map args,ToolExecutionRequest toolExecutionRequest,List paramDefs,UserSceneSession session) { // {"1":"存储过程","2":"SQL查询","3":"第三方API","4":"窗体查询","5":"按钮执行","6":"其它"} - String sBizContent = meta.getSBizContent(); - Integer iBizType = meta.getIBizType(); - args.put("sUserId",session.getUserId()); - args.put("sLoginId",session.getUserName()); - args.put("sMakePerson",session.getUserName()); - args.put("sBrId",session.getSBrandsId()); - args.put("sBrandsId",session.getSBrandsId()); - args.put("sSuId",session.getSSubsidiaryId()); - args.put("sSubsidiaryId",session.getSSubsidiaryId()); - if(iBizType==1 || iBizType==4){ - Map data = new HashMap<>(args); - data.put("sData", JSONObject.toJSONString(data)); - Map searMap = this.dynamicExeDbService.getDoProMap(sBizContent, data); - Map sReturn = this.dynamicExeDbService.getCallPro(searMap,sBizContent); - Integer sCode = ObjectUtil.isNotEmpty(sReturn.get(ProcedureConstant.SCODE))? Integer.valueOf(sReturn.get(ProcedureConstant.SCODE).toString()):0; - String sMsgText = ObjectUtil.isNotEmpty(sReturn.get(ProcedureConstant.SRETURN))? sReturn.get(ProcedureConstant.SRETURN).toString():"操作成功"; - if(sCode< 0){ - String msg = ObjectUtil.isEmpty(sMsgText) ?"调用过程sCode:"+Integer.valueOf(searMap.get(ProcedureConstant.SCODE).toString()):sMsgText; - return String.valueOf(askUserResult(toolExecutionRequest, msg)); - } - session.setSFunPrompts(sMsgText); - return sMsgText; - }else if(iBizType==2 && ObjectUtil.isNotEmpty(sBizContent)){ - //SQL查询 - if(sBizContent.toLowerCase().startsWith("update")){ - this.dynamicExeDbService.updateSql(args,sBizContent); - }else if(sBizContent.toLowerCase().startsWith("delete")){ - this.dynamicExeDbService.delSql(args,sBizContent); - }else if(sBizContent.toLowerCase().startsWith("insert")){ - this.dynamicExeDbService.addSql(args,sBizContent); - }else{ - List> retData = this.dynamicExeDbService.findSql(args,sBizContent); - if(ObjectUtil.isNotEmpty(retData)){ - StringBuffer sb = new StringBuffer(); - retData.forEach(one->{ - one.forEach((k,v)->{ - sb.append(v).append(" "); - }); - sb.append("
"); - }); - if(ObjectUtil.isNotEmpty(retData)){ - sb.append("请根据这些信息安排今天的工作吧!如果有具体任务需要进一步处理,请告诉我"); - } - session.setSFunPrompts(sb.toString()); - if("queryTodayTask".equals(meta.getSMethodNo())){ - session.setBCleanMemory(true); - } - return String.valueOf(successResult(toolExecutionRequest, sb.toString())); - }else{ - session.setSFunPrompts("未找到对应的数据"); - return "未找到对应的数据"; - } - } - }else if(iBizType==3){ - return HttpsRequestUtil.me().doRequestHttp(sBizContent,JSONUtil.toJsonStr(args), - new HashMap<>(),"POST","JSON"); - } - return String.valueOf(successResult(toolExecutionRequest, "操作成功")); + String sBizContent = meta.getSBizContent(); + Integer iBizType = meta.getIBizType(); + args.put("sUserId", session.getUserId()); + args.put("sLoginId", session.getUserName()); + args.put("sMakePerson", session.getUserName()); + args.put("sBrId", session.getSBrandsId()); + args.put("sBrandsId", session.getSBrandsId()); + args.put("sSuId", session.getSSubsidiaryId()); + args.put("sSubsidiaryId", session.getSSubsidiaryId()); + if (iBizType == 1 || iBizType == 4) { + Map data = new HashMap<>(args); + data.put("sData", JSONObject.toJSONString(data)); + Map searMap = this.dynamicExeDbService.getDoProMap(sBizContent, data); + Map sReturn = this.dynamicExeDbService.getCallPro(searMap, sBizContent); + Integer sCode = ObjectUtil.isNotEmpty(sReturn.get(ProcedureConstant.SCODE)) ? Integer.valueOf(sReturn.get(ProcedureConstant.SCODE).toString()) : 0; + String sMsgText = ObjectUtil.isNotEmpty(sReturn.get(ProcedureConstant.SRETURN)) ? sReturn.get(ProcedureConstant.SRETURN).toString() : "操作成功"; + if (sCode < 0) { + String msg = ObjectUtil.isEmpty(sMsgText) ? "调用过程sCode:" + Integer.valueOf(searMap.get(ProcedureConstant.SCODE).toString()) : sMsgText; + return String.valueOf(askUserResult(toolExecutionRequest, msg)); + } + session.setSFunPrompts(sMsgText); + return sMsgText; + } else if (iBizType == 2 && ObjectUtil.isNotEmpty(sBizContent)) { + //SQL查询 + if (sBizContent.toLowerCase().startsWith("update")) { + this.dynamicExeDbService.updateSql(args, sBizContent); + } else if (sBizContent.toLowerCase().startsWith("delete")) { + this.dynamicExeDbService.delSql(args, sBizContent); + } else if (sBizContent.toLowerCase().startsWith("insert")) { + this.dynamicExeDbService.addSql(args, sBizContent); + } else { + List> retData = this.dynamicExeDbService.findSql(args, sBizContent); + if (ObjectUtil.isNotEmpty(retData)) { + StringBuffer sb = new StringBuffer(); + retData.forEach(one -> { + one.forEach((k, v) -> { + sb.append(v).append(" "); + }); + sb.append("
"); + }); + if (ObjectUtil.isNotEmpty(retData)) { + sb.append("请根据这些信息安排今天的工作吧!如果有具体任务需要进一步处理,请告诉我"); + } + session.setSFunPrompts(sb.toString()); + if ("queryTodayTask".equals(meta.getSMethodNo())) { + session.setBCleanMemory(true); + } + return String.valueOf(successResult(toolExecutionRequest, sb.toString())); + } else { + session.setSFunPrompts("未找到对应的数据"); + return "未找到对应的数据"; + } + } + } else if (iBizType == 3) { + return HttpsRequestUtil.me().doRequestHttp(sBizContent, JSONUtil.toJsonStr(args), + new HashMap<>(), "POST", "JSON"); + } + return String.valueOf(successResult(toolExecutionRequest, "操作成功")); } @@ -983,7 +996,7 @@ public class DynamicToolProvider implements ToolProvider { log.info("headers=============================={}", JSONObject.toJSONString(headers)); log.info("请求URL,JSON,headers=={},{},{}",sUrl,JSONObject.toJSONString(sBody),JSONObject.toJSONString(headers)); ErpResult erpResult = JsonUtils.toObject(result,ErpResult.class); - result = buildResultMessageWithTable( meta, erpResult); + result = buildResultMessageWithTable( meta, erpResult, session); }catch (Exception e){ result ="执行异常:"+e.getMessage(); } @@ -993,8 +1006,8 @@ public class DynamicToolProvider implements ToolProvider { /** * 构建 窗体获取数据方法 未清或者明细 */ - public String buildResultMessageWithTable(ToolMeta meta,ErpResult erpResult){ - + public String buildResultMessageWithTable(ToolMeta meta,ErpResult erpResult,UserSceneSession session){ + Map> currentRowData = new HashMap<>(); ErpDataset dataset = erpResult.getDataset(); //返回错误信息 if(erpResult.getCode()<0 && ObjectUtil.isNotEmpty(erpResult.getMsg())){ @@ -1037,24 +1050,36 @@ public class DynamicToolProvider implements ToolProvider { headers.forEach(header -> markdown.append(header).append(" | ")); markdown.append("\n|").append("---|".repeat(headers.size() + 1)).append("\n"); // 填充表格数据 + List> machineData = new LinkedList<>(); for (int i = 0; i < recordData.size(); i++) { // 保存隐藏列的值(如"唯一"字段) String uniqueValue = recordData.get(i).get("sSlaveId") != null ? recordData.get(i).get("sSlaveId").toString() : ""; markdown.append("| ").append(i + 1).append(" | "); + Map rMap = new HashMap<>(); for (String header : headers) { // 这里需要根据你的数据结构来获取对应的值 Object value = recordData.get(i)!= null ? recordData.get(i).get(header) : null; markdown.append(value != null ? value : "—").append(" | "); + rMap.put(header,value); } + rMap.put("sSlaveId",uniqueValue); + rMap.put("唯一",uniqueValue); // 在行末添加隐藏数据的特殊标记(AI可以解析) - markdown.append(" "); + markdown.append(" "); markdown.append("\n"); + machineData.add(rMap); + currentRowData.put(i + 1,recordData.get(i)); } markdown.append(">"); +// // 4. 机器可读的结构化数据(只出现一次!) +// markdown.append("\n"); +// markdown.append(JSONUtil.toJsonStr(machineData)); +// markdown.append("\n\n\n"); if(meta.getIBizType()==4){ markdown.append("\n---\n"); appendConfirmAll(markdown,meta.getSControlName()); } + session.setCurrentRowData(currentRowData); return markdown.toString(); } diff --git a/src/main/java/com/xly/tts/service/PythonTtsProxyService.java b/src/main/java/com/xly/tts/service/PythonTtsProxyService.java index 287236d..032b599 100644 --- a/src/main/java/com/xly/tts/service/PythonTtsProxyService.java +++ b/src/main/java/com/xly/tts/service/PythonTtsProxyService.java @@ -77,7 +77,6 @@ public class PythonTtsProxyService { String sUserType = request.getUsertype(); String authorization = request.getAuthorization(); //校验登录token 是否有效 - AiResponseDTO voiceText = xlyErpService.erpUserInput(userInput,sUserId,sUserName,sBrandsId,sSubsidiaryId,sUserType, authorization); return synthesizeStreamAi(request,voiceText); } diff --git a/src/main/java/com/xly/util/AIModelDataFormatter.java b/src/main/java/com/xly/util/AIModelDataFormatter.java new file mode 100644 index 0000000..14b9928 --- /dev/null +++ b/src/main/java/com/xly/util/AIModelDataFormatter.java @@ -0,0 +1,68 @@ +package com.xly.util; +import cn.hutool.json.JSONUtil; +import com.xly.entity.ToolMeta; + +import java.util.*; + +public class AIModelDataFormatter { + public static String formatDataForAI(ToolMeta meta, Integer TotalCount, + List> rows, + Set headers) { + StringBuilder response = new StringBuilder(); + // 3. **关键:先显示用户友好的表格** + response.append("## 查询结果(共").append(TotalCount).append("条)\n\n"); + response.append(buildMarkdownTable(rows, headers)); + response.append("\n"); + // 4. 机器可读的结构化数据(只出现一次!) + response.append("\n"); + response.append(buildMachineReadableData(rows,headers)); + response.append("\n\n\n"); + return response.toString(); + } + + /** + * 构建Markdown表格 + */ + public static String buildMarkdownTable(List> rows, Set headers) { + StringBuilder table = new StringBuilder(); + // 表头 + table.append("| 序号 | "); + for (String header : headers) { + table.append(header).append(" | "); + } + table.append("\n|"); + table.append("---|".repeat(headers.size() + 1)); + table.append("\n"); + // 表格数据 + for (int i = 0; i < rows.size(); i++) { + Map row = rows.get(i); + table.append("| ").append(i + 1).append(" | "); + for (String header : headers) { + Object value = row.get(header); + table.append(value != null ? value : "—").append(" | "); + } + table.append("\n"); + } + + return table.toString(); + } + + /** + * 构建机器可读数据 + */ + public static String buildMachineReadableData(List> rows, Set headers) { + List> dataList = new ArrayList<>(); + for (int i = 0; i < rows.size(); i++) { + Map row = rows.get(i); + Map item = new LinkedHashMap<>(); + item.put("序号", i + 1); + for (String header : headers) { + Object value = row.get(header); + item.put(header,value); + } + dataList.add(item); + } + return JSONUtil.toJsonStr(dataList); + } + +} \ No newline at end of file diff --git a/src/main/java/com/xly/util/UserChoseIntentParser.java b/src/main/java/com/xly/util/UserChoseIntentParser.java new file mode 100644 index 0000000..ed2c6ff --- /dev/null +++ b/src/main/java/com/xly/util/UserChoseIntentParser.java @@ -0,0 +1,51 @@ +package com.xly.util; + +import java.util.*; +import java.util.stream.Collectors; + +public class UserChoseIntentParser { + + /** + * @param input 用户输入:全部确认 / 合并确认 / 1,3行确认 + * @param rowMap 上面parse出来的全量数据 + * @return 最终要生成订单的明细列表 + */ + public static Map getSelectedRows(String input, Map> rowMap) { + List> sRowData = new ArrayList<>(); + Map rMap = new HashMap<>(); + String sHandleType = "merge";//单个的 就是全部确认 否则就是合并确认 + if (input.contains("全部确认") || input.contains("合并确认")) { + if(input.contains("全部确认")){ + sHandleType = "single"; + } + sRowData = new ArrayList<>(rowMap.values()); + rMap.put("sRowData",sRowData); + rMap.put("sHandleType",sHandleType); + return rMap; + } + + // 解析 1,3,5-7 这类行号 + Set selected = new HashSet<>(); + String[] parts = input.replaceAll("[^0-9,-]", "").split("[,,]"); + for (String part : parts) { + try { + if (part.contains("-")) { + String[] range = part.split("-"); + int start = Integer.parseInt(range[0]); + int end = Integer.parseInt(range[1]); + for (int i = start; i <= end; i++) { + selected.add(i); + } + } else { + selected.add(Integer.parseInt(part)); + } + } catch (Exception ignored) {} + } + rMap.put("sRowData",selected.stream() + .filter(rowMap::containsKey) + .map(rowMap::get) + .collect(Collectors.toList())); + rMap.put("sHandleType",sHandleType); + return rMap; + } +} diff --git a/src/main/resources/templates/chat.html b/src/main/resources/templates/chat.html index 99528e0..9f7f733 100644 --- a/src/main/resources/templates/chat.html +++ b/src/main/resources/templates/chat.html @@ -462,13 +462,13 @@