Commit ac642a35612e5bf48a5024fef39dff6d6b2cfcc1

Authored by zichun
1 parent 1057c7df

fix(quote): don't guess customer; collectForm-first + prefill known values

Bug: '我要报价纸盒' -> agent put product '纸盒' into 客户名称, FK-lookup failed, then asked to CREATE a customer named 纸盒.

- System prompt rule 6: 新增报价 ALWAYS collectForm first; never map product/spec/盒型 to 客户名称;
  unknown customer -> let user pick from dropdown, never invent or offer to create.
- collectForm(entityKeyword, knownFieldsJson?): pre-fills the form from what the user already said
  (产品/数量/尺寸); customer/product render as dropdowns to pick real records.
- FK-not-found message no longer implies creating (was '请确认是否已存在' -> LLM read as 'create it?');
  now '不是系统里已有的X,请从下拉选真实存在的X,不要新建'.

Verified: '我要报价纸盒,1000个,长和宽50cm,高5cm' -> collectForm with 产品名称=纸盒(prefilled),
长/宽=50/高=5/数量=1000(prefilled), 客户名称=empty dropdown (no more guessing/create-customer).
src/main/java/com/xly/service/SystemPromptService.java
@@ -48,9 +48,12 @@ public class SystemPromptService { @@ -48,9 +48,12 @@ public class SystemPromptService {
48 - queryData(question):用**只读 SQL** 回答没有现成表单能直接答的临时统计 / 分析(跨表汇总、计数、排名、分组);\ 48 - queryData(question):用**只读 SQL** 回答没有现成表单能直接答的临时统计 / 分析(跨表汇总、计数、排名、分组);\
49 仅当 readFormData / lookupRecord 都答不了时才用。 49 仅当 readFormData / lookupRecord 都答不了时才用。
50 - askUser(question, options?):意图不明或需二选一时,向用户提一个带选项的小问题;问完即等用户回答。 50 - askUser(question, options?):意图不明或需二选一时,向用户提一个带选项的小问题;问完即等用户回答。
51 - - collectForm(entityKeyword):新增字段较多的单据(如报价)时,弹一张表单让用户一次填齐;提交后你再用 proposeCreate。 51 + - collectForm(entityKeyword, knownFieldsJson?):新增字段较多的单据(尤其**报价**)时,弹一张表单让用户一次填齐。\
  52 + 把用户已说的信息作为 knownFieldsJson 预填(字段中文名->值,如 {"产品名称":"纸盒","数量":"1000","长(L)":"50"}),\
  53 + 客户/产品/物料等会渲染成下拉让用户从真实数据里选。**新增报价一律先用它**,提交后你再用 proposeCreate。
52 - proposeCreate(entityKeyword, fieldsJson):**提议新增**一条记录(写操作,只提议、暂不执行;用户确认后才新增)。\ 54 - proposeCreate(entityKeyword, fieldsJson):**提议新增**一条记录(写操作,只提议、暂不执行;用户确认后才新增)。\
53 - fieldsJson 是已知字段的 JSON(字段中文名->值),如 {"客户名称":"常州测试公司"};主键与必填项会自动补齐。 55 + fieldsJson 是已知字段的 JSON(字段中文名->值),如 {"客户名称":"常州测试公司"};主键与必填项会自动补齐。\
  56 + **仅当字段(尤其客户/产品)都由用户明确给出、无需猜测时才直接用它**;否则先 collectForm。
54 - proposeUpdate(entityKeyword, recordKeyword, fieldChinese, newValue):**提议**修改某条记录的某个字段(写操作,只提议、暂不执行)。 57 - proposeUpdate(entityKeyword, recordKeyword, fieldChinese, newValue):**提议**修改某条记录的某个字段(写操作,只提议、暂不执行)。
55 - proposeDelete(entityKeyword, recordKeyword):**提议删除**某条记录(写操作,只提议、暂不执行)。删除不可恢复,慎用。 58 - proposeDelete(entityKeyword, recordKeyword):**提议删除**某条记录(写操作,只提议、暂不执行)。删除不可恢复,慎用。
56 - proposeExamine(entityKeyword, recordKeyword):**提议审核/过账**某张单据(写操作,只提议、暂不执行)。审核有业务后果,慎用。 59 - proposeExamine(entityKeyword, recordKeyword):**提议审核/过账**某张单据(写操作,只提议、暂不执行)。审核有业务后果,慎用。
@@ -67,6 +70,9 @@ public class SystemPromptService { @@ -67,6 +70,9 @@ public class SystemPromptService {
67 它们都**只生成待确认提议、不立即执行**;真正的写入要用户在对话内点【确认】才发生,你**绝不能声称已经完成**。 70 它们都**只生成待确认提议、不立即执行**;真正的写入要用户在对话内点【确认】才发生,你**绝不能声称已经完成**。
68 5. 用户问某类数据的数量 / 概况 / 某条记录时,**直接用工具读取并如实汇报**,不要无谓反问;\ 71 5. 用户问某类数据的数量 / 概况 / 某条记录时,**直接用工具读取并如实汇报**,不要无谓反问;\
69 只有确实缺少关键参数(如不知道要查哪张单据)时才用 askUser 提问。 72 只有确实缺少关键参数(如不知道要查哪张单据)时才用 askUser 提问。
  73 + 6. **新增报价**("报价X"/"给X报价"/"新增报价")**一律先 collectForm**,把用户说的产品/数量/尺寸作为 knownFieldsJson 预填,\
  74 + 客户/产品让用户在下拉里选真实数据。**绝不要把产品名、盒型、规格猜成客户名称**——"报价纸盒"里"纸盒"是产品不是客户;\
  75 + 客户未知就交给表单让用户选,不要自己编、也不要因为找不到就提议新建客户。
70 """.formatted(renderDomainMap(), renderSkills()); 76 """.formatted(renderDomainMap(), renderSkills());
71 } 77 }
72 78
src/main/java/com/xly/tool/FormCollectTool.java
@@ -9,6 +9,8 @@ import dev.langchain4j.agent.tool.Tool; @@ -9,6 +9,8 @@ import dev.langchain4j.agent.tool.Tool;
9 import org.springframework.jdbc.core.JdbcTemplate; 9 import org.springframework.jdbc.core.JdbcTemplate;
10 10
11 import java.util.ArrayList; 11 import java.util.ArrayList;
  12 +import java.util.HashMap;
  13 +import java.util.Iterator;
12 import java.util.LinkedHashMap; 14 import java.util.LinkedHashMap;
13 import java.util.List; 15 import java.util.List;
14 import java.util.Map; 16 import java.util.Map;
@@ -42,12 +44,18 @@ public class FormCollectTool { @@ -42,12 +44,18 @@ public class FormCollectTool {
42 this.mapper = mapper; 44 this.mapper = mapper;
43 } 45 }
44 46
45 - @Tool("在对话里弹出一张 ERP 表单让用户一次性填齐多个字段(而不是逐个追问)。用于字段较多的新建/录入场景"  
46 - + "(如新建报价、新建客户)。入参 = 实体/单据类型(如 报价 / 客户)。用户填完提交后,你再用 proposeCreate 生成待确认的新增。")  
47 - public String collectForm(@P("要新建的实体/单据类型,如 报价 / 客户 / 物料") String entityKeyword) { 47 + @Tool("在对话里弹出一张 ERP 表单让用户一次性填齐多个字段(而不是逐个追问、更不是自己猜)。用于字段较多的新建/录入场景"
  48 + + "(尤其**新建报价**)。entityKeyword = 单据类型(如 报价 / 客户);knownFieldsJson = 用户已说的字段(中文名->值)用于预填"
  49 + + "(如 {\"产品名称\":\"纸盒\",\"数量\":\"1000\",\"长(L)\":\"50\"})。客户/产品/物料会渲染成下拉让用户从真实数据里选。"
  50 + + "用户填完提交后,你再用 proposeCreate 生成待确认的新增。")
  51 + public String collectForm(
  52 + @P("要新建的实体/单据类型,如 报价 / 客户 / 物料") String entityKeyword,
  53 + @P(value = "可选:用户已说的字段 JSON(中文名->值),用于预填表单,如 {\"产品名称\":\"纸盒\",\"数量\":\"1000\"}",
  54 + required = false) String knownFieldsJson) {
48 if (entityKeyword == null || entityKeyword.isBlank()) { 55 if (entityKeyword == null || entityKeyword.isBlank()) {
49 return err("缺少实体类型。"); 56 return err("缺少实体类型。");
50 } 57 }
  58 + Map<String, String> known = parseKnown(knownFieldsJson);
51 Map<String, Object> form = resolver.resolveMasterForm(entityKeyword.trim()); 59 Map<String, Object> form = resolver.resolveMasterForm(entityKeyword.trim());
52 if (form == null) { 60 if (form == null) {
53 return err("找不到「" + entityKeyword + "」对应的可新建表单。"); 61 return err("找不到「" + entityKeyword + "」对应的可新建表单。");
@@ -93,6 +101,10 @@ public class FormCollectTool { @@ -93,6 +101,10 @@ public class FormCollectTool {
93 if (hint != null && !String.valueOf(hint).isBlank()) { 101 if (hint != null && !String.valueOf(hint).isBlank()) {
94 f.put("hint", String.valueOf(hint)); 102 f.put("hint", String.valueOf(hint));
95 } 103 }
  104 + String pre = known.get(normLabel(String.valueOf(f.get("label"))));
  105 + if (pre != null && !pre.isBlank()) {
  106 + f.put("default", pre); // 预填用户已说的值
  107 + }
96 fields.add(f); 108 fields.add(f);
97 } 109 }
98 110
@@ -204,6 +216,34 @@ public class FormCollectTool { @@ -204,6 +216,34 @@ public class FormCollectTool {
204 return o == null ? null : o.toString(); 216 return o == null ? null : o.toString();
205 } 217 }
206 218
  219 + /** 解析预填 JSON(中文名->值),键归一化(去括号/空白)。 */
  220 + private Map<String, String> parseKnown(String json) {
  221 + Map<String, String> m = new HashMap<>();
  222 + if (json == null || json.isBlank()) {
  223 + return m;
  224 + }
  225 + try {
  226 + com.fasterxml.jackson.databind.JsonNode n = mapper.readTree(json.trim());
  227 + Iterator<Map.Entry<String, com.fasterxml.jackson.databind.JsonNode>> it = n.fields();
  228 + while (it.hasNext()) {
  229 + Map.Entry<String, com.fasterxml.jackson.databind.JsonNode> e = it.next();
  230 + String v = e.getValue().asText("");
  231 + if (v != null && !v.isBlank()) {
  232 + m.put(normLabel(e.getKey()), v);
  233 + }
  234 + }
  235 + } catch (Exception ignore) {
  236 + }
  237 + return m;
  238 + }
  239 +
  240 + private static String normLabel(String s) {
  241 + if (s == null) {
  242 + return "";
  243 + }
  244 + return s.replaceAll("[((].*?[))]", "").replaceAll("\\s+", "").trim();
  245 + }
  246 +
207 private String err(String msg) { 247 private String err(String msg) {
208 Map<String, Object> m = new LinkedHashMap<>(); 248 Map<String, Object> m = new LinkedHashMap<>();
209 m.put("error", msg); 249 m.put("error", msg);
src/main/java/com/xly/tool/ProposeWriteTool.java
@@ -252,7 +252,9 @@ public class ProposeWriteTool { @@ -252,7 +252,9 @@ public class ProposeWriteTool {
252 if (fk != null && !fk.isBlank()) { 252 if (fk != null && !fk.isBlank()) {
253 String id = resolver.resolveFk(fk, v); // 外键:名称 -> id 253 String id = resolver.resolveFk(fk, v); // 外键:名称 -> id
254 if (id == null) { 254 if (id == null) {
255 - return err("找不到名为「" + v + "」的" + zh + ",请确认该" + zh + "是否已存在。"); 255 + String ent = zh.replace("名称", "");
  256 + return err("「" + v + "」不是系统里已有的" + ent + "。请让用户从下拉里选真实存在的"
  257 + + ent + ";**不要**因此新建" + ent + "。");
256 } 258 }
257 col.put(colName, id); 259 col.put(colName, id);
258 } else { 260 } else {
@@ -421,7 +423,9 @@ public class ProposeWriteTool { @@ -421,7 +423,9 @@ public class ProposeWriteTool {
421 if (fk != null && !fk.isBlank()) { 423 if (fk != null && !fk.isBlank()) {
422 String id = resolver.resolveFk(fk, v); 424 String id = resolver.resolveFk(fk, v);
423 if (id == null) { 425 if (id == null) {
424 - return err("找不到名为「" + v + "」的" + zh + ",请确认是否已存在。"); 426 + String ent = zh.replace("名称", "");
  427 + return err("「" + v + "」不是系统里已有的" + ent + "。请用 collectForm 让用户从下拉里选真实存在的"
  428 + + ent + ";**不要**因此新建" + ent + ",也不要把产品/规格当成客户。");
425 } 429 }
426 value = id; 430 value = id;
427 if ("sCustomerId".equals(colName)) { 431 if ("sCustomerId".equals(colName)) {