KgQueryTool.java
6.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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();
}
}