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}