package com.xly.tool;
import com.xly.service.AuditService;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.model.ollama.OllamaChatModel;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.Select;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* Query 工具:**只读 SQL 兜底** —— 回答没有现成表单/记录能直接答的临时统计/分析问题
* (跨表汇总、按条件计数排名等)。
*
*
安全栈:用 coder 模型据 KG 字段字典接地生成 SQL → jsqlparser 强制**单条 SELECT** →
* 挡 {@code INTO OUTFILE / LOAD_FILE / information_schema / SLEEP / BENCHMARK} 与多语句 →
* 强制 LIMIT。(本地单品牌,租户注入留作生产加固;见架构 §9。)SQL 入审计。
*/
@Component
public class QueryTool {
private final OllamaChatModel sqlModel;
private final JdbcTemplate jdbc;
private final AuditService audit;
public QueryTool(@Qualifier("sqlChatModel") OllamaChatModel sqlModel, JdbcTemplate jdbc, AuditService audit) {
this.sqlModel = sqlModel;
this.jdbc = jdbc;
this.audit = audit;
}
@Tool("用**只读 SQL** 回答没有现成表单/记录能直接答的临时统计或分析问题"
+ "(如跨表汇总、按条件计数、排名、分组统计)。仅在 readFormData / lookupRecord 无法回答时才用。")
public String queryData(@P("用自然语言描述要统计/分析什么") String question) {
if (question == null || question.isBlank()) {
return "请描述要查询统计的内容。";
}
String hint = schemaHint(question);
String prompt = """
你是 MySQL 专家。根据【问题】生成 **一条** MySQL SELECT 查询来回答它。
数据库 = xlyweberp_saas。可用的表和字段(列名=中文名):
%s
规则:只用 SELECT(严禁任何写操作 / 文件操作);需要时 JOIN;务必带合适的 LIMIT(<=100);
**列别名一律用英文**(如 cnt、total、name),ORDER BY 用英文列名或序号,**绝不要用中文做别名**;
表名、列名一律用上面给定的英文名。**只输出 SQL 本身**,不要解释、不要 markdown 代码围栏。
问题:%s
""".formatted(hint, question);
String sql;
try {
sql = cleanSql(sqlModel.chat(prompt));
} catch (Exception e) {
return "生成查询失败:" + e.getMessage();
}
String reject = validate(sql);
if (reject != null) {
audit.log(null, null, "query", "REJECTED", sql, false, reject);
return "无法安全执行该查询(" + reject + ")。可以换个更具体的问法。";
}
sql = forceLimit(sql);
try {
List