001/* 002 * Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.agentsflex.core.prompt; 017 018import com.agentsflex.core.util.MapUtil; 019import com.alibaba.fastjson2.JSON; 020import com.alibaba.fastjson2.JSONPath; 021import com.alibaba.fastjson2.JSONWriter; 022 023import java.util.*; 024import java.util.concurrent.ConcurrentHashMap; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028/** 029 * 文本模板引擎,用于将包含 {{xxx}} 占位符的字符串模板,动态渲染为最终文本。 030 * 支持 JSONPath 取值语法与 “??” 空值兜底逻辑。 031 * <p> 032 * 例如: 033 * 模板: "Hello {{ user.name ?? 'Unknown' }}!" 034 * 数据: { "user": { "name": "Alice" } } 035 * 输出: "Hello Alice!" 036 * <p> 037 * 支持缓存模板与 JSONPath 编译结果,提升性能。 038 */ 039public class PromptTemplate { 040 041 /** 042 * 匹配 {{ expression }} 的正则表达式 043 */ 044 private static final Pattern PLACEHOLDER_PATTERN = 045 Pattern.compile("\\{\\{\\s*([^{}]+?)\\s*}}"); 046 047 /** 048 * 模板缓存(按原始模板字符串) 049 */ 050 private static final Map<String, PromptTemplate> TEMPLATE_CACHE = new ConcurrentHashMap<>(); 051 052 /** 053 * JSONPath 编译缓存,避免重复编译 054 */ 055 private static final Map<String, JSONPath> JSONPATH_CACHE = new ConcurrentHashMap<>(); 056 057 /** 058 * 原始模板字符串 059 */ 060 private final String originalTemplate; 061 062 /** 063 * 模板中拆分出的静态与动态 token 列表 064 */ 065 private final List<TemplateToken> tokens; 066 067 public PromptTemplate(String template) { 068 this.originalTemplate = template != null ? template : ""; 069 this.tokens = Collections.unmodifiableList(parseTemplate(this.originalTemplate)); 070 } 071 072 /** 073 * 从缓存中获取或新建模板实例 074 */ 075 public static PromptTemplate of(String template) { 076 String finalTemplate = template != null ? template : ""; 077 return MapUtil.computeIfAbsent(TEMPLATE_CACHE, finalTemplate, k -> new PromptTemplate(finalTemplate)); 078 } 079 080 /** 081 * 清空模板与 JSONPath 缓存 082 */ 083 public static void clearCache() { 084 TEMPLATE_CACHE.clear(); 085 JSONPATH_CACHE.clear(); 086 } 087 088 089 /** 090 * 将模板格式化为字符串 091 */ 092 public String format(Map<String, Object> rootMap) { 093 return format(rootMap, false); 094 } 095 096 /** 097 * 将模板格式化为字符串,可选择是否对结果进行 JSON 转义 098 * 099 * @param rootMap 数据上下文 100 * @param escapeForJsonOutput 是否对结果进行 JSON 字符串转义 101 */ 102 public String format(Map<String, Object> rootMap, boolean escapeForJsonOutput) { 103 if (tokens.isEmpty()) return originalTemplate; 104 if (rootMap == null) rootMap = Collections.emptyMap(); 105 106 StringBuilder sb = new StringBuilder(originalTemplate.length() + 64); 107 108 for (TemplateToken token : tokens) { 109 if (token.isStatic) { 110 // 静态文本,直接拼接 111 sb.append(token.content); 112 continue; 113 } 114 115 // 动态表达式求值 116 String value = evaluate(token.parseResult, rootMap, escapeForJsonOutput); 117 118 // 没有兜底且值为空时抛出异常 119 if (!token.explicitEmptyFallback && value.isEmpty()) { 120 throw new IllegalArgumentException(String.format( 121 "Missing value for expression: \"%s\"%nTemplate: %s%nProvided parameters:%n%s", 122 token.rawExpression, 123 originalTemplate, 124 JSON.toJSONString(rootMap, JSONWriter.Feature.PrettyFormat) 125 )); 126 } 127 sb.append(value); 128 } 129 130 return sb.toString(); 131 } 132 133 /** 134 * 解析模板字符串,将其拆解为静态文本与动态占位符片段 135 */ 136 private List<TemplateToken> parseTemplate(String template) { 137 List<TemplateToken> result = new ArrayList<>(template.length() / 8); 138 if (template == null || template.isEmpty()) return result; 139 140 Matcher matcher = PLACEHOLDER_PATTERN.matcher(template); 141 int lastEnd = 0; 142 143 while (matcher.find()) { 144 int start = matcher.start(); 145 int end = matcher.end(); 146 147 // 处理 {{ 前的静态文本 148 if (start > lastEnd) { 149 result.add(TemplateToken.staticText(template.substring(lastEnd, start))); 150 } 151 152 // 处理 {{ ... }} 动态部分 153 String rawExpr = matcher.group(1); 154 TemplateParseResult parsed = parseTemplateExpression(rawExpr); 155 result.add(TemplateToken.dynamic(parsed.parseResult, rawExpr, parsed.explicitEmptyFallback)); 156 157 lastEnd = end; 158 } 159 160 // 末尾剩余静态文本 161 if (lastEnd < template.length()) { 162 result.add(TemplateToken.staticText(template.substring(lastEnd))); 163 } 164 165 return result; 166 } 167 168 /** 169 * 解析单个表达式内容,处理 ?? 空值兜底逻辑。 170 * 例如: user.name ?? user.nick ?? "未知" 171 */ 172 private TemplateParseResult parseTemplateExpression(String expr) { 173 // 无 ?? 表示该值必填 174 if (!expr.contains("??")) { 175 return new TemplateParseResult(new ParseResult(expr.trim(), null), false); 176 } 177 178 // 按 ?? 分割,支持链式兜底 179 String[] parts = expr.split("\\s*\\?\\?\\s*", -1); 180 boolean explicitEmptyFallback = parts[parts.length - 1].trim().isEmpty(); 181 182 // 从右往左构建兜底链 183 ParseResult result = null; 184 for (int i = parts.length - 1; i >= 0; i--) { 185 String p = parts[i].trim(); 186 if (p.isEmpty()) p = "\"\""; // 空串转为 "" 字面量 187 result = new ParseResult(p, result); 188 } 189 190 return new TemplateParseResult(result, explicitEmptyFallback); 191 } 192 193 /** 194 * 递归求值表达式(支持多级兜底) 195 */ 196 private String evaluate(ParseResult pr, Map<String, Object> root, boolean escapeForJsonOutput) { 197 if (pr == null) return ""; 198 199 // 字面量直接返回 200 if (pr.isLiteral) { 201 String literal = pr.getUnquotedLiteral(); 202 return escapeForJsonOutput ? escapeJsonString(literal) : literal; 203 } 204 205 // 尝试从 JSONPath 取值 206 Object value = getValueByJsonPath(root, pr.expression, escapeForJsonOutput); 207 if (value != null) { 208 return value.toString(); 209 } 210 211 // 若未取到,则尝试 fallback 212 return evaluate(pr.defaultResult, root, escapeForJsonOutput); 213 } 214 215 /** 216 * 根据 JSONPath 获取对象值 217 */ 218 private Object getValueByJsonPath(Map<String, Object> root, String path, boolean escapeForJsonOutput) { 219 try { 220 String fullPath = path.startsWith("$") ? path : "$." + path; 221 JSONPath compiled = MapUtil.computeIfAbsent(JSONPATH_CACHE, fullPath, JSONPath::compile); 222 Object value = compiled.eval(root); 223 if (escapeForJsonOutput && value instanceof String) { 224 return escapeJsonString((String) value); 225 } 226 return value; 227 } catch (Exception ignored) { 228 return null; 229 } 230 } 231 232 /** 233 * 将字符串进行 JSON 安全转义 234 */ 235 private static String escapeJsonString(String input) { 236 if (input == null || input.isEmpty()) return input; 237 return input 238 .replace("\\", "\\\\") 239 .replace("\"", "\\\"") 240 .replace("\b", "\\b") 241 .replace("\f", "\\f") 242 .replace("\n", "\\n") 243 .replace("\r", "\\r") 244 .replace("\t", "\\t"); 245 } 246 247 /** 248 * 去掉字符串两端的引号 249 */ 250 private static String unquote(String str) { 251 if (str == null || str.length() < 2) return str; 252 char first = str.charAt(0); 253 char last = str.charAt(str.length() - 1); 254 if ((first == '\'' && last == '\'') || (first == '"' && last == '"')) { 255 return str.substring(1, str.length() - 1); 256 } 257 return str; 258 } 259 260 261 /** 262 * 模板片段对象。 263 * 每个模板字符串会被解析为若干个 TemplateToken: 264 * - 静态文本(isStatic = true) 265 * - 动态表达式(isStatic = false) 266 */ 267 private static class TemplateToken { 268 final boolean isStatic; // 是否为静态文本 269 final String content; // 静态文本内容 270 final ParseResult parseResult; // 动态解析结果(表达式树) 271 final String rawExpression; // 原始表达式字符串 272 final boolean explicitEmptyFallback; // 是否显式声明空兜底(以 ?? 结尾) 273 274 private TemplateToken(boolean isStatic, String content, 275 ParseResult parseResult, String rawExpression, 276 boolean explicitEmptyFallback) { 277 this.isStatic = isStatic; 278 this.content = content; 279 this.parseResult = parseResult; 280 this.rawExpression = rawExpression; 281 this.explicitEmptyFallback = explicitEmptyFallback; 282 } 283 284 /** 285 * 创建静态文本 token 286 */ 287 static TemplateToken staticText(String text) { 288 return new TemplateToken(true, text, null, null, false); 289 } 290 291 /** 292 * 创建动态表达式 token 293 */ 294 static TemplateToken dynamic(ParseResult parseResult, String rawExpression, boolean explicitEmptyFallback) { 295 return new TemplateToken(false, null, parseResult, rawExpression, explicitEmptyFallback); 296 } 297 } 298 299 /** 300 * 表达式解析结果。 301 * 支持嵌套的默认值链,如:user.name ?? user.nick ?? "匿名" 302 */ 303 private static class ParseResult { 304 final String expression; // 当前表达式内容(可能是 JSONPath 或字符串字面量) 305 final ParseResult defaultResult; // 默认值链的下一个节点 306 final boolean isLiteral; // 是否为字面量字符串('xxx' 或 "xxx") 307 308 ParseResult(String expression, ParseResult defaultResult) { 309 this.expression = expression; 310 this.defaultResult = defaultResult; 311 this.isLiteral = isLiteralExpression(expression); 312 } 313 314 /** 315 * 判断是否是字符串字面量 316 */ 317 private static boolean isLiteralExpression(String expr) { 318 if (expr == null || expr.length() < 2) return false; 319 char first = expr.charAt(0); 320 char last = expr.charAt(expr.length() - 1); 321 return (first == '\'' && last == '\'') || (first == '"' && last == '"'); 322 } 323 324 /** 325 * 返回去除引号后的字符串字面量值 326 */ 327 String getUnquotedLiteral() { 328 if (!isLiteral) throw new IllegalStateException("Not a literal: " + expression); 329 return unquote(expression); 330 } 331 } 332 333 /** 334 * 模板解析的最终结果,包含: 335 * - 解析后的表达式树(ParseResult) 336 * - 是否显式声明空兜底 337 */ 338 private static class TemplateParseResult { 339 final ParseResult parseResult; 340 final boolean explicitEmptyFallback; 341 342 TemplateParseResult(ParseResult parseResult, boolean explicitEmptyFallback) { 343 this.parseResult = parseResult; 344 this.explicitEmptyFallback = explicitEmptyFallback; 345 } 346 } 347}