KgQueryTool.java 6.81 KB
package com.xly.tool;

import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

/**
 * 知识图谱查询工具(架构 §5 #3 KgSearch / §11)。两个只读能力:
 * <ul>
 *   <li>{@link #findForms} —— 按关键词检索 ERP 业务表单目录(视图 {@code viw_ai_useful_forms},
 *       1748 张有用业务表单),即「表单名 → formId/moduleId」定位;</li>
 *   <li>{@link #kgSearch} —— L2 单据流转/相邻单据({@code viw_kg_form_neighbors})与
 *       L3 字段→表/列({@code viw_kg_field_dict}),按词查询、结果截断,绝不整表 dump。</li>
 * </ul>
 *
 * <p>表单目录是**全局元数据**(对所有品牌一致),不涉及行级/租户数据,因此不需要租户过滤。
 */
@Component
public class KgQueryTool {

    private final JdbcTemplate jdbc;

    public KgQueryTool(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    @Tool("按关键词检索 ERP 业务表单目录,返回匹配的表单名、底层数据源、所属菜单id(moduleId) 与表单id(formId)。"
            + "当用户提到某类单据或报表、但你不确定具体是哪一张表单时,先用它来定位。最多返回 15 条。")
    public String findForms(
            @P("表单名或业务关键词,例如:报价 / 客户 / 库存 / 送货 / 应收 / 采购订单") String keyword) {

        if (keyword == null || keyword.isBlank()) {
            return "请提供一个表单名或业务关键词再检索。";
        }
        String kw = keyword.trim();
        String like = "%" + kw + "%";

        List<Map<String, Object>> rows = jdbc.queryForList(
                "SELECT af.sFormTitle, af.sDataSource, af.sExecType, af.sModuleId, af.sFormId " +
                        "FROM viw_ai_useful_forms af " +
                        "LEFT JOIN viw_kg_form f ON f.sFormId = af.sFormId " +
                        "WHERE af.sFormTitle LIKE ? " +
                        "ORDER BY COALESCE(f.bAiTool,0) DESC, (af.sExecType='table') DESC, " +
                        "(COALESCE(f.iUpstream,0)+COALESCE(f.iDownstream,0)) DESC, CHAR_LENGTH(af.sFormTitle) ASC " +
                        "LIMIT 12",
                like);

        if (rows.isEmpty()) {
            return "没有找到名称包含「" + kw + "」的业务表单。可以换个更常见的说法,或告诉我更具体的单据名称。";
        }

        StringBuilder sb = new StringBuilder();
        sb.append("找到 ").append(rows.size()).append(" 张与「").append(kw).append("」相关的业务表单:\n\n");
        sb.append("| 表单 | 数据源(").append("表/视图/存储过程) | 菜单id | 表单id |\n");
        sb.append("|---|---|---|---|\n");
        for (Map<String, Object> r : rows) {
            sb.append("| ").append(str(r.get("sFormTitle")))
                    .append(" | ").append(str(r.get("sDataSource"))).append(" · ").append(str(r.get("sExecType")))
                    .append(" | ").append(str(r.get("sModuleId")))
                    .append(" | ").append(str(r.get("sFormId")))
                    .append(" |\n");
        }
        return sb.toString();
    }

    @Tool("查知识图谱(KG):某表单的**上下游流转/相邻单据**(L2)与某字段**在哪些表/列**(L3)。"
            + "用于弄清「这张单从哪来、到哪去、和谁相关」或「某个字段落在哪张表」。入参 = 表单名或字段中文名关键词。")
    public String kgSearch(@P("表单名或字段中文名关键词,如 采购订单 / 送货 / 单价 / 数量") String keyword) {
        if (keyword == null || keyword.isBlank()) {
            return "请提供一个表单名或字段中文名关键词。";
        }
        String kw = keyword.trim();
        String like = "%" + kw + "%";
        StringBuilder sb = new StringBuilder();

        // L2:表单邻居 / 上下游流转
        List<Map<String, Object>> flow = safeQuery(
                "SELECT sFormTitle, sDomain, iUpstream, iDownstream, sUpForms, sDownForms, sRefTables " +
                        "FROM viw_kg_form_neighbors WHERE sFormTitle LIKE ? " +
                        "ORDER BY (COALESCE(iUpstream,0)+COALESCE(iDownstream,0)) DESC LIMIT 5", like);
        if (!flow.isEmpty()) {
            sb.append("【单据流转 / 相邻单据】\n");
            for (Map<String, Object> r : flow) {
                sb.append("- ").append(str(r.get("sFormTitle")))
                        .append("(域:").append(str(r.get("sDomain"))).append(")");
                String up = clip(str(r.get("sUpForms")), 80);
                String down = clip(str(r.get("sDownForms")), 80);
                if (!up.isBlank()) sb.append("\n    上游←:").append(up);
                if (!down.isBlank()) sb.append("\n    下游→:").append(down);
                String ref = clip(str(r.get("sRefTables")), 80);
                if (!ref.isBlank()) sb.append("\n    引用表:").append(ref);
                sb.append('\n');
            }
        }

        // L3:字段 -> 表/列(按词查,绝不整表 dump)
        List<Map<String, Object>> fields = safeQuery(
                "SELECT sTable, sField, sChinese, sFkTable FROM viw_kg_field_dict " +
                        "WHERE sChinese LIKE ? AND sTable NOT LIKE 'viw%' " +
                        "ORDER BY iFormUses DESC LIMIT 8", like);
        if (!fields.isEmpty()) {
            sb.append("\n【字段所在表/列】\n");
            for (Map<String, Object> r : fields) {
                sb.append("- ").append(str(r.get("sChinese")))
                        .append(" = ").append(str(r.get("sTable"))).append(".").append(str(r.get("sField")));
                String fk = str(r.get("sFkTable"));
                if (fk != null && !fk.isBlank() && !"null".equalsIgnoreCase(fk)) {
                    sb.append("(外键→").append(fk).append(")");
                }
                sb.append('\n');
            }
        }

        if (sb.length() == 0) {
            return "知识图谱里没有与「" + kw + "」直接相关的流转或字段。可以换个更常见的单据名或字段名。";
        }
        return sb.toString();
    }

    private List<Map<String, Object>> safeQuery(String sql, Object... args) {
        try {
            return jdbc.queryForList(sql, args);
        } catch (Exception e) {
            return List.of();
        }
    }

    private static String clip(String s, int max) {
        if (s == null) {
            return "";
        }
        String t = s.trim();
        if ("null".equalsIgnoreCase(t)) {
            return "";
        }
        return t.length() > max ? t.substring(0, max) + "…" : t;
    }

    private static String str(Object o) {
        return o == null ? "" : o.toString();
    }
}