Commit 1599dfd45f2f5b31a14d4849a23d5878239cf570
1 parent
37c6147d
添加未清选择 改成动态引导语
Showing
10 changed files
with
655 additions
and
233 deletions
src/main/java/com/xly/agent/ChatiAgent.java
| ... | ... | @@ -2,30 +2,45 @@ package com.xly.agent; |
| 2 | 2 | |
| 3 | 3 | import dev.langchain4j.service.MemoryId; |
| 4 | 4 | import dev.langchain4j.service.SystemMessage; |
| 5 | +import dev.langchain4j.service.TokenStream; | |
| 5 | 6 | import dev.langchain4j.service.UserMessage; |
| 6 | 7 | import dev.langchain4j.service.V; |
| 7 | -import reactor.core.publisher.Flux; | |
| 8 | 8 | |
| 9 | +/** | |
| 10 | + * 闲聊智能体(小羚羊人设) | |
| 11 | + * 说明:同步/流式两个方法共用同一份人设提示词,改一处即可,避免两份拷贝走偏 | |
| 12 | + */ | |
| 9 | 13 | public interface ChatiAgent { |
| 10 | - @SystemMessage(""" | |
| 11 | - 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 | |
| 12 | - 要求:1. 不生硬、不说教,避免书面化表达; | |
| 13 | - 2. 主动接梗,适当延伸话题,不一问一答; | |
| 14 | - 3. 偶尔带点小幽默,保持轻松无压力的氛围; | |
| 15 | - 4. 回答简洁,符合日常聊天的语气,不啰嗦。 | |
| 16 | - 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。 | |
| 17 | - """) | |
| 14 | + | |
| 15 | + | |
| 16 | + /** 闲聊人设提示词(编译期常量,可直接用于 @SystemMessage) */ | |
| 17 | + String CHAT_SYSTEM_PROMPT = """ | |
| 18 | + 你叫“小羚羊”,是用户身边轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 | |
| 19 | + 【聊天风格】 | |
| 20 | + 1. 不生硬、不说教,避免书面化表达; | |
| 21 | + 2. 主动接梗,适当延伸话题,不要一问一答; | |
| 22 | + 3. 偶尔带点小幽默,保持轻松无压力的氛围。 | |
| 23 | + 【身份规则】 | |
| 24 | + 4. 只有当用户把你叫成别的名字或别的助手时,才俏皮地回一句“我不是…,我是小羚羊~”; | |
| 25 | + 用户没有称呼你的时候,绝对不要主动提这句话。 | |
| 26 | + 【输出要求】 | |
| 27 | + 5. 回答控制在 60 字以内,一到两句话,不啰嗦、不分点、不做总结; | |
| 28 | + 6. 回复会被转成语音播报,只输出纯文本:不要 Markdown、标题、列表、表情符号和特殊符号; | |
| 29 | + 7. 不要编造业务数据。用户问到订单、报价、客户、库存等业务问题时, | |
| 30 | + 提示他输入“重置”重新选择业务场景,不要自己瞎答。 | |
| 31 | + """; | |
| 32 | + | |
| 33 | + @SystemMessage(CHAT_SYSTEM_PROMPT) | |
| 18 | 34 | @UserMessage("用户说:{{userInput}}") |
| 19 | 35 | String chat(@MemoryId String userId, @V("userInput") String userInput); |
| 20 | 36 | |
| 21 | - @SystemMessage(""" | |
| 22 | - 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 | |
| 23 | - 要求:1. 不生硬、不说教,避免书面化表达; | |
| 24 | - 2. 主动接梗,适当延伸话题,不一问一答; | |
| 25 | - 3. 偶尔带点小幽默,保持轻松无压力的氛围; | |
| 26 | - 4. 回答简洁,符合日常聊天的语气,不啰嗦。 | |
| 27 | - 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。 | |
| 28 | - """) | |
| 37 | + /** | |
| 38 | + * 流式闲聊。 | |
| 39 | + * 注意:返回 TokenStream 而不是 Flux —— Flux 返回值依赖 langchain4j-reactor 模块提供的 | |
| 40 | + * TokenStreamAdapter(SPI),本项目未引入该依赖,直接返回 Flux 会在调用时抛 | |
| 41 | + * “Can't find suitable TokenStreamAdapter”。调用方用 Flux.create 桥接即可。 | |
| 42 | + */ | |
| 43 | + @SystemMessage(CHAT_SYSTEM_PROMPT) | |
| 29 | 44 | @UserMessage("用户说:{{userInput}}") |
| 30 | - Flux<String> chatStream(@MemoryId String userId, @V("userInput") String userInput); | |
| 31 | -} | |
| 45 | + TokenStream chatStream(@MemoryId String userId, @V("userInput") String userInput); | |
| 46 | +} | |
| 32 | 47 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/agent/SecMethodAiAgent.java
0 → 100644
| 1 | +package com.xly.agent; | |
| 2 | + | |
| 3 | +import dev.langchain4j.service.*; | |
| 4 | + | |
| 5 | +public interface SecMethodAiAgent { | |
| 6 | + | |
| 7 | + @SystemMessage("{{sSystemPrompt}}") | |
| 8 | + @UserMessage("用户输入:{{userInput}}") | |
| 9 | + Result<String> choiceMethod( | |
| 10 | + @MemoryId String userId, | |
| 11 | + @V("userInput") String userInput, | |
| 12 | + @V("sSystemPrompt") String sSystemPrompt | |
| 13 | + ); | |
| 14 | +} | |
| 0 | 15 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/agent/SystemPromptGenerator.java
| ... | ... | @@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSON; |
| 5 | 5 | import com.alibaba.fastjson2.JSONObject; |
| 6 | 6 | import com.alibaba.fastjson2.JSONWriter; |
| 7 | 7 | import com.xly.entity.ParamRule; |
| 8 | +import com.xly.entity.ToolMeta; | |
| 8 | 9 | import com.xly.entity.UserSceneSession; |
| 9 | 10 | |
| 10 | 11 | import java.util.List; |
| ... | ... | @@ -110,4 +111,60 @@ public class SystemPromptGenerator { |
| 110 | 111 | """.formatted(methodNo); |
| 111 | 112 | } |
| 112 | 113 | |
| 114 | + | |
| 115 | + /** | |
| 116 | + * 方法选择阶段:从可选方法树中,让大模型选出唯一最匹配的一个 sMethodNo。 | |
| 117 | + * 与 generate()(参数提取)配套,是它的上游一步。 | |
| 118 | + * | |
| 119 | + * @param toolList 当前场景可选的方法列表(方法树扁平化后的叶子集合) | |
| 120 | + * @param sKnowledgeBase 行业知识库,可为空 | |
| 121 | + * @return 供 choiceMethod 使用的 sSystemPrompt | |
| 122 | + */ | |
| 123 | + public static String generateChoiceMethodPrompt(List<ToolMeta> toolList, String sKnowledgeBase) { | |
| 124 | + StringBuilder sb = new StringBuilder(); | |
| 125 | + | |
| 126 | + sb.append("你是一个方法路由分类器。你的职责是理解用户意图,从候选方法中选出唯一最匹配的一项。\n\n"); | |
| 127 | + | |
| 128 | + sb.append("【任务】\n"); | |
| 129 | + sb.append("阅读用户输入,与【可选方法列表】逐项比对语义,选出意图最贴合的一个方法,输出它的 sMethodNo。\n"); | |
| 130 | + sb.append("判断时以用户的核心操作意图为准,只依据用户明确表达的内容,不推测未提及的需求。\n\n"); | |
| 131 | + | |
| 132 | + sb.append("【输出要求】\n"); | |
| 133 | + sb.append("- 只输出一个 JSON 对象,形如:{\"sMethodNo\": \"方法编号\", \"confidence\": 0.0}\n"); | |
| 134 | + sb.append("- sMethodNo 取自【可选方法列表】中的编号;均不匹配时取 \"none\"。\n"); | |
| 135 | + sb.append("- confidence 为 0~1 的匹配置信度:意图明确取高值,表述模糊取低值。\n"); | |
| 136 | + sb.append("- 不要输出解释、思考过程或 JSON 以外的任何文字。\n\n"); | |
| 137 | + | |
| 138 | + if (ObjectUtil.isNotEmpty(sKnowledgeBase)) { | |
| 139 | + sb.append("【行业背景知识】\n"); | |
| 140 | + sb.append("以下知识用于帮助你理解用户输入中的行业术语,请结合它判断意图:\n"); | |
| 141 | + sb.append(sKnowledgeBase).append("\n\n"); | |
| 142 | + } | |
| 143 | + | |
| 144 | + sb.append("【可选方法列表】\n"); | |
| 145 | + for (ToolMeta tool : toolList) { | |
| 146 | + sb.append("- sMethodNo: ").append(tool.getSMethodNo()) | |
| 147 | + .append(" 名称: ").append(tool.getSMethodName()); | |
| 148 | + // 描述为空时降级用场景名,保证每个方法都有判别依据 | |
| 149 | + String desc = ObjectUtil.isNotEmpty(tool.getStoolDesc()) | |
| 150 | + ? tool.getStoolDesc() : tool.getSceneName(); | |
| 151 | + if (ObjectUtil.isNotEmpty(desc)) { | |
| 152 | + sb.append(" 适用场景: ").append(desc); | |
| 153 | + } | |
| 154 | + sb.append("\n"); | |
| 155 | + } | |
| 156 | + | |
| 157 | + sb.append("\n【示例】\n"); | |
| 158 | + int count = 0; | |
| 159 | + for (ToolMeta tool : toolList) { | |
| 160 | + if (count++ >= 3) break; | |
| 161 | + sb.append("- 用户输入涉及\"").append(tool.getSMethodName()) | |
| 162 | + .append("\" → {\"sMethodNo\": \"").append(tool.getSMethodNo()) | |
| 163 | + .append("\", \"confidence\": 0.9}\n"); | |
| 164 | + } | |
| 165 | + sb.append("- 用户输入\"你好\" → {\"sMethodNo\": \"none\", \"confidence\": 0}\n"); | |
| 166 | + | |
| 167 | + return sb.toString(); | |
| 168 | + } | |
| 169 | + | |
| 113 | 170 | } |
| 114 | 171 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/config/ModelConfig.java
| 1 | 1 | package com.xly.config; |
| 2 | 2 | |
| 3 | +import com.fasterxml.jackson.databind.DeserializationFeature; | |
| 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 4 | 5 | import com.fasterxml.jackson.databind.SerializationFeature; |
| 5 | 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
| 7 | +import com.xly.agent.ChatiAgent; | |
| 6 | 8 | import com.xly.agent.DynamicTableNl2SqlAiAgent; |
| 7 | 9 | import com.xly.agent.SceneSelectorAiAgent; |
| 10 | +import com.xly.agent.SecMethodAiAgent; | |
| 11 | +import dev.langchain4j.memory.chat.MessageWindowChatMemory; | |
| 8 | 12 | import dev.langchain4j.model.ollama.OllamaChatModel; |
| 9 | 13 | import dev.langchain4j.model.ollama.OllamaStreamingChatModel; |
| 10 | 14 | import dev.langchain4j.service.AiServices; |
| 11 | -import dev.langchain4j.memory.chat.MessageWindowChatMemory; | |
| 12 | 15 | import org.springframework.beans.factory.annotation.Qualifier; |
| 13 | 16 | import org.springframework.beans.factory.annotation.Value; |
| 14 | 17 | import org.springframework.context.annotation.Bean; |
| ... | ... | @@ -17,11 +20,17 @@ import org.springframework.context.annotation.Primary; |
| 17 | 20 | |
| 18 | 21 | import java.time.Duration; |
| 19 | 22 | |
| 23 | +/** | |
| 24 | + * 模型与 Agent 统一装配(langchain4j 1.14.0 新 API:ChatModel / StreamingChatModel) | |
| 25 | + * 约定: | |
| 26 | + * - 路由/SQL/参数提取类任务 → 低温(确定性) | |
| 27 | + * - 闲聊类任务 → 高温(多样性) | |
| 28 | + */ | |
| 20 | 29 | @Configuration |
| 21 | 30 | public class ModelConfig { |
| 22 | 31 | |
| 23 | 32 | @Value("${langchain4j.ollama.base-url}") |
| 24 | - private String chatModelUrl; | |
| 33 | + private String baseUrl; | |
| 25 | 34 | |
| 26 | 35 | @Value("${langchain4j.ollama.chat-model-name}") |
| 27 | 36 | private String chatModelName; |
| ... | ... | @@ -29,88 +38,106 @@ public class ModelConfig { |
| 29 | 38 | @Value("${langchain4j.ollama.sql-model-name}") |
| 30 | 39 | private String sqlModelName; |
| 31 | 40 | |
| 32 | - // ====================== 主对话模型 ====================== | |
| 41 | + // ======================================================== | |
| 42 | + // 一、私有 builder 工厂:消除重复配置 | |
| 43 | + // ======================================================== | |
| 44 | + | |
| 45 | + /** 非流式 Ollama 模型的公共构造 */ | |
| 46 | + private OllamaChatModel.OllamaChatModelBuilder chatBuilder(String modelName, | |
| 47 | + double temperature, | |
| 48 | + double topP, | |
| 49 | + long timeoutSeconds, | |
| 50 | + int maxRetries) { | |
| 51 | + return OllamaChatModel.builder() | |
| 52 | + .baseUrl(baseUrl) | |
| 53 | + .modelName(modelName) | |
| 54 | + .temperature(temperature) | |
| 55 | + .topP(topP) | |
| 56 | + .timeout(Duration.ofSeconds(timeoutSeconds)) | |
| 57 | + .maxRetries(maxRetries); | |
| 58 | + } | |
| 59 | + | |
| 60 | + /** 流式 Ollama 模型的公共构造 */ | |
| 61 | + private OllamaStreamingChatModel.OllamaStreamingChatModelBuilder streamingBuilder(String modelName, | |
| 62 | + double temperature, | |
| 63 | + double topP, | |
| 64 | + int numPredict, | |
| 65 | + long timeoutSeconds) { | |
| 66 | + return OllamaStreamingChatModel.builder() | |
| 67 | + .baseUrl(baseUrl) | |
| 68 | + .modelName(modelName) | |
| 69 | + .temperature(temperature) | |
| 70 | + .topP(topP) | |
| 71 | + .numPredict(numPredict) | |
| 72 | + .timeout(Duration.ofSeconds(timeoutSeconds)); | |
| 73 | + } | |
| 74 | + | |
| 75 | + // ======================================================== | |
| 76 | + // 二、模型 Bean | |
| 77 | + // ======================================================== | |
| 78 | + | |
| 79 | + /** 主对话模型:路由/场景/方法选择等确定性任务共用 */ | |
| 33 | 80 | @Bean |
| 34 | 81 | @Primary |
| 35 | 82 | public OllamaChatModel chatLanguageModel() { |
| 36 | - return OllamaChatModel.builder() | |
| 37 | - .baseUrl(chatModelUrl) | |
| 38 | - .modelName(chatModelName) | |
| 39 | - .temperature(0.1) | |
| 40 | - .topP(0.95) | |
| 41 | - .timeout(Duration.ofSeconds(120)) | |
| 42 | - .maxRetries(2) | |
| 83 | + return chatBuilder(chatModelName, 0.1, 0.95, 120, 2) | |
| 43 | 84 | .logRequests(true) |
| 44 | 85 | .logResponses(true) |
| 45 | 86 | .build(); |
| 46 | 87 | } |
| 47 | 88 | |
| 48 | - // ====================== 自由闲聊模型 ====================== | |
| 89 | + /** 自由闲聊(非流式) */ | |
| 49 | 90 | @Bean("chatiModel") |
| 50 | 91 | public OllamaChatModel chatiModel() { |
| 51 | - return OllamaChatModel.builder() | |
| 52 | - .baseUrl(chatModelUrl) | |
| 53 | - .modelName(chatModelName) | |
| 54 | - .temperature(0.7) | |
| 55 | - .topP(0.9) | |
| 56 | - .timeout(Duration.ofSeconds(60)) | |
| 57 | - .maxRetries(2) | |
| 58 | - .build(); | |
| 92 | + return chatBuilder(chatModelName, 0.7, 0.9, 60, 2).build(); | |
| 93 | + } | |
| 94 | + | |
| 95 | + /** 自由闲聊(流式) */ | |
| 96 | + @Bean("chatiStreamingModel") | |
| 97 | + public OllamaStreamingChatModel chatiStreamingModel() { | |
| 98 | + return streamingBuilder(chatModelName, 0.7, 0.9, 512, 60).build(); | |
| 59 | 99 | } |
| 60 | 100 | |
| 61 | - // ====================== SQL 专用模型 ====================== | |
| 101 | + /** SQL 专用(非流式,零温 + 大输出) */ | |
| 62 | 102 | @Bean("sqlChatModel") |
| 63 | 103 | public OllamaChatModel sqlChatModel() { |
| 64 | - return OllamaChatModel.builder() | |
| 65 | - .baseUrl(chatModelUrl) | |
| 66 | - .modelName(sqlModelName) | |
| 67 | - .temperature(0.0) | |
| 68 | - .topP(0.95) | |
| 104 | + return chatBuilder(sqlModelName, 0.0, 0.95, 120, 3) | |
| 69 | 105 | .numPredict(4096) |
| 70 | - .timeout(Duration.ofSeconds(120)) | |
| 71 | - .maxRetries(3) | |
| 72 | 106 | .build(); |
| 73 | 107 | } |
| 74 | 108 | |
| 75 | - // ====================== 流式对话模型 ====================== | |
| 109 | + /** 主流式对话 */ | |
| 76 | 110 | @Bean("streamingChatModel") |
| 77 | 111 | @Primary |
| 78 | 112 | public OllamaStreamingChatModel streamingChatModel() { |
| 79 | - return OllamaStreamingChatModel.builder() | |
| 80 | - .baseUrl(chatModelUrl) | |
| 81 | - .modelName(chatModelName) | |
| 82 | - .temperature(0.3) | |
| 83 | - .topP(0.9) | |
| 84 | - .numPredict(1024) | |
| 85 | - .timeout(Duration.ofSeconds(60)) | |
| 86 | - .build(); | |
| 113 | + return streamingBuilder(chatModelName, 0.3, 0.9, 1024, 60).build(); | |
| 87 | 114 | } |
| 88 | 115 | |
| 89 | - // ====================== 流式 SQL 模型 ====================== | |
| 116 | + /** SQL 流式 */ | |
| 90 | 117 | @Bean("streamingSqlModel") |
| 91 | 118 | public OllamaStreamingChatModel streamingSqlModel() { |
| 92 | - return OllamaStreamingChatModel.builder() | |
| 93 | - .baseUrl(chatModelUrl) | |
| 94 | - .modelName(sqlModelName) | |
| 95 | - .temperature(0.2) | |
| 96 | - .topP(0.95) | |
| 97 | - .numPredict(2048) | |
| 98 | - .timeout(Duration.ofSeconds(120)) | |
| 99 | - .build(); | |
| 119 | + return streamingBuilder(sqlModelName, 0.2, 0.95, 2048, 120).build(); | |
| 100 | 120 | } |
| 101 | 121 | |
| 102 | - // ====================== JSON ====================== | |
| 122 | + // ======================================================== | |
| 123 | + // 三、JSON | |
| 124 | + // ======================================================== | |
| 125 | + | |
| 103 | 126 | @Bean |
| 104 | 127 | @Primary |
| 105 | 128 | public ObjectMapper objectMapper() { |
| 106 | 129 | ObjectMapper mapper = new ObjectMapper(); |
| 107 | 130 | mapper.registerModule(new JavaTimeModule()); |
| 108 | 131 | mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); |
| 109 | - mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |
| 132 | + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |
| 110 | 133 | return mapper; |
| 111 | 134 | } |
| 112 | 135 | |
| 113 | - // ====================== 动态 SQL Agent ====================== | |
| 136 | + // ======================================================== | |
| 137 | + // 四、Agent Bean | |
| 138 | + // ======================================================== | |
| 139 | + | |
| 140 | + /** 动态 SQL Agent */ | |
| 114 | 141 | @Bean |
| 115 | 142 | public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent( |
| 116 | 143 | @Qualifier("sqlChatModel") OllamaChatModel sqlModel) { |
| ... | ... | @@ -120,13 +147,45 @@ public class ModelConfig { |
| 120 | 147 | .build(); |
| 121 | 148 | } |
| 122 | 149 | |
| 123 | - // ====================== 场景选择 Agent ====================== | |
| 150 | + /** | |
| 151 | + * 闲聊 Agent:会话隔离由 @MemoryId + OperableChatMemoryProvider 完成, | |
| 152 | + * 全局共用一个实例即可 | |
| 153 | + */ | |
| 154 | + @Bean | |
| 155 | + public ChatiAgent chatiAgent( | |
| 156 | + @Qualifier("chatiModel") OllamaChatModel chatiModel, | |
| 157 | + @Qualifier("chatiStreamingModel") OllamaStreamingChatModel chatiStreamingModel, | |
| 158 | + OperableChatMemoryProvider operableChatMemoryProvider) { | |
| 159 | + return AiServices.builder(ChatiAgent.class) | |
| 160 | + .chatModel(chatiModel) | |
| 161 | + .streamingChatModel(chatiStreamingModel) | |
| 162 | + .chatMemoryProvider(operableChatMemoryProvider) | |
| 163 | + .maxSequentialToolsInvocations(1) | |
| 164 | + .build(); | |
| 165 | + } | |
| 166 | + | |
| 167 | + /** 场景选择 Agent(一级路由) */ | |
| 124 | 168 | @Bean |
| 125 | 169 | public SceneSelectorAiAgent sceneSelectorAiAgent( |
| 126 | 170 | @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) { |
| 127 | 171 | return AiServices.builder(SceneSelectorAiAgent.class) |
| 128 | 172 | .chatModel(chatLanguageModel) |
| 129 | 173 | .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) |
| 174 | + .maxSequentialToolsInvocations(1) | |
| 175 | + .build(); | |
| 176 | + } | |
| 177 | + | |
| 178 | + /** | |
| 179 | + * 方法选择 Agent(二级路由):从场景内方法树中路由出 sMethodNo。 | |
| 180 | + * 分类任务,用极短记忆窗口避免被上文惯性带偏。 | |
| 181 | + */ | |
| 182 | + @Bean | |
| 183 | + public SecMethodAiAgent secMethodAiAgent( | |
| 184 | + @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) { | |
| 185 | + return AiServices.builder(SecMethodAiAgent.class) | |
| 186 | + .chatModel(chatLanguageModel) | |
| 187 | + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(2)) | |
| 188 | + .maxSequentialToolsInvocations(1) | |
| 130 | 189 | .build(); |
| 131 | 190 | } |
| 132 | 191 | } |
| 133 | 192 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/entity/MethodChoiceResult.java
0 → 100644
src/main/java/com/xly/service/MethodRouterService.java
0 → 100644
| 1 | +package com.xly.service; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ObjectUtil; | |
| 4 | +import com.alibaba.fastjson2.JSON; | |
| 5 | +import com.xly.agent.SecMethodAiAgent; | |
| 6 | +import com.xly.agent.SystemPromptGenerator; | |
| 7 | +import com.xly.entity.MethodChoiceResult; | |
| 8 | +import com.xly.entity.ToolMeta; | |
| 9 | +import com.xly.entity.UserSceneSession; | |
| 10 | +import lombok.RequiredArgsConstructor; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | +import org.springframework.stereotype.Service; | |
| 13 | + | |
| 14 | +import java.util.List; | |
| 15 | +import java.util.Optional; | |
| 16 | + | |
| 17 | +@Slf4j | |
| 18 | +@Service | |
| 19 | +@RequiredArgsConstructor | |
| 20 | +public class MethodRouterService { | |
| 21 | + | |
| 22 | + private final SecMethodAiAgent aiAgent; | |
| 23 | + | |
| 24 | + /** confidence 低于此值视为未命中,走澄清兜底 */ | |
| 25 | + private static final double CONFIDENCE_THRESHOLD = 0.5; | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 根据用户输入路由到具体方法,命中则写入 session.currentTool 并返回该方法。 | |
| 29 | + * | |
| 30 | + * @return 命中的 ToolMeta;未命中返回 null(由上层决定澄清或走通用 chat) | |
| 31 | + */ | |
| 32 | + public ToolMeta route(UserSceneSession session, String userInput, | |
| 33 | + List<ToolMeta> toolList, String sKnowledgeBase) { | |
| 34 | + | |
| 35 | + if (ObjectUtil.isEmpty(toolList)) { | |
| 36 | + log.warn("方法路由:可选方法列表为空, userId={}", session.getUserId()); | |
| 37 | + return null; | |
| 38 | + } | |
| 39 | + | |
| 40 | + // ① 生成方法选择 prompt | |
| 41 | + String sSystemPrompt = SystemPromptGenerator | |
| 42 | + .generateChoiceMethodPrompt(toolList, sKnowledgeBase); | |
| 43 | + | |
| 44 | + // ② 调大模型 | |
| 45 | + String raw = aiAgent.choiceMethod(session.getUserId(), userInput, sSystemPrompt).content(); | |
| 46 | + log.debug("方法路由原始输出: {}", raw); | |
| 47 | + | |
| 48 | + // ③ 解析(含 ```json 容错) | |
| 49 | + MethodChoiceResult result = parse(raw); | |
| 50 | + if (result == null || ObjectUtil.isEmpty(result.getSMethodNo())) { | |
| 51 | + return null; | |
| 52 | + } | |
| 53 | + | |
| 54 | + String methodNo = result.getSMethodNo(); | |
| 55 | + Double confidence = result.getConfidence(); | |
| 56 | + | |
| 57 | + // ④ none / 阈值 / 白名单三重校验 | |
| 58 | + if ("none".equalsIgnoreCase(methodNo)) { | |
| 59 | + log.info("方法路由:无匹配方法, input={}", userInput); | |
| 60 | + return null; | |
| 61 | + } | |
| 62 | + if (confidence == null || confidence < CONFIDENCE_THRESHOLD) { | |
| 63 | + log.info("方法路由:置信度不足({}), input={}", confidence, userInput); | |
| 64 | + return null; | |
| 65 | + } | |
| 66 | + Optional<ToolMeta> hit = toolList.stream() | |
| 67 | + .filter(t -> methodNo.equals(t.getSMethodNo())) | |
| 68 | + .findFirst(); | |
| 69 | + if (hit.isEmpty()) { | |
| 70 | + log.warn("方法路由:模型返回了不存在的编号 {}", methodNo); // 防编造 | |
| 71 | + return null; | |
| 72 | + } | |
| 73 | + | |
| 74 | + // ⑤ 命中,写入当前工具,交给下游参数提取 | |
| 75 | + ToolMeta chosen = hit.get(); | |
| 76 | + session.setCurrentTool(chosen); | |
| 77 | + log.info("方法路由命中: {} - {}, confidence={}", | |
| 78 | + chosen.getSMethodNo(), chosen.getSMethodName(), confidence); | |
| 79 | + return chosen; | |
| 80 | + } | |
| 81 | + | |
| 82 | + /** 解析模型输出,兼容被 ```json ... ``` 包裹或前后带杂字符的情况 */ | |
| 83 | + private MethodChoiceResult parse(String raw) { | |
| 84 | + if (ObjectUtil.isEmpty(raw)) return null; | |
| 85 | + try { | |
| 86 | + String json = stripCodeFence(raw); | |
| 87 | + return JSON.parseObject(json, MethodChoiceResult.class); | |
| 88 | + } catch (Exception e) { | |
| 89 | + log.error("方法路由结果解析失败, raw={}", raw, e); | |
| 90 | + return null; | |
| 91 | + } | |
| 92 | + } | |
| 93 | + | |
| 94 | + /** 去掉 markdown 代码块,并截取第一个 { 到最后一个 } */ | |
| 95 | + private String stripCodeFence(String raw) { | |
| 96 | + String s = raw.trim() | |
| 97 | + .replaceAll("(?s)```json", "") | |
| 98 | + .replaceAll("(?s)```", "") | |
| 99 | + .trim(); | |
| 100 | + int start = s.indexOf('{'); | |
| 101 | + int end = s.lastIndexOf('}'); | |
| 102 | + return (start >= 0 && end > start) ? s.substring(start, end + 1) : s; | |
| 103 | + } | |
| 104 | +} | |
| 0 | 105 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/service/UserSceneSessionService.java
| 1 | 1 | package com.xly.service; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.ObjectUtil; |
| 4 | -import com.xly.agent.ChatiAgent; | |
| 5 | 4 | import com.xly.agent.DynamicTableNl2SqlAiAgent; |
| 6 | 5 | import com.xly.agent.ErpAiAgent; |
| 7 | 6 | import com.xly.config.OperableChatMemoryProvider; |
| ... | ... | @@ -25,7 +24,7 @@ public class UserSceneSessionService { |
| 25 | 24 | |
| 26 | 25 | // 原有缓存:Agent实例、会话记忆 |
| 27 | 26 | public static final Map<String, ErpAiAgent> ERP_AGENT_CACHE = new HashMap<>(); |
| 28 | - public static final Map<String, ChatiAgent> CHAT_AGENT_CACHE = new HashMap<>(); | |
| 27 | + // 闲聊Agent已改为全局单例Bean(会话隔离由 @MemoryId 完成),无需再按用户缓存 | |
| 29 | 28 | public static final Map<String, DynamicTableNl2SqlAiAgent> ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>(); |
| 30 | 29 | |
| 31 | 30 | |
| ... | ... | @@ -72,7 +71,6 @@ public class UserSceneSessionService { |
| 72 | 71 | public void cleanAllSession(){ |
| 73 | 72 | USER_SCENE_SESSION_CACHE.clear(); |
| 74 | 73 | ERP_AGENT_CACHE.clear(); |
| 75 | - CHAT_AGENT_CACHE.clear(); | |
| 76 | 74 | ERP_DynamicTableNl2SqlAiAgent_CACHE.clear(); |
| 77 | 75 | } |
| 78 | 76 | |
| ... | ... | @@ -86,7 +84,6 @@ public class UserSceneSessionService { |
| 86 | 84 | public void cleanUserSession(String sUserId){ |
| 87 | 85 | USER_SCENE_SESSION_CACHE.remove(sUserId); |
| 88 | 86 | ERP_AGENT_CACHE.remove(sUserId); |
| 89 | - CHAT_AGENT_CACHE.remove(sUserId); | |
| 90 | 87 | ERP_DynamicTableNl2SqlAiAgent_CACHE.remove(sUserId); |
| 91 | 88 | } |
| 92 | 89 | ... | ... |
src/main/java/com/xly/service/XlyErpService.java
| ... | ... | @@ -24,10 +24,7 @@ import com.xly.thread.MultiThreadPoolServer; |
| 24 | 24 | import com.xly.tool.DynamicToolProvider; |
| 25 | 25 | import com.xly.tool.ToolSpecificationHolder; |
| 26 | 26 | import com.xly.util.*; |
| 27 | -import dev.langchain4j.agent.tool.ReturnBehavior; | |
| 28 | -import dev.langchain4j.agent.tool.ToolExecutionRequest; | |
| 29 | 27 | import dev.langchain4j.agent.tool.ToolSpecification; |
| 30 | -import dev.langchain4j.data.message.AiMessage; | |
| 31 | 28 | import dev.langchain4j.data.message.ChatMessage; |
| 32 | 29 | |
| 33 | 30 | import dev.langchain4j.memory.ChatMemory; |
| ... | ... | @@ -37,10 +34,8 @@ import dev.langchain4j.service.Result; |
| 37 | 34 | import dev.langchain4j.service.tool.ToolExecutor; |
| 38 | 35 | import lombok.RequiredArgsConstructor; |
| 39 | 36 | import lombok.extern.slf4j.Slf4j; |
| 40 | -import org.apache.commons.lang3.time.DateFormatUtils; | |
| 41 | 37 | import org.springframework.beans.factory.annotation.Value; |
| 42 | 38 | import org.springframework.stereotype.Service; |
| 43 | -import org.springframework.util.IdGenerator; | |
| 44 | 39 | import reactor.core.publisher.Flux; |
| 45 | 40 | |
| 46 | 41 | import java.time.Duration; |
| ... | ... | @@ -54,7 +49,7 @@ import java.util.stream.IntStream; |
| 54 | 49 | public class XlyErpService { |
| 55 | 50 | //中文对话模型 |
| 56 | 51 | private final OllamaChatModel chatModel; |
| 57 | - private final OllamaChatModel chatiModel; | |
| 52 | + private final ChatiAgent chatiAgent; | |
| 58 | 53 | private final SceneSelectorAiAgent sceneSelectorAiAgent; |
| 59 | 54 | private final UserSceneSessionService userSceneSessionService; |
| 60 | 55 | private final DynamicToolProvider dynamicToolProvider; |
| ... | ... | @@ -63,6 +58,7 @@ public class XlyErpService { |
| 63 | 58 | private final RedisService redisService; |
| 64 | 59 | private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService; |
| 65 | 60 | private final MilvusService milvusService; |
| 61 | + private final SecMethodAiAgent secMethodAiAgent; | |
| 66 | 62 | |
| 67 | 63 | //执行动态语句 执行异常的情况下 最多执行次数 |
| 68 | 64 | private final Integer maxRetries = 5; |
| ... | ... | @@ -342,12 +338,49 @@ public class XlyErpService { |
| 342 | 338 | // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示) |
| 343 | 339 | return handleSceneSelect(userId, input, session,1,0); |
| 344 | 340 | } |
| 345 | - // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续 | |
| 346 | - ErpAiAgent aiAgent = createErpAiAgent(userId, input, session); | |
| 347 | - // 没有选择到场景,进闲聊模式 | |
| 348 | - if (aiAgent == null){ | |
| 349 | - return getChatiAgent (input,session); | |
| 341 | + List<ToolMeta> metaAll = dynamicToolProvider.metaAll; | |
| 342 | + if(ObjectUtil.isNotEmpty(metaAll)){ | |
| 343 | + List<ToolMeta> metaOne = metaAll.stream().filter(m-> | |
| 344 | + userInput.equals(m.getSMethodName().trim()) || ("查询"+userInput).equals(m.getSMethodName().trim()) | |
| 345 | + ).collect(Collectors.toUnmodifiableList()); | |
| 346 | + if(ObjectUtil.isNotEmpty(metaOne)){ | |
| 347 | + session.setCurrentTool(metaOne.get(0)); | |
| 348 | + String sResponMessage = metaOne.get(0).getSTsMemo(); | |
| 349 | + methodName = metaOne.get(0).getSMethodName(); | |
| 350 | + return AiResponseDTO.builder().sSceneName(sceneName) | |
| 351 | + .sMethodName(methodName) | |
| 352 | + .aiText(sResponMessage) | |
| 353 | + .sCopyTo(session.getSCopyTo()) | |
| 354 | + .sCopyToSrcId(session.getSCopyToSrcId()) | |
| 355 | + .dbType(session.getDbType()) | |
| 356 | + .dbCach(session.getDbCach()) | |
| 357 | + .sReturnType(sReturnType) | |
| 358 | + .build(); | |
| 359 | + } | |
| 350 | 360 | } |
| 361 | + | |
| 362 | + //获取方法 | |
| 363 | + ErpAiAgent aiAgent = null; | |
| 364 | + //不存在方法需要选择方法 | |
| 365 | + if(ObjectUtil.isEmpty(session.getCurrentTool())){ | |
| 366 | + MethodChoiceResult mr = methodChoiceResult(userId, userInput, session); | |
| 367 | + if(ObjectUtil.isNotEmpty(mr) && ObjectUtil.isNotEmpty(mr.getSMethodNo())){ | |
| 368 | + List<ToolMeta> metaOne = metaAll.stream().filter(m-> mr.getSMethodNo().equals(m.getSMethodNo().trim()) | |
| 369 | + ).collect(Collectors.toUnmodifiableList()); | |
| 370 | + session.setCurrentTool(metaOne.get(0)); | |
| 371 | + } | |
| 372 | + } | |
| 373 | + //还是不存在方法走老的方式兜底 | |
| 374 | + if(ObjectUtil.isEmpty(session.getCurrentTool())){ | |
| 375 | + // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续 | |
| 376 | + aiAgent = createErpAiAgent(userId, input, session); | |
| 377 | + // 没有选择到场景,进闲聊模式 | |
| 378 | + if (aiAgent == null){ | |
| 379 | + return getChatiAgent (input,session); | |
| 380 | + } | |
| 381 | + } | |
| 382 | + //如果输入的文字等于方法描述的文字,直接锁定方法 返回提示语 | |
| 383 | +// String sSceneId = session.getCurrentScene().getSId(); | |
| 351 | 384 | String sResponMessage = StrUtil.EMPTY; |
| 352 | 385 | //用户输入添加方法(如果没有方法,动态SQL方法不需要) |
| 353 | 386 | Boolean isConfirmed = dynamicToolProvider.isConfirmed(input,session); |
| ... | ... | @@ -368,6 +401,7 @@ public class XlyErpService { |
| 368 | 401 | //重新生成新的aiAgent拿新的aiAgent 做选择 |
| 369 | 402 | aiAgent = createConfirmeAgent(session); |
| 370 | 403 | } |
| 404 | + | |
| 371 | 405 | //报价每次定义参数 |
| 372 | 406 | if(!isConfirmed && ObjectUtil.isNotEmpty(session) && ObjectUtil.isNotEmpty(session.getCurrentTool()) && session.getCurrentTool().getBQuo()){ |
| 373 | 407 | sSystemPrompt = SystemPromptGenerator.generate(session.getCurrentTool().getSMethodName(),session,session.getCurrentScene().getSKnowledgeBase()); |
| ... | ... | @@ -486,7 +520,6 @@ public class XlyErpService { |
| 486 | 520 | UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); |
| 487 | 521 | // 清空Agent缓存 |
| 488 | 522 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); |
| 489 | - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | |
| 490 | 523 | cleanMemory(userId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization); |
| 491 | 524 | } |
| 492 | 525 | |
| ... | ... | @@ -503,7 +536,6 @@ public class XlyErpService { |
| 503 | 536 | session.setArgs(null); |
| 504 | 537 | session.setSUserQuestionList(new ArrayList<>()); |
| 505 | 538 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); |
| 506 | - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | |
| 507 | 539 | session.setBCleanMemory(false); |
| 508 | 540 | String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; |
| 509 | 541 | return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(StrUtil.EMPTY).aiText(StrUtil.EMPTY).systemText("清除记忆成功!").sReturnType(ReturnTypeCode.HTML.getCode()).build(); |
| ... | ... | @@ -720,7 +752,6 @@ public class XlyErpService { |
| 720 | 752 | session.setArgs(new HashMap<>()); |
| 721 | 753 | // session.setSceneSelected(false); |
| 722 | 754 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); |
| 723 | - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | |
| 724 | 755 | session.setBCleanMemory(false); |
| 725 | 756 | } |
| 726 | 757 | |
| ... | ... | @@ -944,16 +975,95 @@ public class XlyErpService { |
| 944 | 975 | aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class) |
| 945 | 976 | .chatModel(ol) |
| 946 | 977 | .chatMemoryProvider(operableChatMemoryProvider) |
| 978 | + .maxSequentialToolsInvocations(1) | |
| 947 | 979 | .toolProvider(dynamicToolProvider) |
| 948 | 980 | .build(); |
| 949 | 981 | UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent); |
| 950 | 982 | } |
| 951 | 983 | return aiAgent; |
| 952 | 984 | } |
| 985 | + /*** | |
| 986 | + * @Author 钱豹 | |
| 987 | + * @Date 23:07 2026/7/29 | |
| 988 | + * @Description AI识别获取方法(二级路由:在当前场景内选出一个 sMethodNo) | |
| 989 | + **/ | |
| 990 | + private MethodChoiceResult methodChoiceResult(String userId, String userInput, UserSceneSession session) { | |
| 991 | + | |
| 992 | + // 1. 取当前场景下的可选方法(方法树扁平集合) | |
| 993 | + List<ToolMeta> metaOne = dynamicToolProvider.metaAll.stream() | |
| 994 | + .filter(m -> session.getCurrentScene().getSId().equals(m.getSSceneId())) | |
| 995 | + .collect(Collectors.toList()); | |
| 996 | + | |
| 997 | + if (ObjectUtil.isEmpty(metaOne)) { | |
| 998 | + log.warn("方法路由:当前场景无可选方法, sceneId={}", session.getCurrentScene().getSId()); | |
| 999 | + return none(); | |
| 1000 | + } | |
| 1001 | + | |
| 1002 | + // 2. 生成方法选择 prompt(行业知识库按需注入) | |
| 1003 | + String sKnowledgeBase = session.getCurrentScene().getSKnowledgeBase(); // 无此字段就传 null | |
| 1004 | + String sSystemPrompt = SystemPromptGenerator.generateChoiceMethodPrompt(metaOne, sKnowledgeBase); | |
| 1005 | + | |
| 1006 | + // 3. 调大模型 | |
| 1007 | + String raw = secMethodAiAgent.choiceMethod(userId, userInput, sSystemPrompt).content(); | |
| 1008 | + log.debug("方法路由原始输出: userId={}, raw={}", userId, raw); | |
| 1009 | + | |
| 1010 | + // 4. 解析(含 ```json 容错) | |
| 1011 | + MethodChoiceResult result = parseChoice(raw); | |
| 1012 | + if (result == null || ObjectUtil.isEmpty(result.getSMethodNo())) { | |
| 1013 | + return none(); | |
| 1014 | + } | |
| 1015 | + | |
| 1016 | + String methodNo = result.getSMethodNo(); | |
| 1017 | + Double confidence = result.getConfidence(); | |
| 1018 | + | |
| 1019 | + // 5. none / 阈值 / 白名单 三重校验 | |
| 1020 | + if ("none".equalsIgnoreCase(methodNo)) { | |
| 1021 | + log.info("方法路由:无匹配方法, input={}", userInput); | |
| 1022 | + return none(); | |
| 1023 | + } | |
| 1024 | + if (confidence == null || confidence < 0.5) { | |
| 1025 | + log.info("方法路由:置信度不足({}), input={}", confidence, userInput); | |
| 1026 | + return none(); | |
| 1027 | + } | |
| 1028 | + boolean exists = metaOne.stream().anyMatch(m -> methodNo.equals(m.getSMethodNo())); | |
| 1029 | + if (!exists) { | |
| 1030 | + log.warn("方法路由:模型返回了不存在的编号 {}", methodNo); // 防编造 | |
| 1031 | + return none(); | |
| 1032 | + } | |
| 1033 | + | |
| 1034 | + log.info("方法路由命中: sMethodNo={}, confidence={}", methodNo, confidence); | |
| 1035 | + return result; | |
| 1036 | + } | |
| 1037 | + | |
| 1038 | + /** 解析模型输出,兼容被 ```json ... ``` 包裹或前后带杂字符的情况 */ | |
| 1039 | + private MethodChoiceResult parseChoice(String raw) { | |
| 1040 | + if (ObjectUtil.isEmpty(raw)) return null; | |
| 1041 | + try { | |
| 1042 | + String s = raw.trim() | |
| 1043 | + .replaceAll("(?s)```json", "") | |
| 1044 | + .replaceAll("(?s)```", "") | |
| 1045 | + .trim(); | |
| 1046 | + int start = s.indexOf('{'); | |
| 1047 | + int end = s.lastIndexOf('}'); | |
| 1048 | + String json = (start >= 0 && end > start) ? s.substring(start, end + 1) : s; | |
| 1049 | + return JSON.parseObject(json, MethodChoiceResult.class); | |
| 1050 | + } catch (Exception e) { | |
| 1051 | + log.error("方法路由结果解析失败, raw={}", raw, e); | |
| 1052 | + return null; | |
| 1053 | + } | |
| 1054 | + } | |
| 1055 | + | |
| 1056 | + /** 未命中统一返回值 */ | |
| 1057 | + private MethodChoiceResult none() { | |
| 1058 | + MethodChoiceResult r = new MethodChoiceResult(); | |
| 1059 | + r.setSMethodNo("none"); | |
| 1060 | + r.setConfidence(0.0); | |
| 1061 | + return r; | |
| 1062 | + } | |
| 1063 | + | |
| 953 | 1064 | |
| 954 | 1065 | // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== |
| 955 | 1066 | private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) { |
| 956 | - | |
| 957 | 1067 | // 1. 已选场景:强制绑定该场景工具 |
| 958 | 1068 | if (session.isSceneSelected() && session.getCurrentScene() != null) { |
| 959 | 1069 | dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); |
| ... | ... | @@ -969,7 +1079,7 @@ public class XlyErpService { |
| 969 | 1079 | } |
| 970 | 1080 | // 4. 获取/创建用Agent |
| 971 | 1081 | ErpAiAgent aiAgent = UserSceneSessionService.ERP_AGENT_CACHE.get(userId); |
| 972 | - List<ToolSpecificationHolder> dataList = dynamicToolProvider.sceneToolCacheMap.get(session.getCurrentScene().getSId()); | |
| 1082 | + List<ToolSpecificationHolder> dataList = dynamicToolProvider.sceneToolCacheMapFist.get(session.getCurrentScene().getSId()); | |
| 973 | 1083 | List<ToolSpecificationHolder> dataListOne = |
| 974 | 1084 | dataList.stream().filter(m-> |
| 975 | 1085 | userInput.equals(m.getsMethodName().trim()) || ("查询"+userInput).equals(m.getsMethodName().trim()) |
| ... | ... | @@ -992,9 +1102,10 @@ public class XlyErpService { |
| 992 | 1102 | .chatModel(chatModel) |
| 993 | 1103 | .chatMemoryProvider(operableChatMemoryProvider) |
| 994 | 1104 | .tools(executors,immediateReturnToolNames) |
| 1105 | + .maxSequentialToolsInvocations(1) | |
| 995 | 1106 | .build(); |
| 996 | 1107 | UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent); |
| 997 | -// log.info("用户{}Agent构建完成,已选场景:{},场景ID{}", userId, session.isSceneSelected() ? session.getCurrentScene().getSSceneName() : "未选(全场景匹配)", dynamicToolProvider.sSceneIdMap.get(userId)); | |
| 1108 | + | |
| 998 | 1109 | } |
| 999 | 1110 | return aiAgent; |
| 1000 | 1111 | } |
| ... | ... | @@ -1021,6 +1132,8 @@ public class XlyErpService { |
| 1021 | 1132 | .chatModel(chatModel) |
| 1022 | 1133 | .chatMemoryProvider(operableChatMemoryProvider) |
| 1023 | 1134 | .tools(executors,immediateReturnToolNames) |
| 1135 | + // ★ 关键:防死循环兜底 | |
| 1136 | + .maxSequentialToolsInvocations(1) | |
| 1024 | 1137 | .build(); |
| 1025 | 1138 | return aiAgent; |
| 1026 | 1139 | } |
| ... | ... | @@ -1073,7 +1186,6 @@ public class XlyErpService { |
| 1073 | 1186 | UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); |
| 1074 | 1187 | // 清空Agent缓存 |
| 1075 | 1188 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); |
| 1076 | - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | |
| 1077 | 1189 | return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint(); |
| 1078 | 1190 | } |
| 1079 | 1191 | |
| ... | ... | @@ -1225,18 +1337,15 @@ public class XlyErpService { |
| 1225 | 1337 | String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool()) |
| 1226 | 1338 | ? session.getCurrentTool().getSMethodName() : "随便聊聊"; |
| 1227 | 1339 | |
| 1228 | - // 从缓存获取或创建ChatiAgent | |
| 1229 | - ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId()); | |
| 1230 | - if (ObjectUtil.isEmpty(chatiAgent)) { | |
| 1231 | - chatiAgent = AiServices.builder(ChatiAgent.class) | |
| 1232 | - .chatModel(chatiModel) | |
| 1233 | - .chatMemoryProvider(operableChatMemoryProvider) | |
| 1234 | - .build(); | |
| 1235 | - UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent); | |
| 1236 | - } | |
| 1237 | - | |
| 1238 | - // 调用流式聊天方法 | |
| 1239 | - return chatiAgent.chatStream(session.getUserId(), input) | |
| 1340 | + // TokenStream -> Flux 桥接:onPartialResponse 推片段,完成/异常时结束流 | |
| 1341 | + return Flux.<String>create(sink -> chatiAgent.chatStream(session.getUserId(), input) | |
| 1342 | + .onPartialResponse(sink::next) | |
| 1343 | + .onCompleteResponse(response -> sink.complete()) | |
| 1344 | + .onError(error -> { | |
| 1345 | + log.error("闲聊流式返回异常, userId={}", session.getUserId(), error); | |
| 1346 | + sink.complete(); | |
| 1347 | + }) | |
| 1348 | + .start()) | |
| 1240 | 1349 | .map(chunk -> AiResponseDTO.builder() |
| 1241 | 1350 | .sSceneName(sceneName) |
| 1242 | 1351 | .sMethodName(methodName) |
| ... | ... | @@ -1256,13 +1365,6 @@ public class XlyErpService { |
| 1256 | 1365 | private AiResponseDTO getChatiAgent (String input,UserSceneSession session){ |
| 1257 | 1366 | String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; |
| 1258 | 1367 | String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():"随便聊聊"; |
| 1259 | - ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId()); | |
| 1260 | - if(ObjectUtil.isEmpty(chatiAgent)){ | |
| 1261 | - chatiAgent = AiServices.builder(ChatiAgent.class) | |
| 1262 | - .chatModel(chatiModel) | |
| 1263 | - .chatMemoryProvider(operableChatMemoryProvider) | |
| 1264 | - .build(); | |
| 1265 | - UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent); } | |
| 1266 | 1368 | String sChatMessage = chatiAgent.chat(session.getUserId(), input); |
| 1267 | 1369 | return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sChatMessage).systemText(StrUtil.EMPTY).sReturnType(ReturnTypeCode.HTML.getCode()).build(); |
| 1268 | 1370 | } | ... | ... |
src/main/java/com/xly/tool/DynamicToolProvider.java
| ... | ... | @@ -42,7 +42,6 @@ import java.util.*; |
| 42 | 42 | |
| 43 | 43 | import java.util.concurrent.ConcurrentHashMap; |
| 44 | 44 | import java.util.stream.Collectors; |
| 45 | -import java.util.stream.IntStream; | |
| 46 | 45 | |
| 47 | 46 | @Slf4j |
| 48 | 47 | @Service |
| ... | ... | @@ -57,8 +56,10 @@ public class DynamicToolProvider implements ToolProvider { |
| 57 | 56 | private final OperableChatMemoryProvider operableChatMemoryProvider; |
| 58 | 57 | |
| 59 | 58 | private final Map<String, ToolSpecificationHolder> toolCache = new ConcurrentHashMap<>(); |
| 59 | + public final List<ToolMeta> metaAll = new ArrayList<>(); | |
| 60 | 60 | public final Map<String, String> sSceneIdMap = new ConcurrentHashMap<>(); |
| 61 | 61 | public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMap = new ConcurrentHashMap<>(); |
| 62 | + public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMapFist = new ConcurrentHashMap<>(); | |
| 62 | 63 | private final List<ParamRule> paramRuleDataAll = new ArrayList<>(); |
| 63 | 64 | |
| 64 | 65 | @Value("${erp.baseurl}") |
| ... | ... | @@ -73,20 +74,31 @@ public class DynamicToolProvider implements ToolProvider { |
| 73 | 74 | @jakarta.annotation.PostConstruct |
| 74 | 75 | public void init() { |
| 75 | 76 | List<ToolMeta> metas = toolMetaMapper.findAll(); |
| 77 | + metaAll.addAll(metas); | |
| 76 | 78 | for (ToolMeta meta : metas) { |
| 77 | 79 | try { |
| 78 | 80 | doSetToolAIshowfieldShow(meta); |
| 79 | 81 | ToolSpecification spec = buildToolSpecification(meta); |
| 80 | 82 | ToolExecutor executor = createToolExecutor(meta); |
| 81 | - toolCache.put(meta.getSMethodNo(), new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName())); | |
| 83 | + toolCache.put(meta.getSMethodNo(), new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc())); | |
| 82 | 84 | log.info("已加载动态工具:{}", meta.getSMethodNo()); |
| 83 | 85 | String sceneId = meta.getSSceneId(); |
| 84 | 86 | List<ToolSpecificationHolder> dataList = new ArrayList<>(); |
| 85 | 87 | if(ObjectUtil.isNotEmpty(sceneToolCacheMap.get(sceneId))) { |
| 86 | 88 | dataList = sceneToolCacheMap.get(sceneId); |
| 87 | 89 | } |
| 88 | - dataList.add(new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName())); | |
| 90 | + dataList.add(new ToolSpecificationHolder(spec, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc())); | |
| 89 | 91 | sceneToolCacheMap.put(sceneId, dataList); |
| 92 | + | |
| 93 | + // | |
| 94 | + ToolSpecification specF = buildToolSpecificationF(meta); | |
| 95 | + List<ToolSpecificationHolder> dataListF = new ArrayList<>(); | |
| 96 | + if(ObjectUtil.isNotEmpty(sceneToolCacheMapFist.get(sceneId))) { | |
| 97 | + dataListF = sceneToolCacheMapFist.get(sceneId); | |
| 98 | + } | |
| 99 | + dataListF.add(new ToolSpecificationHolder(specF, executor,meta.getSMethodNo(),meta.getSMethodName(),meta.getStoolDesc())); | |
| 100 | + sceneToolCacheMapFist.put(sceneId, dataListF); | |
| 101 | + | |
| 90 | 102 | } catch (Exception e) { |
| 91 | 103 | e.printStackTrace(); |
| 92 | 104 | log.error("构建工具失败,sMethodNo={}", meta.getSMethodNo(), e); |
| ... | ... | @@ -153,136 +165,147 @@ public class DynamicToolProvider implements ToolProvider { |
| 153 | 165 | log.info("创建工具执行器: {}", meta.getSMethodNo()); |
| 154 | 166 | |
| 155 | 167 | return (toolExecutionRequest, memoryId) -> { |
| 156 | - log.info("===== 工具执行器开始执行 ====="); | |
| 157 | - log.info("工具编号: {}", meta.getSMethodNo()); | |
| 158 | - log.info("工具名称: {}", meta.getSMethodName()); | |
| 159 | - log.info("memoryId: {}", memoryId); | |
| 160 | - log.info("请求参数: {}", toolExecutionRequest.arguments()); | |
| 161 | - UserSceneSession session = UserSceneSessionService.USER_SCENE_SESSION_CACHE.get(memoryId.toString()); | |
| 162 | - session.setCurrentTool(meta); | |
| 163 | - | |
| 164 | - // ====================== 防重复调用:立即上锁 ====================== | |
| 165 | - session.setToolExecuted(true); | |
| 166 | - if (StrUtil.isNotBlank(session.getSFunPrompts())) { | |
| 167 | - // 关键:返回 工具执行失败 = 框架强制停止循环 | |
| 168 | + try{ | |
| 169 | + log.info("===== 工具执行器开始执行 ====="); | |
| 170 | + log.info("工具编号: {}", meta.getSMethodNo()); | |
| 171 | + log.info("工具名称: {}", meta.getSMethodName()); | |
| 172 | + log.info("memoryId: {}", memoryId); | |
| 173 | + log.info("请求参数: {}", toolExecutionRequest.arguments()); | |
| 174 | + UserSceneSession session = UserSceneSessionService.USER_SCENE_SESSION_CACHE.get(memoryId.toString()); | |
| 175 | + session.setCurrentTool(meta); | |
| 176 | + //如果不存在放入数据 | |
| 177 | + if (session.getArgs() == null) { | |
| 178 | + session.setArgs(new HashMap<>()); | |
| 179 | + } | |
| 180 | + // ====================== 防重复调用:立即上锁 ====================== | |
| 181 | + session.setToolExecuted(true); | |
| 182 | + if (StrUtil.isNotBlank(session.getSFunPrompts())) { | |
| 183 | + // 关键:返回 工具执行失败 = 框架强制停止循环 | |
| 168 | 184 | // throw new IllegalStateException("STOP_INVOCATION: 任务已完成,停止调用"); |
| 169 | - return "【任务已完成】请勿重复调用工具,请直接总结结果回复用户:"+session.getSFunPrompts(); | |
| 170 | - } | |
| 171 | - //解析参数失败 | |
| 172 | - Map<String, Object> argsNew; | |
| 173 | - try { | |
| 174 | - argsNew = objectMapper.readValue(toolExecutionRequest.arguments(), new TypeReference<>() {}); | |
| 175 | - log.info("解析后的参数: {}", argsNew); | |
| 176 | - } catch (Exception e) { | |
| 177 | - log.error("参数解析失败", e); | |
| 178 | - // 抛异常 | |
| 179 | - throw new RuntimeException("参数解析失败,请重新输入"); | |
| 180 | - } | |
| 185 | + return "【任务已完成】请勿重复调用工具,请直接总结结果回复用户:"+session.getSFunPrompts(); | |
| 186 | + } | |
| 187 | + //解析参数失败 | |
| 188 | + Map<String, Object> argsNew; | |
| 189 | + try { | |
| 190 | + argsNew = objectMapper.readValue(toolExecutionRequest.arguments(), new TypeReference<>() {}); | |
| 191 | + log.info("解析后的参数: {}", argsNew); | |
| 192 | + } catch (Exception e) { | |
| 193 | + log.error("参数解析失败", e); | |
| 194 | + // 抛异常 | |
| 195 | + throw new RuntimeException("参数解析失败,请重新输入"); | |
| 196 | + } | |
| 181 | 197 | |
| 182 | - //合并动态部件 | |
| 183 | - String sDynamicValue = ObjectUtil.isNotEmpty(argsNew.get("动态部件")) | |
| 184 | - ? argsNew.get("动态部件").toString():( ObjectUtil.isNotEmpty(argsNew.get("sDynamicPart"))? argsNew.get("sDynamicPart").toString():StrUtil.EMPTY); | |
| 185 | - String sDynamicValueOld =StrUtil.EMPTY; | |
| 186 | - if(sDynamicValue.contains("{")){ | |
| 187 | - sDynamicValue = StrUtil.EMPTY; | |
| 188 | - } | |
| 189 | - if(ObjectUtil.isNotEmpty(session.getArgs())){ | |
| 198 | + //合并动态部件 | |
| 199 | + String sDynamicValue = ObjectUtil.isNotEmpty(argsNew.get("动态部件")) | |
| 200 | + ? argsNew.get("动态部件").toString():( ObjectUtil.isNotEmpty(argsNew.get("sDynamicPart"))? argsNew.get("sDynamicPart").toString():StrUtil.EMPTY); | |
| 201 | + String sDynamicValueOld =StrUtil.EMPTY; | |
| 202 | + if(sDynamicValue.contains("{")){ | |
| 203 | + sDynamicValue = StrUtil.EMPTY; | |
| 204 | + } | |
| 205 | + //获取参数 如果不存在放入空 | |
| 190 | 206 | Map<String,Object> argOld = session.getArgs(); |
| 191 | 207 | sDynamicValueOld = ObjectUtil.isNotEmpty(argOld.get("动态部件")) |
| 192 | 208 | ? argOld.get("动态部件").toString():( ObjectUtil.isNotEmpty(argOld.get("sDynamicPart"))? argOld.get("sDynamicPart").toString():StrUtil.EMPTY); |
| 193 | - } | |
| 194 | - Set<String> sDynamicData = new HashSet<>(); | |
| 195 | - if(ObjectUtil.isNotEmpty(sDynamicValueOld)){ | |
| 196 | - List<String> data = new ArrayList<>(); | |
| 197 | - if (JSONUtil.isTypeJSONArray(sDynamicValueOld)) { | |
| 198 | - data = JSONUtil.toList(sDynamicValueOld, String.class); | |
| 199 | - } else { | |
| 200 | - data.add(sDynamicValueOld); | |
| 209 | + | |
| 210 | + Set<String> sDynamicData = new HashSet<>(); | |
| 211 | + if(ObjectUtil.isNotEmpty(sDynamicValueOld)){ | |
| 212 | + List<String> data = new ArrayList<>(); | |
| 213 | + if (JSONUtil.isTypeJSONArray(sDynamicValueOld)) { | |
| 214 | + data = JSONUtil.toList(sDynamicValueOld, String.class); | |
| 215 | + } else { | |
| 216 | + data.add(sDynamicValueOld); | |
| 217 | + } | |
| 218 | + sDynamicData.addAll(data); | |
| 201 | 219 | } |
| 202 | - sDynamicData.addAll(data); | |
| 203 | - } | |
| 204 | - if(ObjectUtil.isNotEmpty(sDynamicValue)){ | |
| 205 | - List<String> data = new ArrayList<>(); | |
| 206 | - if (JSONUtil.isTypeJSONArray(sDynamicValue)) { | |
| 207 | - data = JSONUtil.toList(sDynamicValue, String.class); | |
| 208 | - } else { | |
| 209 | - data.add(sDynamicValue); | |
| 220 | + if(ObjectUtil.isNotEmpty(sDynamicValue)){ | |
| 221 | + List<String> data = new ArrayList<>(); | |
| 222 | + if (JSONUtil.isTypeJSONArray(sDynamicValue)) { | |
| 223 | + data = JSONUtil.toList(sDynamicValue, String.class); | |
| 224 | + } else { | |
| 225 | + data.add(sDynamicValue); | |
| 226 | + } | |
| 227 | + sDynamicData.addAll(data); | |
| 210 | 228 | } |
| 211 | - sDynamicData.addAll(data); | |
| 212 | - } | |
| 213 | - if(ObjectUtil.isNotEmpty(sDynamicData)){ | |
| 214 | - session.getArgs().remove("动态部件"); | |
| 215 | - session.getArgs().remove("sDynamicPart"); | |
| 216 | - argsNew.put("动态部件",JSONUtil.toJsonStr(sDynamicData)); | |
| 217 | - argsNew.put("sDynamicPart",JSONUtil.toJsonStr(sDynamicData)); | |
| 218 | - } | |
| 219 | - //获取之前获取的参数 | |
| 220 | - Map<String, Object> args = session.getArgs(); | |
| 221 | - if(ObjectUtil.isEmpty(args)) args = new HashMap<>(); | |
| 222 | - Map<String, Object> finalArgs = args; | |
| 223 | - argsNew.forEach((k, v)->{ | |
| 224 | - if(Validator.isValid(v)){ | |
| 225 | - finalArgs.put(k,v); | |
| 229 | + if(ObjectUtil.isNotEmpty(sDynamicData)){ | |
| 230 | + session.getArgs().remove("动态部件"); | |
| 231 | + session.getArgs().remove("sDynamicPart"); | |
| 232 | + argsNew.put("动态部件",JSONUtil.toJsonStr(sDynamicData)); | |
| 233 | + argsNew.put("sDynamicPart",JSONUtil.toJsonStr(sDynamicData)); | |
| 226 | 234 | } |
| 227 | - }); | |
| 228 | - if(argsNew.containsKey("删除动态部件")){ | |
| 229 | - argsNew.put("sDynamicPartDel",argsNew.get("删除动态部件")); | |
| 230 | - } | |
| 231 | - if(!argsNew.containsKey("sDynamicPartDel")){ | |
| 232 | - finalArgs.remove("sDynamicPartDel"); | |
| 233 | - finalArgs.remove("删除动态部件"); | |
| 234 | - } | |
| 235 | - // 2 【补全动态参数】动态参数补全(中->英文) | |
| 236 | - try{ | |
| 237 | - args = applyValues(args, meta.getParamRuleListCheck()); | |
| 238 | - }catch (Exception e){ | |
| 239 | - log.error("返回信息",e); | |
| 240 | - String askMsg = e.getMessage(); | |
| 241 | - session.setSFunPrompts(askMsg); | |
| 242 | - // 需要提问用户 → 抛异常停止循环 | |
| 243 | - throw new RuntimeException(askMsg); | |
| 244 | - } | |
| 245 | - // 2.1 【自动补全】应用参数的默认值 | |
| 246 | - List<ParamRule> paramRuleData = meta.getParamRuleListAll(); | |
| 247 | - //添加默认值 | |
| 248 | - args = applyDefaultValues(args, paramRuleData); | |
| 249 | - //客户选择行号转sSLaveId | |
| 250 | - if(ObjectUtil.isNotEmpty(args) | |
| 251 | - &&( | |
| 252 | - (args.containsKey("rowNumbers") && ObjectUtil.isNotEmpty(args.get("rowNumbers"))) | |
| 253 | - || (args.containsKey("operateType") && "全部确认".equals(args.get("operateType"))) | |
| 254 | - )&& ObjectUtil.isNotEmpty(session.getCurrentRowData()) | |
| 255 | - ){ | |
| 256 | - doSetSlaveIdToArgs( args, session); | |
| 257 | - } | |
| 258 | - //存入已采集参数 | |
| 259 | - session.setArgs(args); | |
| 260 | - // 2.1.1 根据当前获取的新参数判断是否存在有值必填例如动态参数、动态部件 | |
| 261 | - if(meta.getBQuo()){ | |
| 262 | - List<ParamRule> dynamicParamRule = doSetParamgetArgsAfter(args, session); | |
| 263 | - if(ObjectUtil.isNotEmpty(dynamicParamRule)){ | |
| 235 | + //获取之前获取的参数 | |
| 236 | + Map<String, Object> args = session.getArgs(); | |
| 237 | + if(ObjectUtil.isEmpty(args)) args = new HashMap<>(); | |
| 238 | + Map<String, Object> finalArgs = args; | |
| 239 | + argsNew.forEach((k, v)->{ | |
| 240 | + if(Validator.isValid(v)){ | |
| 241 | + finalArgs.put(k,v); | |
| 242 | + } | |
| 243 | + }); | |
| 244 | + if(argsNew.containsKey("删除动态部件")){ | |
| 245 | + argsNew.put("sDynamicPartDel",argsNew.get("删除动态部件")); | |
| 246 | + } | |
| 247 | + if(!argsNew.containsKey("sDynamicPartDel")){ | |
| 248 | + finalArgs.remove("sDynamicPartDel"); | |
| 249 | + finalArgs.remove("删除动态部件"); | |
| 250 | + } | |
| 251 | + // 2 【补全动态参数】动态参数补全(中->英文) | |
| 252 | + try{ | |
| 253 | + args = applyValues(args, meta.getParamRuleListCheck()); | |
| 254 | + }catch (Exception e){ | |
| 255 | + log.error("返回信息",e); | |
| 256 | + String askMsg = e.getMessage(); | |
| 257 | + session.setSFunPrompts(askMsg); | |
| 258 | + // 需要提问用户 → 抛异常停止循环 | |
| 259 | + throw new RuntimeException(askMsg); | |
| 260 | + } | |
| 261 | + // 2.1 【自动补全】应用参数的默认值 | |
| 262 | + List<ParamRule> paramRuleData = meta.getParamRuleListAll(); | |
| 263 | + //添加默认值 | |
| 264 | + args = applyDefaultValues(args, paramRuleData); | |
| 265 | + //客户选择行号转sSLaveId | |
| 266 | + if(ObjectUtil.isNotEmpty(args) | |
| 267 | + &&( | |
| 268 | + (args.containsKey("rowNumbers") && ObjectUtil.isNotEmpty(args.get("rowNumbers"))) | |
| 269 | + || (args.containsKey("operateType") && "全部确认".equals(args.get("operateType"))) | |
| 270 | + )&& ObjectUtil.isNotEmpty(session.getCurrentRowData()) | |
| 271 | + ){ | |
| 272 | + doSetSlaveIdToArgs( args, session); | |
| 273 | + } | |
| 274 | + if(ObjectUtil.isEmpty(args)){ | |
| 275 | + args = new HashMap<>(); | |
| 276 | + } | |
| 277 | + //存入已采集参数 | |
| 278 | + session.setArgs(args); | |
| 279 | + // 2.1.1 根据当前获取的新参数判断是否存在有值必填例如动态参数、动态部件 | |
| 280 | + if(meta.getBQuo()){ | |
| 281 | + List<ParamRule> dynamicParamRule = doSetParamgetArgsAfter(args, session); | |
| 282 | + if(ObjectUtil.isNotEmpty(dynamicParamRule)){ | |
| 264 | 283 | // Map<String,Object> data = quoService.getQuoData( args, session); |
| 265 | - session.getCurrentTool().setDynamicParamRule(dynamicParamRule); | |
| 284 | + session.getCurrentTool().setDynamicParamRule(dynamicParamRule); | |
| 266 | 285 | // String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,dynamicParamRule); |
| 267 | 286 | // session.setSSystemPrompt(sSystemPrompt); |
| 287 | + } | |
| 268 | 288 | } |
| 289 | + //获取是否缺失参数 | |
| 290 | + String askMsg = getMissParamMsg( meta, session, args); | |
| 291 | + if(ObjectUtil.isNotEmpty(askMsg)){ | |
| 292 | + return askMsg; | |
| 293 | + } | |
| 294 | + String resp = StrUtil.EMPTY; | |
| 295 | + if(session.getCurrentTool().getBQuo()){ | |
| 296 | + resp = JSONUtil.toJsonStr(session.getQuoData()); | |
| 297 | + session.setSFunPrompts(resp); | |
| 298 | + session.getCurrentTool().setDynamicParamRule(null); | |
| 299 | + return resp; | |
| 300 | + }else{ | |
| 301 | + resp = JSONUtil.toJsonStr(args) ; | |
| 302 | + } | |
| 303 | + // ====================== 返回时带终止指令 ====================== | |
| 304 | + return String.valueOf(successResult(toolExecutionRequest, resp)); | |
| 305 | + }catch (Exception e){ | |
| 306 | + return String.valueOf(successResult(toolExecutionRequest,"执行方法异常"+e.getMessage())); | |
| 269 | 307 | } |
| 270 | - //获取是否缺失参数 | |
| 271 | - String askMsg = getMissParamMsg( meta, session, args); | |
| 272 | - if(ObjectUtil.isNotEmpty(askMsg)){ | |
| 273 | - return askMsg; | |
| 274 | - } | |
| 275 | - String resp = StrUtil.EMPTY; | |
| 276 | - if(session.getCurrentTool().getBQuo()){ | |
| 277 | - resp = JSONUtil.toJsonStr(session.getQuoData()); | |
| 278 | - session.setSFunPrompts(resp); | |
| 279 | - session.getCurrentTool().setDynamicParamRule(null); | |
| 280 | - return resp; | |
| 281 | - }else{ | |
| 282 | - resp = JSONUtil.toJsonStr(args) ; | |
| 283 | - } | |
| 284 | - // ====================== 返回时带终止指令 ====================== | |
| 285 | - return String.valueOf(successResult(toolExecutionRequest, resp)); | |
| 308 | + | |
| 286 | 309 | }; |
| 287 | 310 | } |
| 288 | 311 | |
| ... | ... | @@ -754,6 +777,41 @@ public class DynamicToolProvider implements ToolProvider { |
| 754 | 777 | .build(); |
| 755 | 778 | } |
| 756 | 779 | |
| 780 | + public ToolSpecification buildToolSpecificationF(ToolMeta meta) { | |
| 781 | + ToolSpecification.Builder builder = ToolSpecification.builder() | |
| 782 | + .name(meta.getSMethodNo()); | |
| 783 | + | |
| 784 | + StringBuffer stoolDesc = new StringBuffer(); | |
| 785 | + String forceToolPrompt = """ | |
| 786 | + 【工具调用规则】 | |
| 787 | + 1. 每个自定义方法最多只能调用 1 次 | |
| 788 | + 2. 只要方法返回结果,必须停止调用 | |
| 789 | + 3. 当用户输入包含【数据确认】、确认数据、确认、第*条数据确认时,必须调用本工具 | |
| 790 | + """; | |
| 791 | + stoolDesc.append(forceToolPrompt); | |
| 792 | + if (ObjectUtil.isNotEmpty(meta.getStoolDesc())) { | |
| 793 | + stoolDesc.append("MethodNo:").append(meta.getSMethodNo()) | |
| 794 | + .append(",当用户").append(meta.getSMethodName()) | |
| 795 | + .append("时,必须调用本工具").append(meta.getSMethodNo()) | |
| 796 | + .append(",").append(meta.getStoolDesc()); | |
| 797 | + } | |
| 798 | + JsonObjectSchema.Builder schemaBuilder = JsonObjectSchema.builder(); | |
| 799 | + List<String> requiredParams = new ArrayList<>(); | |
| 800 | + List<String> requiredParamsNew = new ArrayList<>(); | |
| 801 | + if(requiredParams!=null && requiredParams.size()>0){ | |
| 802 | + requiredParamsNew.add(requiredParams.get(0)); | |
| 803 | + } | |
| 804 | + if (!requiredParams.isEmpty()) { | |
| 805 | + schemaBuilder.required(requiredParamsNew); | |
| 806 | + } | |
| 807 | + JsonObjectSchema parameters = schemaBuilder.build(); | |
| 808 | + return builder | |
| 809 | + .description(stoolDesc.toString()) | |
| 810 | + .parameters(parameters) | |
| 811 | + .build(); | |
| 812 | + } | |
| 813 | + | |
| 814 | + | |
| 757 | 815 | private String getConstMeg(ParamRule paramRule){ |
| 758 | 816 | if(!RuleCode.CONST.getCode().equals(paramRule.getSRule())){ |
| 759 | 817 | return StrUtil.EMPTY; |
| ... | ... | @@ -832,7 +890,7 @@ public class DynamicToolProvider implements ToolProvider { |
| 832 | 890 | private String getMissParamMsg(ToolMeta meta,UserSceneSession session, Map<String, Object> args){ |
| 833 | 891 | if(meta.getBQuo()){ |
| 834 | 892 | Map<String,Object> data = quoService.getQuoData(args, session); |
| 835 | - List<Map<String,Object>> sAll = (List<Map<String, Object>>) data.get("sAll"); | |
| 893 | + List<Map<String,Object>> sAll = (ObjectUtil.isEmpty(data.get("sAll")))?new ArrayList<> ():(List<Map<String, Object>>) data.get("sAll"); | |
| 836 | 894 | List<Map<String,Object>> sEmptyData = sAll.stream().filter(one-> BooleanUtil.toBoolean(one.get("bEmpty").toString()) |
| 837 | 895 | && (!Validator.isValid(one.get("sValue"))) |
| 838 | 896 | && !"sDynamicPart".equals(one.get("sName")) |
| ... | ... | @@ -842,9 +900,10 @@ public class DynamicToolProvider implements ToolProvider { |
| 842 | 900 | if(ObjectUtil.isNotEmpty(sEmptyData)){ |
| 843 | 901 | List<ParamRule> missData = session.getCurrentTool().getParamRuleListAll().stream().filter(d -> |
| 844 | 902 | d.getBEmpty() |
| 903 | + && (ObjectUtil.isNotEmpty(args)) | |
| 845 | 904 | && (!Validator.isValid(args.get(d.getSParam()))) |
| 846 | - && !"sDynamicPart".equals(d.getSParamValue()) | |
| 847 | - && !"sDynamicPartDel".equals(d.getSParamValue()) | |
| 905 | + && !"sDynamicPart".equals(d.getSParamValue()) | |
| 906 | + && !"sDynamicPartDel".equals(d.getSParamValue()) | |
| 848 | 907 | ).collect(Collectors.toUnmodifiableList()); |
| 849 | 908 | data.put("bTs",true); |
| 850 | 909 | String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,missData); | ... | ... |
src/main/java/com/xly/tool/ToolSpecificationHolder.java
| ... | ... | @@ -9,11 +9,13 @@ public class ToolSpecificationHolder { |
| 9 | 9 | private final ToolExecutor toolExecutor; |
| 10 | 10 | private final String sName; |
| 11 | 11 | private final String sMethodName; |
| 12 | - public ToolSpecificationHolder(ToolSpecification toolSpecification, ToolExecutor toolExecutor,String sName,String sMethodName) { | |
| 12 | + private final String stoolDesc; | |
| 13 | + public ToolSpecificationHolder(ToolSpecification toolSpecification, ToolExecutor toolExecutor,String sName,String sMethodName,String stoolDesc) { | |
| 13 | 14 | this.toolSpecification = toolSpecification; |
| 14 | 15 | this.toolExecutor = toolExecutor; |
| 15 | 16 | this.sName = sName; |
| 16 | 17 | this.sMethodName = sMethodName; |
| 18 | + this.stoolDesc = stoolDesc; | |
| 17 | 19 | } |
| 18 | 20 | |
| 19 | 21 | public ToolSpecification getToolSpecification() { |
| ... | ... | @@ -31,5 +33,9 @@ public class ToolSpecificationHolder { |
| 31 | 33 | public String getsMethodName() { |
| 32 | 34 | return sMethodName; |
| 33 | 35 | } |
| 36 | + | |
| 37 | + public String getStoolDesc() { | |
| 38 | + return stoolDesc; | |
| 39 | + } | |
| 34 | 40 | } |
| 35 | 41 | ... | ... |