package com.xly.tool;
import com.xly.agent.AgentIdentity;
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.expression.StringValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.util.TablesNamesFinder;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Query 工具:**只读 SQL 兜底** —— 回答没有现成表单/记录能直接答的临时统计/分析问题
* (跨表汇总、按条件计数排名等)。
*
*
安全栈:用 coder 模型据 KG 字段字典接地生成 SQL → jsqlparser 强制**单条 SELECT** →
* 挡 {@code INTO OUTFILE / LOAD_FILE / information_schema / SLEEP / BENCHMARK} 与多语句 →
* 强制 LIMIT。(本地单品牌,租户注入留作生产加固;见架构 §9。)SQL 入审计。
*/
public class QueryTool {
private final OllamaChatModel sqlModel;
private final JdbcTemplate jdbc;
private final AuditService audit;
private final AgentIdentity identity;
public QueryTool(OllamaChatModel sqlModel, JdbcTemplate jdbc, AuditService audit, AgentIdentity identity) {
this.sqlModel = sqlModel;
this.jdbc = jdbc;
this.audit = audit;
this.identity = identity;
}
@Tool("用**只读 SQL** 回答没有现成表单/记录能直接答的临时统计或分析问题"
+ "(如跨表汇总、按条件计数、排名、分组统计)。仅在 readFormData / lookupRecord 无法回答时才用。")
public String queryData(@P("用自然语言描述要统计/分析什么") String question) {
if (question == null || question.isBlank()) {
return "请描述要查询统计的内容。";
}
String hint = schemaHint(question);
String sql = null;
String lastErr = null;
// 自修复重试:SQL 校验/执行报错就把错误喂回模型重新生成,最多 3 次
for (int attempt = 0; attempt < 3; attempt++) {
try {
sql = cleanSql(sqlModel.chat(buildPrompt(hint, question, sql, lastErr)));
} catch (Exception e) {
return "生成查询失败:" + e.getMessage();
}
String reject = validate(sql);
if (reject != null) {
lastErr = reject;
if (attempt < 2) {
continue;
}
audit.log(null, null, "query", "REJECTED", sql, false, reject);
return "无法安全执行该查询(" + reject + ")。可以换个更具体的问法。";
}
String limited = forceLimit(applyTenant(sql));
try {
List