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,30 +2,45 @@ package com.xly.agent; | ||
| 2 | 2 | ||
| 3 | import dev.langchain4j.service.MemoryId; | 3 | import dev.langchain4j.service.MemoryId; |
| 4 | import dev.langchain4j.service.SystemMessage; | 4 | import dev.langchain4j.service.SystemMessage; |
| 5 | +import dev.langchain4j.service.TokenStream; | ||
| 5 | import dev.langchain4j.service.UserMessage; | 6 | import dev.langchain4j.service.UserMessage; |
| 6 | import dev.langchain4j.service.V; | 7 | import dev.langchain4j.service.V; |
| 7 | -import reactor.core.publisher.Flux; | ||
| 8 | 8 | ||
| 9 | +/** | ||
| 10 | + * 闲聊智能体(小羚羊人设) | ||
| 11 | + * 说明:同步/流式两个方法共用同一份人设提示词,改一处即可,避免两份拷贝走偏 | ||
| 12 | + */ | ||
| 9 | public interface ChatiAgent { | 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 | @UserMessage("用户说:{{userInput}}") | 34 | @UserMessage("用户说:{{userInput}}") |
| 19 | String chat(@MemoryId String userId, @V("userInput") String userInput); | 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 | @UserMessage("用户说:{{userInput}}") | 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 | \ No newline at end of file | 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 | \ No newline at end of file | 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,6 +5,7 @@ import com.alibaba.fastjson2.JSON; | ||
| 5 | import com.alibaba.fastjson2.JSONObject; | 5 | import com.alibaba.fastjson2.JSONObject; |
| 6 | import com.alibaba.fastjson2.JSONWriter; | 6 | import com.alibaba.fastjson2.JSONWriter; |
| 7 | import com.xly.entity.ParamRule; | 7 | import com.xly.entity.ParamRule; |
| 8 | +import com.xly.entity.ToolMeta; | ||
| 8 | import com.xly.entity.UserSceneSession; | 9 | import com.xly.entity.UserSceneSession; |
| 9 | 10 | ||
| 10 | import java.util.List; | 11 | import java.util.List; |
| @@ -110,4 +111,60 @@ public class SystemPromptGenerator { | @@ -110,4 +111,60 @@ public class SystemPromptGenerator { | ||
| 110 | """.formatted(methodNo); | 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 | \ No newline at end of file | 171 | \ No newline at end of file |
src/main/java/com/xly/config/ModelConfig.java
| 1 | package com.xly.config; | 1 | package com.xly.config; |
| 2 | 2 | ||
| 3 | +import com.fasterxml.jackson.databind.DeserializationFeature; | ||
| 3 | import com.fasterxml.jackson.databind.ObjectMapper; | 4 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 4 | import com.fasterxml.jackson.databind.SerializationFeature; | 5 | import com.fasterxml.jackson.databind.SerializationFeature; |
| 5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
| 7 | +import com.xly.agent.ChatiAgent; | ||
| 6 | import com.xly.agent.DynamicTableNl2SqlAiAgent; | 8 | import com.xly.agent.DynamicTableNl2SqlAiAgent; |
| 7 | import com.xly.agent.SceneSelectorAiAgent; | 9 | import com.xly.agent.SceneSelectorAiAgent; |
| 10 | +import com.xly.agent.SecMethodAiAgent; | ||
| 11 | +import dev.langchain4j.memory.chat.MessageWindowChatMemory; | ||
| 8 | import dev.langchain4j.model.ollama.OllamaChatModel; | 12 | import dev.langchain4j.model.ollama.OllamaChatModel; |
| 9 | import dev.langchain4j.model.ollama.OllamaStreamingChatModel; | 13 | import dev.langchain4j.model.ollama.OllamaStreamingChatModel; |
| 10 | import dev.langchain4j.service.AiServices; | 14 | import dev.langchain4j.service.AiServices; |
| 11 | -import dev.langchain4j.memory.chat.MessageWindowChatMemory; | ||
| 12 | import org.springframework.beans.factory.annotation.Qualifier; | 15 | import org.springframework.beans.factory.annotation.Qualifier; |
| 13 | import org.springframework.beans.factory.annotation.Value; | 16 | import org.springframework.beans.factory.annotation.Value; |
| 14 | import org.springframework.context.annotation.Bean; | 17 | import org.springframework.context.annotation.Bean; |
| @@ -17,11 +20,17 @@ import org.springframework.context.annotation.Primary; | @@ -17,11 +20,17 @@ import org.springframework.context.annotation.Primary; | ||
| 17 | 20 | ||
| 18 | import java.time.Duration; | 21 | import java.time.Duration; |
| 19 | 22 | ||
| 23 | +/** | ||
| 24 | + * 模型与 Agent 统一装配(langchain4j 1.14.0 新 API:ChatModel / StreamingChatModel) | ||
| 25 | + * 约定: | ||
| 26 | + * - 路由/SQL/参数提取类任务 → 低温(确定性) | ||
| 27 | + * - 闲聊类任务 → 高温(多样性) | ||
| 28 | + */ | ||
| 20 | @Configuration | 29 | @Configuration |
| 21 | public class ModelConfig { | 30 | public class ModelConfig { |
| 22 | 31 | ||
| 23 | @Value("${langchain4j.ollama.base-url}") | 32 | @Value("${langchain4j.ollama.base-url}") |
| 24 | - private String chatModelUrl; | 33 | + private String baseUrl; |
| 25 | 34 | ||
| 26 | @Value("${langchain4j.ollama.chat-model-name}") | 35 | @Value("${langchain4j.ollama.chat-model-name}") |
| 27 | private String chatModelName; | 36 | private String chatModelName; |
| @@ -29,88 +38,106 @@ public class ModelConfig { | @@ -29,88 +38,106 @@ public class ModelConfig { | ||
| 29 | @Value("${langchain4j.ollama.sql-model-name}") | 38 | @Value("${langchain4j.ollama.sql-model-name}") |
| 30 | private String sqlModelName; | 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 | @Bean | 80 | @Bean |
| 34 | @Primary | 81 | @Primary |
| 35 | public OllamaChatModel chatLanguageModel() { | 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 | .logRequests(true) | 84 | .logRequests(true) |
| 44 | .logResponses(true) | 85 | .logResponses(true) |
| 45 | .build(); | 86 | .build(); |
| 46 | } | 87 | } |
| 47 | 88 | ||
| 48 | - // ====================== 自由闲聊模型 ====================== | 89 | + /** 自由闲聊(非流式) */ |
| 49 | @Bean("chatiModel") | 90 | @Bean("chatiModel") |
| 50 | public OllamaChatModel chatiModel() { | 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 | @Bean("sqlChatModel") | 102 | @Bean("sqlChatModel") |
| 63 | public OllamaChatModel sqlChatModel() { | 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 | .numPredict(4096) | 105 | .numPredict(4096) |
| 70 | - .timeout(Duration.ofSeconds(120)) | ||
| 71 | - .maxRetries(3) | ||
| 72 | .build(); | 106 | .build(); |
| 73 | } | 107 | } |
| 74 | 108 | ||
| 75 | - // ====================== 流式对话模型 ====================== | 109 | + /** 主流式对话 */ |
| 76 | @Bean("streamingChatModel") | 110 | @Bean("streamingChatModel") |
| 77 | @Primary | 111 | @Primary |
| 78 | public OllamaStreamingChatModel streamingChatModel() { | 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 | @Bean("streamingSqlModel") | 117 | @Bean("streamingSqlModel") |
| 91 | public OllamaStreamingChatModel streamingSqlModel() { | 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 | @Bean | 126 | @Bean |
| 104 | @Primary | 127 | @Primary |
| 105 | public ObjectMapper objectMapper() { | 128 | public ObjectMapper objectMapper() { |
| 106 | ObjectMapper mapper = new ObjectMapper(); | 129 | ObjectMapper mapper = new ObjectMapper(); |
| 107 | mapper.registerModule(new JavaTimeModule()); | 130 | mapper.registerModule(new JavaTimeModule()); |
| 108 | mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); | 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 | return mapper; | 133 | return mapper; |
| 111 | } | 134 | } |
| 112 | 135 | ||
| 113 | - // ====================== 动态 SQL Agent ====================== | 136 | + // ======================================================== |
| 137 | + // 四、Agent Bean | ||
| 138 | + // ======================================================== | ||
| 139 | + | ||
| 140 | + /** 动态 SQL Agent */ | ||
| 114 | @Bean | 141 | @Bean |
| 115 | public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent( | 142 | public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent( |
| 116 | @Qualifier("sqlChatModel") OllamaChatModel sqlModel) { | 143 | @Qualifier("sqlChatModel") OllamaChatModel sqlModel) { |
| @@ -120,13 +147,45 @@ public class ModelConfig { | @@ -120,13 +147,45 @@ public class ModelConfig { | ||
| 120 | .build(); | 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 | @Bean | 168 | @Bean |
| 125 | public SceneSelectorAiAgent sceneSelectorAiAgent( | 169 | public SceneSelectorAiAgent sceneSelectorAiAgent( |
| 126 | @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) { | 170 | @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) { |
| 127 | return AiServices.builder(SceneSelectorAiAgent.class) | 171 | return AiServices.builder(SceneSelectorAiAgent.class) |
| 128 | .chatModel(chatLanguageModel) | 172 | .chatModel(chatLanguageModel) |
| 129 | .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) | 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 | .build(); | 189 | .build(); |
| 131 | } | 190 | } |
| 132 | } | 191 | } |
| 133 | \ No newline at end of file | 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 | \ No newline at end of file | 105 | \ No newline at end of file |
src/main/java/com/xly/service/UserSceneSessionService.java
| 1 | package com.xly.service; | 1 | package com.xly.service; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.util.ObjectUtil; | 3 | import cn.hutool.core.util.ObjectUtil; |
| 4 | -import com.xly.agent.ChatiAgent; | ||
| 5 | import com.xly.agent.DynamicTableNl2SqlAiAgent; | 4 | import com.xly.agent.DynamicTableNl2SqlAiAgent; |
| 6 | import com.xly.agent.ErpAiAgent; | 5 | import com.xly.agent.ErpAiAgent; |
| 7 | import com.xly.config.OperableChatMemoryProvider; | 6 | import com.xly.config.OperableChatMemoryProvider; |
| @@ -25,7 +24,7 @@ public class UserSceneSessionService { | @@ -25,7 +24,7 @@ public class UserSceneSessionService { | ||
| 25 | 24 | ||
| 26 | // 原有缓存:Agent实例、会话记忆 | 25 | // 原有缓存:Agent实例、会话记忆 |
| 27 | public static final Map<String, ErpAiAgent> ERP_AGENT_CACHE = new HashMap<>(); | 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 | public static final Map<String, DynamicTableNl2SqlAiAgent> ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>(); | 28 | public static final Map<String, DynamicTableNl2SqlAiAgent> ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>(); |
| 30 | 29 | ||
| 31 | 30 | ||
| @@ -72,7 +71,6 @@ public class UserSceneSessionService { | @@ -72,7 +71,6 @@ public class UserSceneSessionService { | ||
| 72 | public void cleanAllSession(){ | 71 | public void cleanAllSession(){ |
| 73 | USER_SCENE_SESSION_CACHE.clear(); | 72 | USER_SCENE_SESSION_CACHE.clear(); |
| 74 | ERP_AGENT_CACHE.clear(); | 73 | ERP_AGENT_CACHE.clear(); |
| 75 | - CHAT_AGENT_CACHE.clear(); | ||
| 76 | ERP_DynamicTableNl2SqlAiAgent_CACHE.clear(); | 74 | ERP_DynamicTableNl2SqlAiAgent_CACHE.clear(); |
| 77 | } | 75 | } |
| 78 | 76 | ||
| @@ -86,7 +84,6 @@ public class UserSceneSessionService { | @@ -86,7 +84,6 @@ public class UserSceneSessionService { | ||
| 86 | public void cleanUserSession(String sUserId){ | 84 | public void cleanUserSession(String sUserId){ |
| 87 | USER_SCENE_SESSION_CACHE.remove(sUserId); | 85 | USER_SCENE_SESSION_CACHE.remove(sUserId); |
| 88 | ERP_AGENT_CACHE.remove(sUserId); | 86 | ERP_AGENT_CACHE.remove(sUserId); |
| 89 | - CHAT_AGENT_CACHE.remove(sUserId); | ||
| 90 | ERP_DynamicTableNl2SqlAiAgent_CACHE.remove(sUserId); | 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,10 +24,7 @@ import com.xly.thread.MultiThreadPoolServer; | ||
| 24 | import com.xly.tool.DynamicToolProvider; | 24 | import com.xly.tool.DynamicToolProvider; |
| 25 | import com.xly.tool.ToolSpecificationHolder; | 25 | import com.xly.tool.ToolSpecificationHolder; |
| 26 | import com.xly.util.*; | 26 | import com.xly.util.*; |
| 27 | -import dev.langchain4j.agent.tool.ReturnBehavior; | ||
| 28 | -import dev.langchain4j.agent.tool.ToolExecutionRequest; | ||
| 29 | import dev.langchain4j.agent.tool.ToolSpecification; | 27 | import dev.langchain4j.agent.tool.ToolSpecification; |
| 30 | -import dev.langchain4j.data.message.AiMessage; | ||
| 31 | import dev.langchain4j.data.message.ChatMessage; | 28 | import dev.langchain4j.data.message.ChatMessage; |
| 32 | 29 | ||
| 33 | import dev.langchain4j.memory.ChatMemory; | 30 | import dev.langchain4j.memory.ChatMemory; |
| @@ -37,10 +34,8 @@ import dev.langchain4j.service.Result; | @@ -37,10 +34,8 @@ import dev.langchain4j.service.Result; | ||
| 37 | import dev.langchain4j.service.tool.ToolExecutor; | 34 | import dev.langchain4j.service.tool.ToolExecutor; |
| 38 | import lombok.RequiredArgsConstructor; | 35 | import lombok.RequiredArgsConstructor; |
| 39 | import lombok.extern.slf4j.Slf4j; | 36 | import lombok.extern.slf4j.Slf4j; |
| 40 | -import org.apache.commons.lang3.time.DateFormatUtils; | ||
| 41 | import org.springframework.beans.factory.annotation.Value; | 37 | import org.springframework.beans.factory.annotation.Value; |
| 42 | import org.springframework.stereotype.Service; | 38 | import org.springframework.stereotype.Service; |
| 43 | -import org.springframework.util.IdGenerator; | ||
| 44 | import reactor.core.publisher.Flux; | 39 | import reactor.core.publisher.Flux; |
| 45 | 40 | ||
| 46 | import java.time.Duration; | 41 | import java.time.Duration; |
| @@ -54,7 +49,7 @@ import java.util.stream.IntStream; | @@ -54,7 +49,7 @@ import java.util.stream.IntStream; | ||
| 54 | public class XlyErpService { | 49 | public class XlyErpService { |
| 55 | //中文对话模型 | 50 | //中文对话模型 |
| 56 | private final OllamaChatModel chatModel; | 51 | private final OllamaChatModel chatModel; |
| 57 | - private final OllamaChatModel chatiModel; | 52 | + private final ChatiAgent chatiAgent; |
| 58 | private final SceneSelectorAiAgent sceneSelectorAiAgent; | 53 | private final SceneSelectorAiAgent sceneSelectorAiAgent; |
| 59 | private final UserSceneSessionService userSceneSessionService; | 54 | private final UserSceneSessionService userSceneSessionService; |
| 60 | private final DynamicToolProvider dynamicToolProvider; | 55 | private final DynamicToolProvider dynamicToolProvider; |
| @@ -63,6 +58,7 @@ public class XlyErpService { | @@ -63,6 +58,7 @@ public class XlyErpService { | ||
| 63 | private final RedisService redisService; | 58 | private final RedisService redisService; |
| 64 | private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService; | 59 | private final AiGlobalAgentQuestionSqlEmitterService aiGlobalAgentQuestionSqlEmitterService; |
| 65 | private final MilvusService milvusService; | 60 | private final MilvusService milvusService; |
| 61 | + private final SecMethodAiAgent secMethodAiAgent; | ||
| 66 | 62 | ||
| 67 | //执行动态语句 执行异常的情况下 最多执行次数 | 63 | //执行动态语句 执行异常的情况下 最多执行次数 |
| 68 | private final Integer maxRetries = 5; | 64 | private final Integer maxRetries = 5; |
| @@ -342,12 +338,49 @@ public class XlyErpService { | @@ -342,12 +338,49 @@ public class XlyErpService { | ||
| 342 | // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示) | 338 | // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示) |
| 343 | return handleSceneSelect(userId, input, session,1,0); | 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 | String sResponMessage = StrUtil.EMPTY; | 384 | String sResponMessage = StrUtil.EMPTY; |
| 352 | //用户输入添加方法(如果没有方法,动态SQL方法不需要) | 385 | //用户输入添加方法(如果没有方法,动态SQL方法不需要) |
| 353 | Boolean isConfirmed = dynamicToolProvider.isConfirmed(input,session); | 386 | Boolean isConfirmed = dynamicToolProvider.isConfirmed(input,session); |
| @@ -368,6 +401,7 @@ public class XlyErpService { | @@ -368,6 +401,7 @@ public class XlyErpService { | ||
| 368 | //重新生成新的aiAgent拿新的aiAgent 做选择 | 401 | //重新生成新的aiAgent拿新的aiAgent 做选择 |
| 369 | aiAgent = createConfirmeAgent(session); | 402 | aiAgent = createConfirmeAgent(session); |
| 370 | } | 403 | } |
| 404 | + | ||
| 371 | //报价每次定义参数 | 405 | //报价每次定义参数 |
| 372 | if(!isConfirmed && ObjectUtil.isNotEmpty(session) && ObjectUtil.isNotEmpty(session.getCurrentTool()) && session.getCurrentTool().getBQuo()){ | 406 | if(!isConfirmed && ObjectUtil.isNotEmpty(session) && ObjectUtil.isNotEmpty(session.getCurrentTool()) && session.getCurrentTool().getBQuo()){ |
| 373 | sSystemPrompt = SystemPromptGenerator.generate(session.getCurrentTool().getSMethodName(),session,session.getCurrentScene().getSKnowledgeBase()); | 407 | sSystemPrompt = SystemPromptGenerator.generate(session.getCurrentTool().getSMethodName(),session,session.getCurrentScene().getSKnowledgeBase()); |
| @@ -486,7 +520,6 @@ public class XlyErpService { | @@ -486,7 +520,6 @@ public class XlyErpService { | ||
| 486 | UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); | 520 | UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); |
| 487 | // 清空Agent缓存 | 521 | // 清空Agent缓存 |
| 488 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); | 522 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); |
| 489 | - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | ||
| 490 | cleanMemory(userId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization); | 523 | cleanMemory(userId, sUserName, sBrandsId, sSubsidiaryId, sUserType, authorization); |
| 491 | } | 524 | } |
| 492 | 525 | ||
| @@ -503,7 +536,6 @@ public class XlyErpService { | @@ -503,7 +536,6 @@ public class XlyErpService { | ||
| 503 | session.setArgs(null); | 536 | session.setArgs(null); |
| 504 | session.setSUserQuestionList(new ArrayList<>()); | 537 | session.setSUserQuestionList(new ArrayList<>()); |
| 505 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); | 538 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); |
| 506 | - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | ||
| 507 | session.setBCleanMemory(false); | 539 | session.setBCleanMemory(false); |
| 508 | String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; | 540 | String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; |
| 509 | return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(StrUtil.EMPTY).aiText(StrUtil.EMPTY).systemText("清除记忆成功!").sReturnType(ReturnTypeCode.HTML.getCode()).build(); | 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,7 +752,6 @@ public class XlyErpService { | ||
| 720 | session.setArgs(new HashMap<>()); | 752 | session.setArgs(new HashMap<>()); |
| 721 | // session.setSceneSelected(false); | 753 | // session.setSceneSelected(false); |
| 722 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); | 754 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); |
| 723 | - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | ||
| 724 | session.setBCleanMemory(false); | 755 | session.setBCleanMemory(false); |
| 725 | } | 756 | } |
| 726 | 757 | ||
| @@ -944,16 +975,95 @@ public class XlyErpService { | @@ -944,16 +975,95 @@ public class XlyErpService { | ||
| 944 | aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class) | 975 | aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class) |
| 945 | .chatModel(ol) | 976 | .chatModel(ol) |
| 946 | .chatMemoryProvider(operableChatMemoryProvider) | 977 | .chatMemoryProvider(operableChatMemoryProvider) |
| 978 | + .maxSequentialToolsInvocations(1) | ||
| 947 | .toolProvider(dynamicToolProvider) | 979 | .toolProvider(dynamicToolProvider) |
| 948 | .build(); | 980 | .build(); |
| 949 | UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent); | 981 | UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent); |
| 950 | } | 982 | } |
| 951 | return aiAgent; | 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 | // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== | 1065 | // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== |
| 955 | private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) { | 1066 | private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) { |
| 956 | - | ||
| 957 | // 1. 已选场景:强制绑定该场景工具 | 1067 | // 1. 已选场景:强制绑定该场景工具 |
| 958 | if (session.isSceneSelected() && session.getCurrentScene() != null) { | 1068 | if (session.isSceneSelected() && session.getCurrentScene() != null) { |
| 959 | dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); | 1069 | dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); |
| @@ -969,7 +1079,7 @@ public class XlyErpService { | @@ -969,7 +1079,7 @@ public class XlyErpService { | ||
| 969 | } | 1079 | } |
| 970 | // 4. 获取/创建用Agent | 1080 | // 4. 获取/创建用Agent |
| 971 | ErpAiAgent aiAgent = UserSceneSessionService.ERP_AGENT_CACHE.get(userId); | 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 | List<ToolSpecificationHolder> dataListOne = | 1083 | List<ToolSpecificationHolder> dataListOne = |
| 974 | dataList.stream().filter(m-> | 1084 | dataList.stream().filter(m-> |
| 975 | userInput.equals(m.getsMethodName().trim()) || ("查询"+userInput).equals(m.getsMethodName().trim()) | 1085 | userInput.equals(m.getsMethodName().trim()) || ("查询"+userInput).equals(m.getsMethodName().trim()) |
| @@ -992,9 +1102,10 @@ public class XlyErpService { | @@ -992,9 +1102,10 @@ public class XlyErpService { | ||
| 992 | .chatModel(chatModel) | 1102 | .chatModel(chatModel) |
| 993 | .chatMemoryProvider(operableChatMemoryProvider) | 1103 | .chatMemoryProvider(operableChatMemoryProvider) |
| 994 | .tools(executors,immediateReturnToolNames) | 1104 | .tools(executors,immediateReturnToolNames) |
| 1105 | + .maxSequentialToolsInvocations(1) | ||
| 995 | .build(); | 1106 | .build(); |
| 996 | UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent); | 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 | return aiAgent; | 1110 | return aiAgent; |
| 1000 | } | 1111 | } |
| @@ -1021,6 +1132,8 @@ public class XlyErpService { | @@ -1021,6 +1132,8 @@ public class XlyErpService { | ||
| 1021 | .chatModel(chatModel) | 1132 | .chatModel(chatModel) |
| 1022 | .chatMemoryProvider(operableChatMemoryProvider) | 1133 | .chatMemoryProvider(operableChatMemoryProvider) |
| 1023 | .tools(executors,immediateReturnToolNames) | 1134 | .tools(executors,immediateReturnToolNames) |
| 1135 | + // ★ 关键:防死循环兜底 | ||
| 1136 | + .maxSequentialToolsInvocations(1) | ||
| 1024 | .build(); | 1137 | .build(); |
| 1025 | return aiAgent; | 1138 | return aiAgent; |
| 1026 | } | 1139 | } |
| @@ -1073,7 +1186,6 @@ public class XlyErpService { | @@ -1073,7 +1186,6 @@ public class XlyErpService { | ||
| 1073 | UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); | 1186 | UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); |
| 1074 | // 清空Agent缓存 | 1187 | // 清空Agent缓存 |
| 1075 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); | 1188 | UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); |
| 1076 | - UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | ||
| 1077 | return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint(); | 1189 | return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint(); |
| 1078 | } | 1190 | } |
| 1079 | 1191 | ||
| @@ -1225,18 +1337,15 @@ public class XlyErpService { | @@ -1225,18 +1337,15 @@ public class XlyErpService { | ||
| 1225 | String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool()) | 1337 | String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool()) |
| 1226 | ? session.getCurrentTool().getSMethodName() : "随便聊聊"; | 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 | .map(chunk -> AiResponseDTO.builder() | 1349 | .map(chunk -> AiResponseDTO.builder() |
| 1241 | .sSceneName(sceneName) | 1350 | .sSceneName(sceneName) |
| 1242 | .sMethodName(methodName) | 1351 | .sMethodName(methodName) |
| @@ -1256,13 +1365,6 @@ public class XlyErpService { | @@ -1256,13 +1365,6 @@ public class XlyErpService { | ||
| 1256 | private AiResponseDTO getChatiAgent (String input,UserSceneSession session){ | 1365 | private AiResponseDTO getChatiAgent (String input,UserSceneSession session){ |
| 1257 | String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; | 1366 | String sceneName = ObjectUtil.isNotEmpty(session.getCurrentScene())?session.getCurrentScene().getSSceneName():StrUtil.EMPTY; |
| 1258 | String methodName = ObjectUtil.isNotEmpty(session.getCurrentTool())?session.getCurrentTool().getSMethodName():"随便聊聊"; | 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 | String sChatMessage = chatiAgent.chat(session.getUserId(), input); | 1368 | String sChatMessage = chatiAgent.chat(session.getUserId(), input); |
| 1267 | return AiResponseDTO.builder().sSceneName(sceneName).sMethodName(methodName).aiText(sChatMessage).systemText(StrUtil.EMPTY).sReturnType(ReturnTypeCode.HTML.getCode()).build(); | 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,7 +42,6 @@ import java.util.*; | ||
| 42 | 42 | ||
| 43 | import java.util.concurrent.ConcurrentHashMap; | 43 | import java.util.concurrent.ConcurrentHashMap; |
| 44 | import java.util.stream.Collectors; | 44 | import java.util.stream.Collectors; |
| 45 | -import java.util.stream.IntStream; | ||
| 46 | 45 | ||
| 47 | @Slf4j | 46 | @Slf4j |
| 48 | @Service | 47 | @Service |
| @@ -57,8 +56,10 @@ public class DynamicToolProvider implements ToolProvider { | @@ -57,8 +56,10 @@ public class DynamicToolProvider implements ToolProvider { | ||
| 57 | private final OperableChatMemoryProvider operableChatMemoryProvider; | 56 | private final OperableChatMemoryProvider operableChatMemoryProvider; |
| 58 | 57 | ||
| 59 | private final Map<String, ToolSpecificationHolder> toolCache = new ConcurrentHashMap<>(); | 58 | private final Map<String, ToolSpecificationHolder> toolCache = new ConcurrentHashMap<>(); |
| 59 | + public final List<ToolMeta> metaAll = new ArrayList<>(); | ||
| 60 | public final Map<String, String> sSceneIdMap = new ConcurrentHashMap<>(); | 60 | public final Map<String, String> sSceneIdMap = new ConcurrentHashMap<>(); |
| 61 | public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMap = new ConcurrentHashMap<>(); | 61 | public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMap = new ConcurrentHashMap<>(); |
| 62 | + public final Map<String, List<ToolSpecificationHolder>> sceneToolCacheMapFist = new ConcurrentHashMap<>(); | ||
| 62 | private final List<ParamRule> paramRuleDataAll = new ArrayList<>(); | 63 | private final List<ParamRule> paramRuleDataAll = new ArrayList<>(); |
| 63 | 64 | ||
| 64 | @Value("${erp.baseurl}") | 65 | @Value("${erp.baseurl}") |
| @@ -73,20 +74,31 @@ public class DynamicToolProvider implements ToolProvider { | @@ -73,20 +74,31 @@ public class DynamicToolProvider implements ToolProvider { | ||
| 73 | @jakarta.annotation.PostConstruct | 74 | @jakarta.annotation.PostConstruct |
| 74 | public void init() { | 75 | public void init() { |
| 75 | List<ToolMeta> metas = toolMetaMapper.findAll(); | 76 | List<ToolMeta> metas = toolMetaMapper.findAll(); |
| 77 | + metaAll.addAll(metas); | ||
| 76 | for (ToolMeta meta : metas) { | 78 | for (ToolMeta meta : metas) { |
| 77 | try { | 79 | try { |
| 78 | doSetToolAIshowfieldShow(meta); | 80 | doSetToolAIshowfieldShow(meta); |
| 79 | ToolSpecification spec = buildToolSpecification(meta); | 81 | ToolSpecification spec = buildToolSpecification(meta); |
| 80 | ToolExecutor executor = createToolExecutor(meta); | 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 | log.info("已加载动态工具:{}", meta.getSMethodNo()); | 84 | log.info("已加载动态工具:{}", meta.getSMethodNo()); |
| 83 | String sceneId = meta.getSSceneId(); | 85 | String sceneId = meta.getSSceneId(); |
| 84 | List<ToolSpecificationHolder> dataList = new ArrayList<>(); | 86 | List<ToolSpecificationHolder> dataList = new ArrayList<>(); |
| 85 | if(ObjectUtil.isNotEmpty(sceneToolCacheMap.get(sceneId))) { | 87 | if(ObjectUtil.isNotEmpty(sceneToolCacheMap.get(sceneId))) { |
| 86 | dataList = sceneToolCacheMap.get(sceneId); | 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 | sceneToolCacheMap.put(sceneId, dataList); | 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 | } catch (Exception e) { | 102 | } catch (Exception e) { |
| 91 | e.printStackTrace(); | 103 | e.printStackTrace(); |
| 92 | log.error("构建工具失败,sMethodNo={}", meta.getSMethodNo(), e); | 104 | log.error("构建工具失败,sMethodNo={}", meta.getSMethodNo(), e); |
| @@ -153,136 +165,147 @@ public class DynamicToolProvider implements ToolProvider { | @@ -153,136 +165,147 @@ public class DynamicToolProvider implements ToolProvider { | ||
| 153 | log.info("创建工具执行器: {}", meta.getSMethodNo()); | 165 | log.info("创建工具执行器: {}", meta.getSMethodNo()); |
| 154 | 166 | ||
| 155 | return (toolExecutionRequest, memoryId) -> { | 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 | // throw new IllegalStateException("STOP_INVOCATION: 任务已完成,停止调用"); | 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 | Map<String,Object> argOld = session.getArgs(); | 206 | Map<String,Object> argOld = session.getArgs(); |
| 191 | sDynamicValueOld = ObjectUtil.isNotEmpty(argOld.get("动态部件")) | 207 | sDynamicValueOld = ObjectUtil.isNotEmpty(argOld.get("动态部件")) |
| 192 | ? argOld.get("动态部件").toString():( ObjectUtil.isNotEmpty(argOld.get("sDynamicPart"))? argOld.get("sDynamicPart").toString():StrUtil.EMPTY); | 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 | // Map<String,Object> data = quoService.getQuoData( args, session); | 283 | // Map<String,Object> data = quoService.getQuoData( args, session); |
| 265 | - session.getCurrentTool().setDynamicParamRule(dynamicParamRule); | 284 | + session.getCurrentTool().setDynamicParamRule(dynamicParamRule); |
| 266 | // String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,dynamicParamRule); | 285 | // String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,dynamicParamRule); |
| 267 | // session.setSSystemPrompt(sSystemPrompt); | 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,6 +777,41 @@ public class DynamicToolProvider implements ToolProvider { | ||
| 754 | .build(); | 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 | private String getConstMeg(ParamRule paramRule){ | 815 | private String getConstMeg(ParamRule paramRule){ |
| 758 | if(!RuleCode.CONST.getCode().equals(paramRule.getSRule())){ | 816 | if(!RuleCode.CONST.getCode().equals(paramRule.getSRule())){ |
| 759 | return StrUtil.EMPTY; | 817 | return StrUtil.EMPTY; |
| @@ -832,7 +890,7 @@ public class DynamicToolProvider implements ToolProvider { | @@ -832,7 +890,7 @@ public class DynamicToolProvider implements ToolProvider { | ||
| 832 | private String getMissParamMsg(ToolMeta meta,UserSceneSession session, Map<String, Object> args){ | 890 | private String getMissParamMsg(ToolMeta meta,UserSceneSession session, Map<String, Object> args){ |
| 833 | if(meta.getBQuo()){ | 891 | if(meta.getBQuo()){ |
| 834 | Map<String,Object> data = quoService.getQuoData(args, session); | 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 | List<Map<String,Object>> sEmptyData = sAll.stream().filter(one-> BooleanUtil.toBoolean(one.get("bEmpty").toString()) | 894 | List<Map<String,Object>> sEmptyData = sAll.stream().filter(one-> BooleanUtil.toBoolean(one.get("bEmpty").toString()) |
| 837 | && (!Validator.isValid(one.get("sValue"))) | 895 | && (!Validator.isValid(one.get("sValue"))) |
| 838 | && !"sDynamicPart".equals(one.get("sName")) | 896 | && !"sDynamicPart".equals(one.get("sName")) |
| @@ -842,9 +900,10 @@ public class DynamicToolProvider implements ToolProvider { | @@ -842,9 +900,10 @@ public class DynamicToolProvider implements ToolProvider { | ||
| 842 | if(ObjectUtil.isNotEmpty(sEmptyData)){ | 900 | if(ObjectUtil.isNotEmpty(sEmptyData)){ |
| 843 | List<ParamRule> missData = session.getCurrentTool().getParamRuleListAll().stream().filter(d -> | 901 | List<ParamRule> missData = session.getCurrentTool().getParamRuleListAll().stream().filter(d -> |
| 844 | d.getBEmpty() | 902 | d.getBEmpty() |
| 903 | + && (ObjectUtil.isNotEmpty(args)) | ||
| 845 | && (!Validator.isValid(args.get(d.getSParam()))) | 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 | ).collect(Collectors.toUnmodifiableList()); | 907 | ).collect(Collectors.toUnmodifiableList()); |
| 849 | data.put("bTs",true); | 908 | data.put("bTs",true); |
| 850 | String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,missData); | 909 | String sSystemPrompt = SystemPromptGenerator.buildMissParamPrompt(session,missData); |
src/main/java/com/xly/tool/ToolSpecificationHolder.java
| @@ -9,11 +9,13 @@ public class ToolSpecificationHolder { | @@ -9,11 +9,13 @@ public class ToolSpecificationHolder { | ||
| 9 | private final ToolExecutor toolExecutor; | 9 | private final ToolExecutor toolExecutor; |
| 10 | private final String sName; | 10 | private final String sName; |
| 11 | private final String sMethodName; | 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 | this.toolSpecification = toolSpecification; | 14 | this.toolSpecification = toolSpecification; |
| 14 | this.toolExecutor = toolExecutor; | 15 | this.toolExecutor = toolExecutor; |
| 15 | this.sName = sName; | 16 | this.sName = sName; |
| 16 | this.sMethodName = sMethodName; | 17 | this.sMethodName = sMethodName; |
| 18 | + this.stoolDesc = stoolDesc; | ||
| 17 | } | 19 | } |
| 18 | 20 | ||
| 19 | public ToolSpecification getToolSpecification() { | 21 | public ToolSpecification getToolSpecification() { |
| @@ -31,5 +33,9 @@ public class ToolSpecificationHolder { | @@ -31,5 +33,9 @@ public class ToolSpecificationHolder { | ||
| 31 | public String getsMethodName() { | 33 | public String getsMethodName() { |
| 32 | return sMethodName; | 34 | return sMethodName; |
| 33 | } | 35 | } |
| 36 | + | ||
| 37 | + public String getStoolDesc() { | ||
| 38 | + return stoolDesc; | ||
| 39 | + } | ||
| 34 | } | 40 | } |
| 35 | 41 |