From 1599dfd45f2f5b31a14d4849a23d5878239cf570 Mon Sep 17 00:00:00 2001 From: qianbao Date: Thu, 30 Jul 2026 09:21:04 +0800 Subject: [PATCH] 添加未清选择 改成动态引导语 --- src/main/java/com/xly/agent/ChatiAgent.java | 53 ++++++++++++++++++++++++++++++++++------------------- src/main/java/com/xly/agent/SecMethodAiAgent.java | 14 ++++++++++++++ src/main/java/com/xly/agent/SystemPromptGenerator.java | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main/java/com/xly/config/ModelConfig.java | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------- src/main/java/com/xly/entity/MethodChoiceResult.java | 9 +++++++++ src/main/java/com/xly/service/MethodRouterService.java | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main/java/com/xly/service/UserSceneSessionService.java | 5 +---- src/main/java/com/xly/service/XlyErpService.java | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------- src/main/java/com/xly/tool/DynamicToolProvider.java | 305 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------------------- src/main/java/com/xly/tool/ToolSpecificationHolder.java | 8 +++++++- 10 files changed, 655 insertions(+), 233 deletions(-) create mode 100644 src/main/java/com/xly/agent/SecMethodAiAgent.java create mode 100644 src/main/java/com/xly/entity/MethodChoiceResult.java create mode 100644 src/main/java/com/xly/service/MethodRouterService.java diff --git a/src/main/java/com/xly/agent/ChatiAgent.java b/src/main/java/com/xly/agent/ChatiAgent.java index 9c8ff9a..37fe34b 100644 --- a/src/main/java/com/xly/agent/ChatiAgent.java +++ b/src/main/java/com/xly/agent/ChatiAgent.java @@ -2,30 +2,45 @@ package com.xly.agent; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.SystemMessage; +import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.V; -import reactor.core.publisher.Flux; +/** + * 闲聊智能体(小羚羊人设) + * 说明:同步/流式两个方法共用同一份人设提示词,改一处即可,避免两份拷贝走偏 + */ public interface ChatiAgent { - @SystemMessage(""" - 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 - 要求:1. 不生硬、不说教,避免书面化表达; - 2. 主动接梗,适当延伸话题,不一问一答; - 3. 偶尔带点小幽默,保持轻松无压力的氛围; - 4. 回答简洁,符合日常聊天的语气,不啰嗦。 - 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。 - """) + + + /** 闲聊人设提示词(编译期常量,可直接用于 @SystemMessage) */ + String CHAT_SYSTEM_PROMPT = """ + 你叫“小羚羊”,是用户身边轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 + 【聊天风格】 + 1. 不生硬、不说教,避免书面化表达; + 2. 主动接梗,适当延伸话题,不要一问一答; + 3. 偶尔带点小幽默,保持轻松无压力的氛围。 + 【身份规则】 + 4. 只有当用户把你叫成别的名字或别的助手时,才俏皮地回一句“我不是…,我是小羚羊~”; + 用户没有称呼你的时候,绝对不要主动提这句话。 + 【输出要求】 + 5. 回答控制在 60 字以内,一到两句话,不啰嗦、不分点、不做总结; + 6. 回复会被转成语音播报,只输出纯文本:不要 Markdown、标题、列表、表情符号和特殊符号; + 7. 不要编造业务数据。用户问到订单、报价、客户、库存等业务问题时, + 提示他输入“重置”重新选择业务场景,不要自己瞎答。 + """; + + @SystemMessage(CHAT_SYSTEM_PROMPT) @UserMessage("用户说:{{userInput}}") String chat(@MemoryId String userId, @V("userInput") String userInput); - @SystemMessage(""" - 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 - 要求:1. 不生硬、不说教,避免书面化表达; - 2. 主动接梗,适当延伸话题,不一问一答; - 3. 偶尔带点小幽默,保持轻松无压力的氛围; - 4. 回答简洁,符合日常聊天的语气,不啰嗦。 - 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。 - """) + /** + * 流式闲聊。 + * 注意:返回 TokenStream 而不是 Flux —— Flux 返回值依赖 langchain4j-reactor 模块提供的 + * TokenStreamAdapter(SPI),本项目未引入该依赖,直接返回 Flux 会在调用时抛 + * “Can't find suitable TokenStreamAdapter”。调用方用 Flux.create 桥接即可。 + */ + @SystemMessage(CHAT_SYSTEM_PROMPT) @UserMessage("用户说:{{userInput}}") - Flux chatStream(@MemoryId String userId, @V("userInput") String userInput); -} + TokenStream chatStream(@MemoryId String userId, @V("userInput") String userInput); +} \ No newline at end of file diff --git a/src/main/java/com/xly/agent/SecMethodAiAgent.java b/src/main/java/com/xly/agent/SecMethodAiAgent.java new file mode 100644 index 0000000..6059609 --- /dev/null +++ b/src/main/java/com/xly/agent/SecMethodAiAgent.java @@ -0,0 +1,14 @@ +package com.xly.agent; + +import dev.langchain4j.service.*; + +public interface SecMethodAiAgent { + + @SystemMessage("{{sSystemPrompt}}") + @UserMessage("用户输入:{{userInput}}") + Result choiceMethod( + @MemoryId String userId, + @V("userInput") String userInput, + @V("sSystemPrompt") String sSystemPrompt + ); +} \ No newline at end of file diff --git a/src/main/java/com/xly/agent/SystemPromptGenerator.java b/src/main/java/com/xly/agent/SystemPromptGenerator.java index 963ff11..98d3d7c 100644 --- a/src/main/java/com/xly/agent/SystemPromptGenerator.java +++ b/src/main/java/com/xly/agent/SystemPromptGenerator.java @@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONWriter; import com.xly.entity.ParamRule; +import com.xly.entity.ToolMeta; import com.xly.entity.UserSceneSession; import java.util.List; @@ -110,4 +111,60 @@ public class SystemPromptGenerator { """.formatted(methodNo); } + + /** + * 方法选择阶段:从可选方法树中,让大模型选出唯一最匹配的一个 sMethodNo。 + * 与 generate()(参数提取)配套,是它的上游一步。 + * + * @param toolList 当前场景可选的方法列表(方法树扁平化后的叶子集合) + * @param sKnowledgeBase 行业知识库,可为空 + * @return 供 choiceMethod 使用的 sSystemPrompt + */ + public static String generateChoiceMethodPrompt(List toolList, String sKnowledgeBase) { + StringBuilder sb = new StringBuilder(); + + sb.append("你是一个方法路由分类器。你的职责是理解用户意图,从候选方法中选出唯一最匹配的一项。\n\n"); + + sb.append("【任务】\n"); + sb.append("阅读用户输入,与【可选方法列表】逐项比对语义,选出意图最贴合的一个方法,输出它的 sMethodNo。\n"); + sb.append("判断时以用户的核心操作意图为准,只依据用户明确表达的内容,不推测未提及的需求。\n\n"); + + sb.append("【输出要求】\n"); + sb.append("- 只输出一个 JSON 对象,形如:{\"sMethodNo\": \"方法编号\", \"confidence\": 0.0}\n"); + sb.append("- sMethodNo 取自【可选方法列表】中的编号;均不匹配时取 \"none\"。\n"); + sb.append("- confidence 为 0~1 的匹配置信度:意图明确取高值,表述模糊取低值。\n"); + sb.append("- 不要输出解释、思考过程或 JSON 以外的任何文字。\n\n"); + + if (ObjectUtil.isNotEmpty(sKnowledgeBase)) { + sb.append("【行业背景知识】\n"); + sb.append("以下知识用于帮助你理解用户输入中的行业术语,请结合它判断意图:\n"); + sb.append(sKnowledgeBase).append("\n\n"); + } + + sb.append("【可选方法列表】\n"); + for (ToolMeta tool : toolList) { + sb.append("- sMethodNo: ").append(tool.getSMethodNo()) + .append(" 名称: ").append(tool.getSMethodName()); + // 描述为空时降级用场景名,保证每个方法都有判别依据 + String desc = ObjectUtil.isNotEmpty(tool.getStoolDesc()) + ? tool.getStoolDesc() : tool.getSceneName(); + if (ObjectUtil.isNotEmpty(desc)) { + sb.append(" 适用场景: ").append(desc); + } + sb.append("\n"); + } + + sb.append("\n【示例】\n"); + int count = 0; + for (ToolMeta tool : toolList) { + if (count++ >= 3) break; + sb.append("- 用户输入涉及\"").append(tool.getSMethodName()) + .append("\" → {\"sMethodNo\": \"").append(tool.getSMethodNo()) + .append("\", \"confidence\": 0.9}\n"); + } + sb.append("- 用户输入\"你好\" → {\"sMethodNo\": \"none\", \"confidence\": 0}\n"); + + return sb.toString(); + } + } \ No newline at end of file diff --git a/src/main/java/com/xly/config/ModelConfig.java b/src/main/java/com/xly/config/ModelConfig.java index a541497..4ec9bdd 100644 --- a/src/main/java/com/xly/config/ModelConfig.java +++ b/src/main/java/com/xly/config/ModelConfig.java @@ -1,14 +1,17 @@ package com.xly.config; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.xly.agent.ChatiAgent; import com.xly.agent.DynamicTableNl2SqlAiAgent; import com.xly.agent.SceneSelectorAiAgent; +import com.xly.agent.SecMethodAiAgent; +import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.service.AiServices; -import dev.langchain4j.memory.chat.MessageWindowChatMemory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -17,11 +20,17 @@ import org.springframework.context.annotation.Primary; import java.time.Duration; +/** + * 模型与 Agent 统一装配(langchain4j 1.14.0 新 API:ChatModel / StreamingChatModel) + * 约定: + * - 路由/SQL/参数提取类任务 → 低温(确定性) + * - 闲聊类任务 → 高温(多样性) + */ @Configuration public class ModelConfig { @Value("${langchain4j.ollama.base-url}") - private String chatModelUrl; + private String baseUrl; @Value("${langchain4j.ollama.chat-model-name}") private String chatModelName; @@ -29,88 +38,106 @@ public class ModelConfig { @Value("${langchain4j.ollama.sql-model-name}") private String sqlModelName; - // ====================== 主对话模型 ====================== + // ======================================================== + // 一、私有 builder 工厂:消除重复配置 + // ======================================================== + + /** 非流式 Ollama 模型的公共构造 */ + private OllamaChatModel.OllamaChatModelBuilder chatBuilder(String modelName, + double temperature, + double topP, + long timeoutSeconds, + int maxRetries) { + return OllamaChatModel.builder() + .baseUrl(baseUrl) + .modelName(modelName) + .temperature(temperature) + .topP(topP) + .timeout(Duration.ofSeconds(timeoutSeconds)) + .maxRetries(maxRetries); + } + + /** 流式 Ollama 模型的公共构造 */ + private OllamaStreamingChatModel.OllamaStreamingChatModelBuilder streamingBuilder(String modelName, + double temperature, + double topP, + int numPredict, + long timeoutSeconds) { + return OllamaStreamingChatModel.builder() + .baseUrl(baseUrl) + .modelName(modelName) + .temperature(temperature) + .topP(topP) + .numPredict(numPredict) + .timeout(Duration.ofSeconds(timeoutSeconds)); + } + + // ======================================================== + // 二、模型 Bean + // ======================================================== + + /** 主对话模型:路由/场景/方法选择等确定性任务共用 */ @Bean @Primary public OllamaChatModel chatLanguageModel() { - return OllamaChatModel.builder() - .baseUrl(chatModelUrl) - .modelName(chatModelName) - .temperature(0.1) - .topP(0.95) - .timeout(Duration.ofSeconds(120)) - .maxRetries(2) + return chatBuilder(chatModelName, 0.1, 0.95, 120, 2) .logRequests(true) .logResponses(true) .build(); } - // ====================== 自由闲聊模型 ====================== + /** 自由闲聊(非流式) */ @Bean("chatiModel") public OllamaChatModel chatiModel() { - return OllamaChatModel.builder() - .baseUrl(chatModelUrl) - .modelName(chatModelName) - .temperature(0.7) - .topP(0.9) - .timeout(Duration.ofSeconds(60)) - .maxRetries(2) - .build(); + return chatBuilder(chatModelName, 0.7, 0.9, 60, 2).build(); + } + + /** 自由闲聊(流式) */ + @Bean("chatiStreamingModel") + public OllamaStreamingChatModel chatiStreamingModel() { + return streamingBuilder(chatModelName, 0.7, 0.9, 512, 60).build(); } - // ====================== SQL 专用模型 ====================== + /** SQL 专用(非流式,零温 + 大输出) */ @Bean("sqlChatModel") public OllamaChatModel sqlChatModel() { - return OllamaChatModel.builder() - .baseUrl(chatModelUrl) - .modelName(sqlModelName) - .temperature(0.0) - .topP(0.95) + return chatBuilder(sqlModelName, 0.0, 0.95, 120, 3) .numPredict(4096) - .timeout(Duration.ofSeconds(120)) - .maxRetries(3) .build(); } - // ====================== 流式对话模型 ====================== + /** 主流式对话 */ @Bean("streamingChatModel") @Primary public OllamaStreamingChatModel streamingChatModel() { - return OllamaStreamingChatModel.builder() - .baseUrl(chatModelUrl) - .modelName(chatModelName) - .temperature(0.3) - .topP(0.9) - .numPredict(1024) - .timeout(Duration.ofSeconds(60)) - .build(); + return streamingBuilder(chatModelName, 0.3, 0.9, 1024, 60).build(); } - // ====================== 流式 SQL 模型 ====================== + /** SQL 流式 */ @Bean("streamingSqlModel") public OllamaStreamingChatModel streamingSqlModel() { - return OllamaStreamingChatModel.builder() - .baseUrl(chatModelUrl) - .modelName(sqlModelName) - .temperature(0.2) - .topP(0.95) - .numPredict(2048) - .timeout(Duration.ofSeconds(120)) - .build(); + return streamingBuilder(sqlModelName, 0.2, 0.95, 2048, 120).build(); } - // ====================== JSON ====================== + // ======================================================== + // 三、JSON + // ======================================================== + @Bean @Primary public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper; } - // ====================== 动态 SQL Agent ====================== + // ======================================================== + // 四、Agent Bean + // ======================================================== + + /** 动态 SQL Agent */ @Bean public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent( @Qualifier("sqlChatModel") OllamaChatModel sqlModel) { @@ -120,13 +147,45 @@ public class ModelConfig { .build(); } - // ====================== 场景选择 Agent ====================== + /** + * 闲聊 Agent:会话隔离由 @MemoryId + OperableChatMemoryProvider 完成, + * 全局共用一个实例即可 + */ + @Bean + public ChatiAgent chatiAgent( + @Qualifier("chatiModel") OllamaChatModel chatiModel, + @Qualifier("chatiStreamingModel") OllamaStreamingChatModel chatiStreamingModel, + OperableChatMemoryProvider operableChatMemoryProvider) { + return AiServices.builder(ChatiAgent.class) + .chatModel(chatiModel) + .streamingChatModel(chatiStreamingModel) + .chatMemoryProvider(operableChatMemoryProvider) + .maxSequentialToolsInvocations(1) + .build(); + } + + /** 场景选择 Agent(一级路由) */ @Bean public SceneSelectorAiAgent sceneSelectorAiAgent( @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) { return AiServices.builder(SceneSelectorAiAgent.class) .chatModel(chatLanguageModel) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) + .maxSequentialToolsInvocations(1) + .build(); + } + + /** + * 方法选择 Agent(二级路由):从场景内方法树中路由出 sMethodNo。 + * 分类任务,用极短记忆窗口避免被上文惯性带偏。 + */ + @Bean + public SecMethodAiAgent secMethodAiAgent( + @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) { + return AiServices.builder(SecMethodAiAgent.class) + .chatModel(chatLanguageModel) + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(2)) + .maxSequentialToolsInvocations(1) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/xly/entity/MethodChoiceResult.java b/src/main/java/com/xly/entity/MethodChoiceResult.java new file mode 100644 index 0000000..8417953 --- /dev/null +++ b/src/main/java/com/xly/entity/MethodChoiceResult.java @@ -0,0 +1,9 @@ +package com.xly.entity; + +import lombok.Data; + +@Data +public class MethodChoiceResult { + private String sMethodNo; + private Double confidence; +} \ No newline at end of file diff --git a/src/main/java/com/xly/service/MethodRouterService.java b/src/main/java/com/xly/service/MethodRouterService.java new file mode 100644 index 0000000..3dc61dd --- /dev/null +++ b/src/main/java/com/xly/service/MethodRouterService.java @@ -0,0 +1,104 @@ +package com.xly.service; + +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson2.JSON; +import com.xly.agent.SecMethodAiAgent; +import com.xly.agent.SystemPromptGenerator; +import com.xly.entity.MethodChoiceResult; +import com.xly.entity.ToolMeta; +import com.xly.entity.UserSceneSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MethodRouterService { + + private final SecMethodAiAgent aiAgent; + + /** confidence 低于此值视为未命中,走澄清兜底 */ + private static final double CONFIDENCE_THRESHOLD = 0.5; + + /** + * 根据用户输入路由到具体方法,命中则写入 session.currentTool 并返回该方法。 + * + * @return 命中的 ToolMeta;未命中返回 null(由上层决定澄清或走通用 chat) + */ + public ToolMeta route(UserSceneSession session, String userInput, + List toolList, String sKnowledgeBase) { + + if (ObjectUtil.isEmpty(toolList)) { + log.warn("方法路由:可选方法列表为空, userId={}", session.getUserId()); + return null; + } + + // ① 生成方法选择 prompt + String sSystemPrompt = SystemPromptGenerator + .generateChoiceMethodPrompt(toolList, sKnowledgeBase); + + // ② 调大模型 + String raw = aiAgent.choiceMethod(session.getUserId(), userInput, sSystemPrompt).content(); + log.debug("方法路由原始输出: {}", raw); + + // ③ 解析(含 ```json 容错) + MethodChoiceResult result = parse(raw); + if (result == null || ObjectUtil.isEmpty(result.getSMethodNo())) { + return null; + } + + String methodNo = result.getSMethodNo(); + Double confidence = result.getConfidence(); + + // ④ none / 阈值 / 白名单三重校验 + if ("none".equalsIgnoreCase(methodNo)) { + log.info("方法路由:无匹配方法, input={}", userInput); + return null; + } + if (confidence == null || confidence < CONFIDENCE_THRESHOLD) { + log.info("方法路由:置信度不足({}), input={}", confidence, userInput); + return null; + } + Optional hit = toolList.stream() + .filter(t -> methodNo.equals(t.getSMethodNo())) + .findFirst(); + if (hit.isEmpty()) { + log.warn("方法路由:模型返回了不存在的编号 {}", methodNo); // 防编造 + return null; + } + + // ⑤ 命中,写入当前工具,交给下游参数提取 + ToolMeta chosen = hit.get(); + session.setCurrentTool(chosen); + log.info("方法路由命中: {} - {}, confidence={}", + chosen.getSMethodNo(), chosen.getSMethodName(), confidence); + return chosen; + } + + /** 解析模型输出,兼容被 ```json ... ``` 包裹或前后带杂字符的情况 */ + private MethodChoiceResult parse(String raw) { + if (ObjectUtil.isEmpty(raw)) return null; + try { + String json = stripCodeFence(raw); + return JSON.parseObject(json, MethodChoiceResult.class); + } catch (Exception e) { + log.error("方法路由结果解析失败, raw={}", raw, e); + return null; + } + } + + /** 去掉 markdown 代码块,并截取第一个 { 到最后一个 } */ + private String stripCodeFence(String raw) { + String s = raw.trim() + .replaceAll("(?s)```json", "") + .replaceAll("(?s)```", "") + .trim(); + int start = s.indexOf('{'); + int end = s.lastIndexOf('}'); + return (start >= 0 && end > start) ? s.substring(start, end + 1) : s; + } +} \ No newline at end of file diff --git a/src/main/java/com/xly/service/UserSceneSessionService.java b/src/main/java/com/xly/service/UserSceneSessionService.java index 68477f6..4e15f22 100644 --- a/src/main/java/com/xly/service/UserSceneSessionService.java +++ b/src/main/java/com/xly/service/UserSceneSessionService.java @@ -1,7 +1,6 @@ package com.xly.service; import cn.hutool.core.util.ObjectUtil; -import com.xly.agent.ChatiAgent; import com.xly.agent.DynamicTableNl2SqlAiAgent; import com.xly.agent.ErpAiAgent; import com.xly.config.OperableChatMemoryProvider; @@ -25,7 +24,7 @@ public class UserSceneSessionService { // 原有缓存:Agent实例、会话记忆 public static final Map ERP_AGENT_CACHE = new HashMap<>(); - public static final Map CHAT_AGENT_CACHE = new HashMap<>(); + // 闲聊Agent已改为全局单例Bean(会话隔离由 @MemoryId 完成),无需再按用户缓存 public static final Map ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>(); @@ -72,7 +71,6 @@ public class UserSceneSessionService { public void cleanAllSession(){ USER_SCENE_SESSION_CACHE.clear(); ERP_AGENT_CACHE.clear(); - CHAT_AGENT_CACHE.clear(); ERP_DynamicTableNl2SqlAiAgent_CACHE.clear(); } @@ -86,7 +84,6 @@ public class UserSceneSessionService { public void cleanUserSession(String sUserId){ USER_SCENE_SESSION_CACHE.remove(sUserId); ERP_AGENT_CACHE.remove(sUserId); - CHAT_AGENT_CACHE.remove(sUserId); ERP_DynamicTableNl2SqlAiAgent_CACHE.remove(sUserId); } diff --git a/src/main/java/com/xly/service/XlyErpService.java b/src/main/java/com/xly/service/XlyErpService.java index c4d89a1..409b6e4 100644 --- a/src/main/java/com/xly/service/XlyErpService.java +++ b/src/main/java/com/xly/service/XlyErpService.java @@ -24,10 +24,7 @@ import com.xly.thread.MultiThreadPoolServer; import com.xly.tool.DynamicToolProvider; import com.xly.tool.ToolSpecificationHolder; import com.xly.util.*; -import dev.langchain4j.agent.tool.ReturnBehavior; -import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.agent.tool.ToolSpecification; -import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.memory.ChatMemory; @@ -37,10 +34,8 @@ import dev.langchain4j.service.Result; import dev.langchain4j.service.tool.ToolExecutor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.time.DateFormatUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.util.IdGenerator; import reactor.core.publisher.Flux; import java.time.Duration; @@ -54,7 +49,7 @@ import java.util.stream.IntStream; public class XlyErpService { //中文对话模型 private final OllamaChatModel chatModel; - private final OllamaChatModel chatiModel; + private final ChatiAgent chatiAgent; private final SceneSelectorAiAgent sceneSelectorAiAgent; private final UserSceneSessionService userSceneSessionService; private final DynamicToolProvider dynamicToolProvider; @@ -63,6 +58,7 @@ public class XlyErpService { private final RedisService redisService; private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService; private final MilvusService milvusService; + private final SecMethodAiAgent secMethodAiAgent; //执行动态语句 执行异常的情况下 最多执行次数 private final Integer maxRetries = 5; @@ -342,12 +338,49 @@ public class XlyErpService { // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示) return handleSceneSelect(userId, input, session,1,0); } - // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续 - ErpAiAgent aiAgent = createErpAiAgent(userId, input, session); - // 没有选择到场景,进闲聊模式 - if (aiAgent == null){ - return getChatiAgent (input,session); + List metaAll = dynamicToolProvider.metaAll; + if(ObjectUtil.isNotEmpty(metaAll)){ + List metaOne = metaAll.stream().filter(m-> + userInput.equals(m.getSMethodName().trim()) || ("查询"+userInput).equals(m.getSMethodName().trim()) + ).collect(Collectors.toUnmodifiableList()); + if(ObjectUtil.isNotEmpty(metaOne)){ + session.setCurrentTool(metaOne.get(0)); + String sResponMessage = metaOne.get(0).getSTsMemo(); + methodName = metaOne.get(0).getSMethodName(); + return AiResponseDTO.builder().sSceneName(sceneName) + .sMethodName(methodName) + .aiText(sResponMessage) + .sCopyTo(session.getSCopyTo()) + .sCopyToSrcId(session.getSCopyToSrcId()) + .dbType(session.getDbType()) + .dbCach(session.getDbCach()) + .sReturnType(sReturnType) + .build(); + } } + + //获取方法 + ErpAiAgent aiAgent = null; + //不存在方法需要选择方法 + if(ObjectUtil.isEmpty(session.getCurrentTool())){ + MethodChoiceResult mr = methodChoiceResult(userId, userInput, session); + if(ObjectUtil.isNotEmpty(mr) && ObjectUtil.isNotEmpty(mr.getSMethodNo())){ + List metaOne = metaAll.stream().filter(m-> mr.getSMethodNo().equals(m.getSMethodNo().trim()) + ).collect(Collectors.toUnmodifiableList()); + session.setCurrentTool(metaOne.get(0)); + } + } + //还是不存在方法走老的方式兜底 + if(ObjectUtil.isEmpty(session.getCurrentTool())){ + // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续 + aiAgent = createErpAiAgent(userId, input, session); + // 没有选择到场景,进闲聊模式 + if (aiAgent == null){ + return getChatiAgent (input,session); + } + } + //如果输入的文字等于方法描述的文字,直接锁定方法 返回提示语 +// String sSceneId = session.getCurrentScene().getSId(); String sResponMessage = StrUtil.EMPTY; //用户输入添加方法(如果没有方法,动态SQL方法不需要) Boolean isConfirmed = dynamicToolProvider.isConfirmed(input,session); @@ -368,6 +401,7 @@ public class XlyErpService { //重新生成新的aiAgent拿新的aiAgent 做选择 aiAgent = createConfirmeAgent(session); } + //报价每次定义参数 if(!isConfirmed && ObjectUtil.isNotEmpty(session) && ObjectUtil.isNotEmpty(session.getCurrentTool()) && session.getCurrentTool().getBQuo()){ sSystemPrompt = SystemPromptGenerator.generate(session.getCurrentTool().getSMethodName(),session,session.getCurrentScene().getSKnowledgeBase()); @@ -486,7 +520,6 @@ public class XlyErpService { UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); // 清空Agent缓存 UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); cleanMemory(userId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization); } @@ -503,7 +536,6 @@ public class XlyErpService { session.setArgs(null); session.setSUserQuestionList(new ArrayList<>()); UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); session.setBCleanMemory(false); String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(StrUtil.EMPTY).aiText(StrUtil.EMPTY).systemText("清除记忆成功!").sReturnType(ReturnTypeCode.HTML.getCode()).build(); @@ -720,7 +752,6 @@ public class XlyErpService { session.setArgs(new HashMap<>()); // session.setSceneSelected(false); UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); session.setBCleanMemory(false); } @@ -944,16 +975,95 @@ public class XlyErpService { aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class) .chatModel(ol) .chatMemoryProvider(operableChatMemoryProvider) + .maxSequentialToolsInvocations(1) .toolProvider(dynamicToolProvider) .build(); UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent); } return aiAgent; } + /*** + * @Author 钱豹 + * @Date 23:07 2026/7/29 + * @Description AI识别获取方法(二级路由:在当前场景内选出一个 sMethodNo) + **/ + private MethodChoiceResult methodChoiceResult(String userId, String userInput, UserSceneSession session) { + + // 1. 取当前场景下的可选方法(方法树扁平集合) + List metaOne = dynamicToolProvider.metaAll.stream() + .filter(m -> session.getCurrentScene().getSId().equals(m.getSSceneId())) + .collect(Collectors.toList()); + + if (ObjectUtil.isEmpty(metaOne)) { + log.warn("方法路由:当前场景无可选方法, sceneId={}", session.getCurrentScene().getSId()); + return none(); + } + + // 2. 生成方法选择 prompt(行业知识库按需注入) + String sKnowledgeBase = session.getCurrentScene().getSKnowledgeBase(); // 无此字段就传 null + String sSystemPrompt = SystemPromptGenerator.generateChoiceMethodPrompt(metaOne, sKnowledgeBase); + + // 3. 调大模型 + String raw = secMethodAiAgent.choiceMethod(userId, userInput, sSystemPrompt).content(); + log.debug("方法路由原始输出: userId={}, raw={}", userId, raw); + + // 4. 解析(含 ```json 容错) + MethodChoiceResult result = parseChoice(raw); + if (result == null || ObjectUtil.isEmpty(result.getSMethodNo())) { + return none(); + } + + String methodNo = result.getSMethodNo(); + Double confidence = result.getConfidence(); + + // 5. none / 阈值 / 白名单 三重校验 + if ("none".equalsIgnoreCase(methodNo)) { + log.info("方法路由:无匹配方法, input={}", userInput); + return none(); + } + if (confidence == null || confidence < 0.5) { + log.info("方法路由:置信度不足({}), input={}", confidence, userInput); + return none(); + } + boolean exists = metaOne.stream().anyMatch(m -> methodNo.equals(m.getSMethodNo())); + if (!exists) { + log.warn("方法路由:模型返回了不存在的编号 {}", methodNo); // 防编造 + return none(); + } + + log.info("方法路由命中: sMethodNo={}, confidence={}", methodNo, confidence); + return result; + } + + /** 解析模型输出,兼容被 ```json ... ``` 包裹或前后带杂字符的情况 */ + private MethodChoiceResult parseChoice(String raw) { + if (ObjectUtil.isEmpty(raw)) return null; + try { + String s = raw.trim() + .replaceAll("(?s)```json", "") + .replaceAll("(?s)```", "") + .trim(); + int start = s.indexOf('{'); + int end = s.lastIndexOf('}'); + String json = (start >= 0 && end > start) ? s.substring(start, end + 1) : s; + return JSON.parseObject(json, MethodChoiceResult.class); + } catch (Exception e) { + log.error("方法路由结果解析失败, raw={}", raw, e); + return null; + } + } + + /** 未命中统一返回值 */ + private MethodChoiceResult none() { + MethodChoiceResult r = new MethodChoiceResult(); + r.setSMethodNo("none"); + r.setConfidence(0.0); + return r; + } + // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) { - // 1. 已选场景:强制绑定该场景工具 if (session.isSceneSelected() && session.getCurrentScene() != null) { dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); @@ -969,7 +1079,7 @@ public class XlyErpService { } // 4. 获取/创建用Agent ErpAiAgent aiAgent = UserSceneSessionService.ERP_AGENT_CACHE.get(userId); - List dataList = dynamicToolProvider.sceneToolCacheMap.get(session.getCurrentScene().getSId()); + List dataList = dynamicToolProvider.sceneToolCacheMapFist.get(session.getCurrentScene().getSId()); List dataListOne = dataList.stream().filter(m-> userInput.equals(m.getsMethodName().trim()) || ("查询"+userInput).equals(m.getsMethodName().trim()) @@ -992,9 +1102,10 @@ public class XlyErpService { .chatModel(chatModel) .chatMemoryProvider(operableChatMemoryProvider) .tools(executors,immediateReturnToolNames) + .maxSequentialToolsInvocations(1) .build(); UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent); -// log.info("用户{}Agent构建完成,已选场景:{},场景ID{}", userId, session.isSceneSelected() ? session.getCurrentScene().getSSceneName() : "未选(全场景匹配)", dynamicToolProvider.sSceneIdMap.get(userId)); + } return aiAgent; } @@ -1021,6 +1132,8 @@ public class XlyErpService { .chatModel(chatModel) .chatMemoryProvider(operableChatMemoryProvider) .tools(executors,immediateReturnToolNames) + // ★ 关键:防死循环兜底 + .maxSequentialToolsInvocations(1) .build(); return aiAgent; } @@ -1073,7 +1186,6 @@ public class XlyErpService { 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(); } @@ -1225,18 +1337,15 @@ public class XlyErpService { String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool()) ? session.getCurrentTool().getSMethodName() : "随便聊聊"; - // 从缓存获取或创建ChatiAgent - ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId()); - if (ObjectUtil.isEmpty(chatiAgent)) { - chatiAgent = AiServices.builder(ChatiAgent.class) - .chatModel(chatiModel) - .chatMemoryProvider(operableChatMemoryProvider) - .build(); - UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent); - } - - // 调用流式聊天方法 - return chatiAgent.chatStream(session.getUserId(), input) + // TokenStream -> Flux 桥接:onPartialResponse 推片段,完成/异常时结束流 + return Flux.create(sink -> chatiAgent.chatStream(session.getUserId(), input) + .onPartialResponse(sink::next) + .onCompleteResponse(response -> sink.complete()) + .onError(error -> { + log.error("闲聊流式返回异常, userId={}", session.getUserId(), error); + sink.complete(); + }) + .start()) .map(chunk -> AiResponseDTO.builder() .sSceneName(sceneName) .sMethodName(methodName) @@ -1256,13 +1365,6 @@ public class XlyErpService { private AiResponseDTO getChatiAgent (String input,UserSceneSession session){ String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():"随便聊聊"; - ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId()); - if(ObjectUtil.isEmpty(chatiAgent)){ - chatiAgent = AiServices.builder(ChatiAgent.class) - .chatModel(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(); } diff --git a/src/main/java/com/xly/tool/DynamicToolProvider.java b/src/main/java/com/xly/tool/DynamicToolProvider.java index 0285955..8e3c59b 100644 --- a/src/main/java/com/xly/tool/DynamicToolProvider.java +++ b/src/main/java/com/xly/tool/DynamicToolProvider.java @@ -42,7 +42,6 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import java.util.stream.IntStream; @Slf4j @Service @@ -57,8 +56,10 @@ public class DynamicToolProvider implements ToolProvider { private final OperableChatMemoryProvider operableChatMemoryProvider; private final Map toolCache = new ConcurrentHashMap<>(); + public final List metaAll = new ArrayList<>(); public final Map sSceneIdMap = new ConcurrentHashMap<>(); public final Map> sceneToolCacheMap = new ConcurrentHashMap<>(); + public final Map> sceneToolCacheMapFist = new ConcurrentHashMap<>(); private final List paramRuleDataAll = new ArrayList<>(); @Value("${erp.baseurl}") @@ -73,20 +74,31 @@ public class DynamicToolProvider implements ToolProvider { @jakarta.annotation.PostConstruct public void init() { List metas = toolMetaMapper.findAll(); + metaAll.addAll(metas); for (ToolMeta meta : metas) { try { doSetToolAIshowfieldShow(meta); ToolSpecification spec = buildToolSpecification(meta); ToolExecutor executor = createToolExecutor(meta); - toolCache.put(meta.getSMethodNo(), new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName())); + toolCache.put(meta.getSMethodNo(), new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc())); log.info("已加载动态工具:{}", meta.getSMethodNo()); String sceneId = meta.getSSceneId(); List dataList = new ArrayList<>(); if(ObjectUtil.isNotEmpty(sceneToolCacheMap.get(sceneId))) { dataList = sceneToolCacheMap.get(sceneId); } - dataList.add(new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName())); + dataList.add(new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc())); sceneToolCacheMap.put(sceneId, dataList); + + // + ToolSpecification specF = buildToolSpecificationF(meta); + List dataListF = new ArrayList<>(); + if(ObjectUtil.isNotEmpty(sceneToolCacheMapFist.get(sceneId))) { + dataListF = sceneToolCacheMapFist.get(sceneId); + } + dataListF.add(new ToolSpecificationHolder(specF, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc())); + sceneToolCacheMapFist.put(sceneId, dataListF); + } catch (Exception e) { e.printStackTrace(); log.error("构建工具失败,sMethodNo={}", meta.getSMethodNo(), e); @@ -153,136 +165,147 @@ public class DynamicToolProvider implements ToolProvider { log.info("创建工具执行器: {}", meta.getSMethodNo()); return (toolExecutionRequest, memoryId) -> { - log.info("===== 工具执行器开始执行 ====="); - log.info("工具编号: {}", meta.getSMethodNo()); - log.info("工具名称: {}", meta.getSMethodName()); - log.info("memoryId: {}", memoryId); - log.info("请求参数: {}", toolExecutionRequest.arguments()); - UserSceneSession session = UserSceneSessionService.USER_SCENE_SESSION_CACHE.get(memoryId.toString()); - session.setCurrentTool(meta); - - // ====================== 防重复调用:立即上锁 ====================== - session.setToolExecuted(true); - if (StrUtil.isNotBlank(session.getSFunPrompts())) { - // 关键:返回 工具执行失败 = 框架强制停止循环 + try{ + log.info("===== 工具执行器开始执行 ====="); + log.info("工具编号: {}", meta.getSMethodNo()); + log.info("工具名称: {}", meta.getSMethodName()); + log.info("memoryId: {}", memoryId); + log.info("请求参数: {}", toolExecutionRequest.arguments()); + UserSceneSession session = UserSceneSessionService.USER_SCENE_SESSION_CACHE.get(memoryId.toString()); + session.setCurrentTool(meta); + //如果不存在放入数据 + if (session.getArgs() == null) { + session.setArgs(new HashMap<>()); + } + // ====================== 防重复调用:立即上锁 ====================== + session.setToolExecuted(true); + if (StrUtil.isNotBlank(session.getSFunPrompts())) { + // 关键:返回 工具执行失败 = 框架强制停止循环 // throw new IllegalStateException("STOP_INVOCATION: 任务已完成,停止调用"); - return "【任务已完成】请勿重复调用工具,请直接总结结果回复用户:"+session.getSFunPrompts(); - } - //解析参数失败 - Map argsNew; - try { - argsNew = objectMapper.readValue(toolExecutionRequest.arguments(), new TypeReference<>() {}); - log.info("解析后的参数: {}", argsNew); - } catch (Exception e) { - log.error("参数解析失败", e); - // 抛异常 - throw new RuntimeException("参数解析失败,请重新输入"); - } + return "【任务已完成】请勿重复调用工具,请直接总结结果回复用户:"+session.getSFunPrompts(); + } + //解析参数失败 + Map argsNew; + try { + argsNew = objectMapper.readValue(toolExecutionRequest.arguments(), new TypeReference<>() {}); + log.info("解析后的参数: {}", argsNew); + } catch (Exception e) { + log.error("参数解析失败", e); + // 抛异常 + throw new RuntimeException("参数解析失败,请重新输入"); + } - //合并动态部件 - String sDynamicValue = ObjectUtil.isNotEmpty(argsNew.get("动态部件")) - ? argsNew.get("动态部件").toString():( ObjectUtil.isNotEmpty(argsNew.get("sDynamicPart"))? argsNew.get("sDynamicPart").toString():StrUtil.EMPTY); - String sDynamicValueOld =StrUtil.EMPTY; - if(sDynamicValue.contains("{")){ - sDynamicValue = StrUtil.EMPTY; - } - if(ObjectUtil.isNotEmpty(session.getArgs())){ + //合并动态部件 + String sDynamicValue = ObjectUtil.isNotEmpty(argsNew.get("动态部件")) + ? argsNew.get("动态部件").toString():( ObjectUtil.isNotEmpty(argsNew.get("sDynamicPart"))? argsNew.get("sDynamicPart").toString():StrUtil.EMPTY); + String sDynamicValueOld =StrUtil.EMPTY; + if(sDynamicValue.contains("{")){ + sDynamicValue = StrUtil.EMPTY; + } + //获取参数 如果不存在放入空 Map argOld = session.getArgs(); sDynamicValueOld = ObjectUtil.isNotEmpty(argOld.get("动态部件")) ? argOld.get("动态部件").toString():( ObjectUtil.isNotEmpty(argOld.get("sDynamicPart"))? argOld.get("sDynamicPart").toString():StrUtil.EMPTY); - } - Set sDynamicData = new HashSet<>(); - if(ObjectUtil.isNotEmpty(sDynamicValueOld)){ - List data = new ArrayList<>(); - if (JSONUtil.isTypeJSONArray(sDynamicValueOld)) { - data = JSONUtil.toList(sDynamicValueOld, String.class); - } else { - data.add(sDynamicValueOld); + + Set sDynamicData = new HashSet<>(); + if(ObjectUtil.isNotEmpty(sDynamicValueOld)){ + List data = new ArrayList<>(); + if (JSONUtil.isTypeJSONArray(sDynamicValueOld)) { + data = JSONUtil.toList(sDynamicValueOld, String.class); + } else { + data.add(sDynamicValueOld); + } + sDynamicData.addAll(data); } - sDynamicData.addAll(data); - } - if(ObjectUtil.isNotEmpty(sDynamicValue)){ - List data = new ArrayList<>(); - if (JSONUtil.isTypeJSONArray(sDynamicValue)) { - data = JSONUtil.toList(sDynamicValue, String.class); - } else { - data.add(sDynamicValue); + if(ObjectUtil.isNotEmpty(sDynamicValue)){ + List data = new ArrayList<>(); + if (JSONUtil.isTypeJSONArray(sDynamicValue)) { + data = JSONUtil.toList(sDynamicValue, String.class); + } else { + data.add(sDynamicValue); + } + sDynamicData.addAll(data); } - sDynamicData.addAll(data); - } - if(ObjectUtil.isNotEmpty(sDynamicData)){ - session.getArgs().remove("动态部件"); - session.getArgs().remove("sDynamicPart"); - argsNew.put("动态部件",JSONUtil.toJsonStr(sDynamicData)); - argsNew.put("sDynamicPart",JSONUtil.toJsonStr(sDynamicData)); - } - //获取之前获取的参数 - Map args = session.getArgs(); - if(ObjectUtil.isEmpty(args)) args = new HashMap<>(); - Map finalArgs = args; - argsNew.forEach((k, v)->{ - if(Validator.isValid(v)){ - finalArgs.put(k,v); + if(ObjectUtil.isNotEmpty(sDynamicData)){ + session.getArgs().remove("动态部件"); + session.getArgs().remove("sDynamicPart"); + argsNew.put("动态部件",JSONUtil.toJsonStr(sDynamicData)); + argsNew.put("sDynamicPart",JSONUtil.toJsonStr(sDynamicData)); } - }); - if(argsNew.containsKey("删除动态部件")){ - argsNew.put("sDynamicPartDel",argsNew.get("删除动态部件")); - } - if(!argsNew.containsKey("sDynamicPartDel")){ - finalArgs.remove("sDynamicPartDel"); - finalArgs.remove("删除动态部件"); - } - // 2 【补全动态参数】动态参数补全(中->英文) - try{ - args = applyValues(args, meta.getParamRuleListCheck()); - }catch (Exception e){ - log.error("返回信息",e); - String askMsg = e.getMessage(); - session.setSFunPrompts(askMsg); - // 需要提问用户 → 抛异常停止循环 - throw new RuntimeException(askMsg); - } - // 2.1 【自动补全】应用参数的默认值 - List paramRuleData = meta.getParamRuleListAll(); - //添加默认值 - args = applyDefaultValues(args, paramRuleData); - //客户选择行号转sSLaveId - if(ObjectUtil.isNotEmpty(args) - &&( - (args.containsKey("rowNumbers") && ObjectUtil.isNotEmpty(args.get("rowNumbers"))) - || (args.containsKey("operateType") && "全部确认".equals(args.get("operateType"))) - )&& ObjectUtil.isNotEmpty(session.getCurrentRowData()) - ){ - doSetSlaveIdToArgs( args, session); - } - //存入已采集参数 - session.setArgs(args); - // 2.1.1 根据当前获取的新参数判断是否存在有值必填例如动态参数、动态部件 - if(meta.getBQuo()){ - List dynamicParamRule = doSetParamgetArgsAfter(args, session); - if(ObjectUtil.isNotEmpty(dynamicParamRule)){ + //获取之前获取的参数 + Map args = session.getArgs(); + if(ObjectUtil.isEmpty(args)) args = new HashMap<>(); + Map finalArgs = args; + argsNew.forEach((k, v)->{ + if(Validator.isValid(v)){ + finalArgs.put(k,v); + } + }); + if(argsNew.containsKey("删除动态部件")){ + argsNew.put("sDynamicPartDel",argsNew.get("删除动态部件")); + } + if(!argsNew.containsKey("sDynamicPartDel")){ + finalArgs.remove("sDynamicPartDel"); + finalArgs.remove("删除动态部件"); + } + // 2 【补全动态参数】动态参数补全(中->英文) + try{ + args = applyValues(args, meta.getParamRuleListCheck()); + }catch (Exception e){ + log.error("返回信息",e); + String askMsg = e.getMessage(); + session.setSFunPrompts(askMsg); + // 需要提问用户 → 抛异常停止循环 + throw new RuntimeException(askMsg); + } + // 2.1 【自动补全】应用参数的默认值 + List paramRuleData = meta.getParamRuleListAll(); + //添加默认值 + args = applyDefaultValues(args, paramRuleData); + //客户选择行号转sSLaveId + if(ObjectUtil.isNotEmpty(args) + &&( + (args.containsKey("rowNumbers") && ObjectUtil.isNotEmpty(args.get("rowNumbers"))) + || (args.containsKey("operateType") && "全部确认".equals(args.get("operateType"))) + )&& ObjectUtil.isNotEmpty(session.getCurrentRowData()) + ){ + doSetSlaveIdToArgs( args, session); + } + if(ObjectUtil.isEmpty(args)){ + args = new HashMap<>(); + } + //存入已采集参数 + session.setArgs(args); + // 2.1.1 根据当前获取的新参数判断是否存在有值必填例如动态参数、动态部件 + if(meta.getBQuo()){ + List dynamicParamRule = doSetParamgetArgsAfter(args, session); + if(ObjectUtil.isNotEmpty(dynamicParamRule)){ // Map data = quoService.getQuoData( args, session); - session.getCurrentTool().setDynamicParamRule(dynamicParamRule); + session.getCurrentTool().setDynamicParamRule(dynamicParamRule); // String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,dynamicParamRule); // session.setSSystemPrompt(sSystemPrompt); + } } + //获取是否缺失参数 + String askMsg = getMissParamMsg( meta, session, args); + if(ObjectUtil.isNotEmpty(askMsg)){ + return askMsg; + } + String resp = StrUtil.EMPTY; + if(session.getCurrentTool().getBQuo()){ + resp = JSONUtil.toJsonStr(session.getQuoData()); + session.setSFunPrompts(resp); + session.getCurrentTool().setDynamicParamRule(null); + return resp; + }else{ + resp = JSONUtil.toJsonStr(args) ; + } + // ====================== 返回时带终止指令 ====================== + return String.valueOf(successResult(toolExecutionRequest, resp)); + }catch (Exception e){ + return String.valueOf(successResult(toolExecutionRequest,"执行方法异常"+e.getMessage())); } - //获取是否缺失参数 - String askMsg = getMissParamMsg( meta, session, args); - if(ObjectUtil.isNotEmpty(askMsg)){ - return askMsg; - } - String resp = StrUtil.EMPTY; - if(session.getCurrentTool().getBQuo()){ - resp = JSONUtil.toJsonStr(session.getQuoData()); - session.setSFunPrompts(resp); - session.getCurrentTool().setDynamicParamRule(null); - return resp; - }else{ - resp = JSONUtil.toJsonStr(args) ; - } - // ====================== 返回时带终止指令 ====================== - return String.valueOf(successResult(toolExecutionRequest, resp)); + }; } @@ -754,6 +777,41 @@ public class DynamicToolProvider implements ToolProvider { .build(); } + public ToolSpecification buildToolSpecificationF(ToolMeta meta) { + ToolSpecification.Builder builder = ToolSpecification.builder() + .name(meta.getSMethodNo()); + + StringBuffer stoolDesc = new StringBuffer(); + String forceToolPrompt = """ + 【工具调用规则】 + 1. 每个自定义方法最多只能调用 1 次 + 2. 只要方法返回结果,必须停止调用 + 3. 当用户输入包含【数据确认】、确认数据、确认、第*条数据确认时,必须调用本工具 + """; + stoolDesc.append(forceToolPrompt); + if (ObjectUtil.isNotEmpty(meta.getStoolDesc())) { + stoolDesc.append("MethodNo:").append(meta.getSMethodNo()) + .append(",当用户").append(meta.getSMethodName()) + .append("时,必须调用本工具").append(meta.getSMethodNo()) + .append(",").append(meta.getStoolDesc()); + } + JsonObjectSchema.Builder schemaBuilder = JsonObjectSchema.builder(); + List requiredParams = new ArrayList<>(); + List requiredParamsNew = new ArrayList<>(); + if(requiredParams!=null && requiredParams.size()>0){ + requiredParamsNew.add(requiredParams.get(0)); + } + if (!requiredParams.isEmpty()) { + schemaBuilder.required(requiredParamsNew); + } + JsonObjectSchema parameters = schemaBuilder.build(); + return builder + .description(stoolDesc.toString()) + .parameters(parameters) + .build(); + } + + private String getConstMeg(ParamRule paramRule){ if(!RuleCode.CONST.getCode().equals(paramRule.getSRule())){ return StrUtil.EMPTY; @@ -832,7 +890,7 @@ public class DynamicToolProvider implements ToolProvider { private String getMissParamMsg(ToolMeta meta,UserSceneSession session, Map args){ if(meta.getBQuo()){ Map data = quoService.getQuoData(args, session); - List> sAll = (List>) data.get("sAll"); + List> sAll = (ObjectUtil.isEmpty(data.get("sAll")))?new ArrayList<> ():(List>) data.get("sAll"); List> sEmptyData = sAll.stream().filter(one-> BooleanUtil.toBoolean(one.get("bEmpty").toString()) && (!Validator.isValid(one.get("sValue"))) && !"sDynamicPart".equals(one.get("sName")) @@ -842,9 +900,10 @@ public class DynamicToolProvider implements ToolProvider { if(ObjectUtil.isNotEmpty(sEmptyData)){ List missData = session.getCurrentTool().getParamRuleListAll().stream().filter(d -> d.getBEmpty() + && (ObjectUtil.isNotEmpty(args)) && (!Validator.isValid(args.get(d.getSParam()))) - && !"sDynamicPart".equals(d.getSParamValue()) - && !"sDynamicPartDel".equals(d.getSParamValue()) + && !"sDynamicPart".equals(d.getSParamValue()) + && !"sDynamicPartDel".equals(d.getSParamValue()) ).collect(Collectors.toUnmodifiableList()); data.put("bTs",true); String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,missData); diff --git a/src/main/java/com/xly/tool/ToolSpecificationHolder.java b/src/main/java/com/xly/tool/ToolSpecificationHolder.java index c1a439c..005198c 100644 --- a/src/main/java/com/xly/tool/ToolSpecificationHolder.java +++ b/src/main/java/com/xly/tool/ToolSpecificationHolder.java @@ -9,11 +9,13 @@ public class ToolSpecificationHolder { private final ToolExecutor toolExecutor; private final String sName; private final String sMethodName; - public ToolSpecificationHolder(ToolSpecification toolSpecification, ToolExecutor toolExecutor,String sName,String sMethodName) { + private final String stoolDesc; + public ToolSpecificationHolder(ToolSpecification toolSpecification, ToolExecutor toolExecutor,String sName,String sMethodName,String stoolDesc) { this.toolSpecification = toolSpecification; this.toolExecutor = toolExecutor; this.sName = sName; this.sMethodName = sMethodName; + this.stoolDesc = stoolDesc; } public ToolSpecification getToolSpecification() { @@ -31,5 +33,9 @@ public class ToolSpecificationHolder { public String getsMethodName() { return sMethodName; } + + public String getStoolDesc() { + return stoolDesc; + } } -- libgit2 0.22.2