SlotFillService.java 8.37 KB
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();
    }
}