SlotFillService.java
8.37 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package com.xly.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.agent.Intent;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 新增单据的**确定性槽位映射**:把意图门已抽好的「带角色实体」+ 正则解析的尺寸/数量,
* 按表单真实字段落位,产出 collectForm 的预填 JSON。
*
* <p>为什么不用 LLM 做这步:实测 qwen3:14b 能可靠区分「产品 vs 客户」(意图门已给出角色),
* 但对「长和宽50cm,高5cm」这类**尺寸拆分**始终出错(塞进备注/单价)。这类窄任务用 Java 正则 +
* 角色映射远比让模型受约束填槽可靠——把不确定的判断交给能确定处理的代码,是治『不智能』的关键手法之一。
*/
@Service
public class SlotFillService {
private final ObjectMapper mapper;
public SlotFillService(ObjectMapper mapper) {
this.mapper = mapper;
}
// 标注式尺寸:"长和宽50"、"高5cm"、"长:50"
private static final Pattern DIM_LABELED =
Pattern.compile("([长宽高厚深](?:[和与、,,及/][长宽高厚深])*)\\s*[::是为]?\\s*(\\d+(?:\\.\\d+)?)");
// 位置式尺寸:"50*30*5"、"50x30x5"、"50×30×5"
private static final Pattern DIM_POS =
Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*[*xX×]\\s*(\\d+(?:\\.\\d+)?)(?:\\s*[*xX×]\\s*(\\d+(?:\\.\\d+)?))?");
/**
* 依据意图门的角色实体 + 正则,确定性地构建新增表单的预填字段(中文字段名 -> 值)。
*
* @param utterance 用户原话(用于解析尺寸)
* @param entity 单据/实体类型(如 报价 / 客户)
* @param fields {@code FormResolverService.businessFields}(含 label/fk/type)
* @param it 意图门结果(提供带角色的实体)
*/
public Map<String, String> buildCreateFields(String utterance, String entity,
List<Map<String, Object>> fields, Intent it) {
Map<String, String> known = new LinkedHashMap<>();
if (fields == null || fields.isEmpty()) {
return known;
}
// 干净字段名 -> 原始 label(供尺寸/数量按名定位)
Map<String, String> cleanToLabel = new LinkedHashMap<>();
for (Map<String, Object> f : fields) {
String label = str(f.get("label"));
if (label != null && !label.isBlank()) {
cleanToLabel.putIfAbsent(cleanLabel(label), label);
}
}
String cust = it == null ? null : it.firstValueByRole("客户");
String prod = it == null ? null : it.firstValueByRole("产品");
String mat = it == null ? null : it.firstValueByRole("物料");
String sup = it == null ? null : it.firstValueByRole("供应商");
// 1) 外键角色字段:客户/产品/物料/供应商 → 对应 FK 字段
for (Map<String, Object> f : fields) {
String fk = str(f.get("fk"));
String label = str(f.get("label"));
if (fk == null || label == null) continue;
if ("elecustomer".equals(fk) && cust != null) known.putIfAbsent(label, cust);
else if ("eleproduct".equals(fk) && prod != null) known.putIfAbsent(label, prod);
else if ("elematerials".equals(fk) && mat != null) known.putIfAbsent(label, mat);
else if ("elesupplier".equals(fk) && sup != null) known.putIfAbsent(label, sup);
}
// 2) 数量:意图门角色=数量 → 只留数字
String qty = it == null ? null : it.firstValueByRole("数量");
if (qty != null) {
String lbl = cleanToLabel.get("数量");
if (lbl != null) {
String d = qty.replaceAll("[^0-9.]", "");
if (!d.isEmpty()) known.putIfAbsent(lbl, d);
}
}
// 3) 尺寸:正则从原话解析 长/宽/高
Map<String, String> dims = parseDimensions(utterance);
for (Map.Entry<String, String> e : dims.entrySet()) {
String lbl = cleanToLabel.get(e.getKey()); // "长"/"宽"/"高"
if (lbl != null) known.putIfAbsent(lbl, e.getValue());
}
// 4) 名称字段:新增某实体本身(如"新增客户叫X")时,实体名要落到它的名称字段
// (客户表本身没有 elecustomer 外键列,故不会被上面的 FK 环填上,需在此补)。
String nameLabel = findNameField(fields);
if (nameLabel != null && !known.containsKey(nameLabel)) {
String selfRole = danjuToRole(entity);
String nameVal = (selfRole != null && it != null) ? it.firstValueByRole(selfRole) : null;
if (nameVal == null && cust == null && prod == null && mat == null && sup == null) {
nameVal = firstEntityValue(it); // 兜底:单据类型不含角色词时用首个实体
}
if (nameVal != null) {
known.put(nameLabel, nameVal);
}
}
return known;
}
/** 单据/实体类型 → 它自身的业务角色(新增该实体时,该角色的实体值即其名称)。 */
private static String danjuToRole(String entity) {
if (entity == null) return null;
if (entity.contains("客户")) return "客户";
if (entity.contains("供应商")) return "供应商";
if (entity.contains("物料")) return "物料";
if (entity.contains("产品")) return "产品";
return null;
}
/** 解析尺寸 → {长,宽,高} 的数字(cm/mm 单位忽略,只取数值)。 */
public Map<String, String> parseDimensions(String utterance) {
Map<String, String> out = new LinkedHashMap<>();
if (utterance == null || utterance.isBlank()) return out;
Matcher m = DIM_LABELED.matcher(utterance);
boolean any = false;
while (m.find()) {
String labels = m.group(1);
String val = m.group(2);
for (int i = 0; i < labels.length(); i++) {
char c = labels.charAt(i);
String key = dimKey(c);
if (key != null) {
out.put(key, val);
any = true;
}
}
}
if (!any) {
Matcher p = DIM_POS.matcher(utterance);
if (p.find()) {
out.put("长", p.group(1));
out.put("宽", p.group(2));
if (p.group(3) != null) out.put("高", p.group(3));
}
}
return out;
}
private static String dimKey(char c) {
switch (c) {
case '长': return "长";
case '宽': return "宽";
case '高':
case '厚':
case '深': return "高";
default: return null;
}
}
/** 名称字段:优先 label 以「名称」结尾且非外键的字段;否则第一个 label 含「名称」的。 */
private static String findNameField(List<Map<String, Object>> fields) {
for (Map<String, Object> f : fields) {
String label = str(f.get("label"));
String fk = str(f.get("fk"));
if (label != null && label.endsWith("名称") && (fk == null || fk.isBlank())) {
return label;
}
}
for (Map<String, Object> f : fields) {
String label = str(f.get("label"));
if (label != null && label.contains("名称")) return label;
}
return null;
}
private static String firstEntityValue(Intent it) {
if (it == null) return null;
for (Intent.Entity e : it.entities) {
if (e != null && e.value != null && !e.value.isBlank()) return e.value.trim();
}
return null;
}
/** 序列化为 knownFieldsJson,供 {@code FormCollectTool.collectForm} 预填。 */
public String toJson(Map<String, String> known) {
try {
return mapper.writeValueAsString(known);
} catch (Exception e) {
return "{}";
}
}
private static String cleanLabel(String s) {
if (s == null) return "";
return s.replaceAll("[((].*?[))]", "").replaceAll("\\s+", "").trim();
}
private static String str(Object o) {
return o == null ? null : o.toString();
}
}