# ============================================================ # 主数据/订单自然语言只读查询(moduleId: domain-master-query, 可重生 ✅) # 让对话能检索 world 中的销售订单、物料、工序、BOM、工艺路线、柔性资源。 # ============================================================ from __future__ import annotations from typing import Any from server.engines.queries import find_bom_items, find_routing_steps World = dict[str, Any] ENTITIES = ("order", "material", "operation", "bom", "routing", "overview", "flex", "mrp") CODE_REQUIRED_ENTITIES = frozenset({"order", "material", "operation", "bom", "routing"}) MRP_ASPECTS = frozenset({"outsource", "purchase", "all"}) def _input_error(message: str, *, entity: str | None = None) -> dict[str, Any]: return { "ok": False, "entity": entity, "error": message, "count": 0, "rows": [], } def _find_product(world: World, code_or_name: str | None) -> dict | None: mats = world.get("materials") or [] if not code_or_name: return None key = code_or_name.strip().lower() for m in mats: if str(m.get("code", "")).lower() == key: return m if str(m.get("name", "")).lower() == key: return m # flex 成品 for m in world.get("flexMaterials") or []: if m.get("type") == "FINISHED_PRODUCT" and ( str(m.get("code", "")).lower() == key or str(m.get("name", "")).lower() == key ): return {"id": None, "code": m["code"], "name": m["name"], "type": "FINISHED_PRODUCT", "_flex": True} return None def query_orders(world: World, code: str | None = None, limit: int = 20) -> dict[str, Any]: rows = [] for so in world.get("salesOrders") or []: if code and code not in str(so.get("orderNo", "")) and code not in str(so.get("customerName", "")): continue item = (so.get("items") or [{}])[0] rows.append({ "orderNo": so.get("orderNo"), "status": so.get("status"), "customer": so.get("customerName"), "level": so.get("customerLevel"), "productCode": item.get("productCode"), "productName": item.get("productName"), "quantity": item.get("quantity"), "deliveryDate": so.get("deliveryDate") or item.get("deliveryDate"), "wbs": so.get("wbs") or "", "special": so.get("specialRequirements") or "", }) # 柔性订单补充(若固定轨无命中) if not rows or (code and not any(r["orderNo"] == code for r in rows)): for fo in world.get("flexOrders") or []: if code and code not in str(fo.get("orderNo", "")): continue rows.append({ "orderNo": fo.get("orderNo"), "status": fo.get("status"), "customer": fo.get("project") or "柔性订单", "level": "", "productCode": fo.get("productCode"), "productName": fo.get("productCode"), "quantity": fo.get("quantity"), "deliveryDate": fo.get("dueDate"), "wbs": fo.get("wbs") or "", "special": "flex", }) return {"entity": "order", "count": len(rows), "rows": rows[:limit]} def query_materials(world: World, code: str | None = None, limit: int = 40) -> dict[str, Any]: rows = [] for m in world.get("materials") or []: if code: c = code.lower() if c not in str(m.get("code", "")).lower() and c not in str(m.get("name", "")).lower(): continue rows.append({ "code": m.get("code"), "name": m.get("name"), "type": m.get("type"), "unit": m.get("unit"), "stock": m.get("stock"), "inTransit": m.get("inTransit"), "spec": m.get("spec") or "", }) if not rows: for m in world.get("flexMaterials") or []: if code: c = code.lower() if c not in str(m.get("code", "")).lower() and c not in str(m.get("name", "")).lower(): continue rows.append({ "code": m.get("code"), "name": m.get("name"), "type": m.get("type"), "unit": m.get("unit"), "stock": m.get("stock"), "inTransit": m.get("inTransit"), "spec": "", }) return {"entity": "material", "count": len(rows), "rows": rows[:limit]} def query_operations(world: World, code: str | None = None, limit: int = 40) -> dict[str, Any]: rows = [] ops = world.get("operations") or [] if not ops: ops = [ {"code": o.get("code"), "name": o.get("name"), "type": "INTERNAL", "standardTime": o.get("changeoverMin")} for o in (world.get("flexOperations") or []) ] for o in ops: if code: c = code.lower() if c not in str(o.get("code", "")).lower() and c not in str(o.get("name", "")).lower(): continue rows.append({ "code": o.get("code"), "name": o.get("name"), "type": o.get("type"), "standardTime": o.get("standardTime"), }) return {"entity": "operation", "count": len(rows), "rows": rows[:limit]} def query_bom(world: World, code: str | None = None, limit: int = 50) -> dict[str, Any]: product = _find_product(world, code) if not product: return {"entity": "bom", "count": 0, "rows": [], "hint": "未找到成品,请带料号如 28200003654300"} if product.get("_flex") or product.get("id") is None: pc = product["code"] rows = [] mats = {m["code"]: m for m in (world.get("flexMaterials") or [])} for b in world.get("flexBom") or []: if b.get("productCode") != pc: continue mat = mats.get(b.get("materialCode") or "", {}) rows.append({ "materialCode": b.get("materialCode"), "materialName": mat.get("name") or b.get("materialCode"), "quantity": b.get("quantity"), "consumeOp": b.get("consumeOp"), "isKey": b.get("isKey"), }) return {"entity": "bom", "productCode": pc, "productName": product.get("name"), "count": len(rows), "rows": rows[:limit]} pid = product["id"] mats = {m["id"]: m for m in world.get("materials") or []} ops = {o["id"]: o for o in world.get("operations") or []} rows = [] for it in find_bom_items(world, pid): mat = mats.get(it["materialId"], {}) op = ops.get(it.get("operationId"), {}) rows.append({ "materialCode": mat.get("code"), "materialName": mat.get("name"), "quantity": it.get("quantity"), "consumeOp": op.get("code") or op.get("name"), "isKey": it.get("isKeyMaterial"), }) return {"entity": "bom", "productCode": product.get("code"), "productName": product.get("name"), "count": len(rows), "rows": rows[:limit]} def query_routing(world: World, code: str | None = None, limit: int = 40) -> dict[str, Any]: product = _find_product(world, code) if not product: return {"entity": "routing", "count": 0, "rows": [], "hint": "未找到成品"} if product.get("_flex") or product.get("id") is None: pc = product["code"] ops = {o["code"]: o for o in (world.get("flexOperations") or [])} rows = [] for r in sorted((world.get("flexRoutings") or []), key=lambda x: x.get("seq", 0)): if r.get("productCode") != pc: continue op = ops.get(r.get("operationCode") or "", {}) rows.append({ "seq": r.get("seq"), "operationCode": r.get("operationCode"), "operationName": op.get("name") or r.get("operationCode"), "stdTimePerUnit": r.get("stdTimePerUnit"), "requireMold": r.get("requireMold"), }) return {"entity": "routing", "productCode": pc, "productName": product.get("name"), "count": len(rows), "rows": rows[:limit]} pid = product["id"] ops = {o["id"]: o for o in world.get("operations") or []} rows = [] for s in find_routing_steps(world, pid): op = ops.get(s["operationId"], {}) rows.append({ "seq": s.get("sequenceNo"), "operationCode": op.get("code"), "operationName": op.get("name"), "setupTime": s.get("setupTime"), "runTimePerUnit": s.get("runTimePerUnit"), "isExternal": s.get("isExternal"), }) return {"entity": "routing", "productCode": product.get("code"), "productName": product.get("name"), "count": len(rows), "rows": rows[:limit]} def query_overview(world: World) -> dict[str, Any]: factories = [f.get("name") for f in (world.get("factories") or [])] return { "entity": "overview", "factory": factories[0] if factories else "(无)", "counts": { "factories": len(world.get("factories") or []), "lines": len(world.get("lines") or []), "workstations": len(world.get("workstations") or []), "materials": len(world.get("materials") or []), "operations": len(world.get("operations") or []), "boms": len(world.get("boms") or []), "routings": len(world.get("routings") or []), "salesOrders": len(world.get("salesOrders") or []), "flexOrders": len(world.get("flexOrders") or []), "flexEquipment": len(world.get("flexEquipment") or []), }, } def query_flex(world: World, limit: int = 20) -> dict[str, Any]: return { "entity": "flex", "orders": [ {"orderNo": o.get("orderNo"), "productCode": o.get("productCode"), "qty": o.get("quantity"), "due": o.get("dueDate")} for o in (world.get("flexOrders") or [])[:limit] ], "equipment": [ {"code": e.get("code"), "name": e.get("name"), "zone": e.get("zone"), "caps": len(e.get("capabilities") or [])} for e in (world.get("flexEquipment") or [])[:limit] ], "operations": [ {"code": o.get("code"), "name": o.get("name"), "bottleneck": o.get("isBottleneck")} for o in (world.get("flexOperations") or [])[:limit] ], } def query_mrp(world: World, order_no: str | None = None, *, aspect: str | None = None, text: str = "") -> dict[str, Any]: """查某单(或全部)的采购/委外建议,并核对工艺是否有外协步骤。""" del text # 兼容旧签名;禁止从原始自然语言推断 aspect。 if aspect not in MRP_ASPECTS: return _input_error( "请明确采购/委外查询范围:aspect 必须为 outsource、purchase 或 all。", entity="mrp", ) order_no = (order_no or "").strip() or None def _match_so(row: dict) -> bool: if not order_no: return True return str(row.get("salesOrderNo") or "") == order_no purchase = [r for r in (world.get("purchaseOrders") or []) if _match_so(r)] outsource = [r for r in (world.get("outsourceOrders") or []) if _match_so(r)] # 工艺外协核对(即使尚未分解也能答「有没有委外」) external_steps: list[dict] = [] product_code = None if order_no: so = next((s for s in (world.get("salesOrders") or []) if str(s.get("orderNo")) == order_no), None) fo = next((o for o in (world.get("flexOrders") or []) if str(o.get("orderNo")) == order_no), None) product_id = None if so: item = (so.get("items") or [{}])[0] product_code = item.get("productCode") product_id = item.get("productId") if product_id is None and product_code: mat = next((m for m in (world.get("materials") or []) if m.get("code") == product_code), None) product_id = mat["id"] if mat else None elif fo: product_code = fo.get("productCode") mat = next((m for m in (world.get("materials") or []) if m.get("code") == product_code), None) product_id = mat["id"] if mat else None for s in (world.get("flexRoutings") or []): if s.get("productCode") == product_code and s.get("isExternal"): external_steps.append({ "seq": s.get("seq"), "operationCode": s.get("operationCode"), "operationName": s.get("operationName") or s.get("operationCode"), }) if product_id is not None and not external_steps: for step in find_routing_steps(world, product_id): if step.get("isExternal"): op = next((o for o in (world.get("operations") or []) if o.get("id") == step.get("operationId")), {}) external_steps.append({ "seq": step.get("sequenceNo"), "operationCode": op.get("code"), "operationName": op.get("name") or op.get("code"), }) return { "entity": "mrp", "aspect": aspect, "orderNo": order_no, "productCode": product_code, "purchase": [ {"orderNo": r.get("orderNo"), "materialCode": r.get("materialCode"), "materialName": r.get("materialName"), "quantity": r.get("quantity"), "status": r.get("status"), "suggestedOrderDate": r.get("suggestedOrderDate")} for r in purchase ], "outsource": [ {"orderNo": r.get("orderNo"), "operationName": r.get("operationName"), "quantity": r.get("quantity"), "status": r.get("status"), "requiredDate": r.get("requiredDate")} for r in outsource ], "externalSteps": external_steps, "decomposed": bool(purchase or outsource), } def run_master_query(world: World, *, entity: str | None = None, code: str | None = None, text: str = "", limit: int = 40, aspect: str | None = None) -> dict[str, Any]: del text # 兼容旧 caller;原始 query 不再参与实体、编码或 MRP 语义推断。 ent = str(entity or "").strip().lower() if not ent: return _input_error( "请明确要查询的主数据类型:order、material、operation、bom、routing、overview、flex 或 mrp。" ) if ent not in ENTITIES: return _input_error( "主数据类型不受支持,请从 order、material、operation、bom、routing、overview、flex、mrp 中选择。", entity=ent, ) normalized_code = str(code or "").strip() if ent in CODE_REQUIRED_ENTITIES and not normalized_code: return _input_error( f"请提供要查询的 {ent} 编码,不能从原始问句自动猜测。", entity=ent, ) code = normalized_code or None if ent == "order": return query_orders(world, code, limit=limit) if ent == "material": return query_materials(world, code, limit=limit) if ent == "operation": return query_operations(world, code, limit=limit) if ent == "bom": return query_bom(world, code, limit=limit) if ent == "routing": return query_routing(world, code, limit=limit) if ent == "flex": return query_flex(world, limit=limit) if ent == "mrp": if aspect not in MRP_ASPECTS: return _input_error( "请明确采购/委外查询范围:aspect 必须为 outsource、purchase 或 all。", entity="mrp", ) return query_mrp(world, code, aspect=aspect) return query_overview(world) def format_master_query(result: dict[str, Any]) -> str: """把查询结果格式化为对话可读文本。""" if result.get("ok") is False: return str(result.get("error") or "缺少必要查询参数,请补充后重试。") ent = result.get("entity") if ent == "overview": c = result.get("counts") or {} return ( f"主数据概览 · 工厂【{result.get('factory')}】\n" f"· 产线 {c.get('lines', 0)} / 工位 {c.get('workstations', 0)}\n" f"· 物料 {c.get('materials', 0)} / 工序 {c.get('operations', 0)} / " f"BOM {c.get('boms', 0)} / 路线 {c.get('routings', 0)}\n" f"· 销售订单 {c.get('salesOrders', 0)} / 柔性订单 {c.get('flexOrders', 0)} / " f"柔性设备 {c.get('flexEquipment', 0)}\n" "可继续说:「查订单」「查物料」「查 BOM 28200003654300」「查工艺路线」。" ) if ent == "order": rows = result.get("rows") or [] if not rows: return "未找到匹配订单。可说「有哪些订单」或带订单号如「查订单 102285668」。" lines = [f"订单共 {result.get('count')} 条:"] for r in rows: lines.append( f"· {r['orderNo']} [{r.get('status')}] {r.get('productName') or r.get('productCode')}" f" ×{r.get('quantity')} 交期 {r.get('deliveryDate')} " f"客户 {r.get('customer')}" + (f" WBS={r['wbs']}" if r.get("wbs") else "") ) return "\n".join(lines) if ent == "material": rows = result.get("rows") or [] if not rows: return "未找到匹配物料。" lines = [f"物料共 {result.get('count')} 条(展示前 {len(rows)}):"] for r in rows: lines.append( f"· {r['code']} {r.get('name')} [{r.get('type')}] " f"库存 {r.get('stock')}{r.get('unit') or ''}" ) return "\n".join(lines) if ent == "operation": rows = result.get("rows") or [] if not rows: return "工序库为空。" lines = [f"工序共 {result.get('count')} 条:"] for r in rows: st = r.get("standardTime") lines.append(f"· {r['code']} {r.get('name')} 标准工时={st}") return "\n".join(lines) if ent == "bom": if result.get("hint") and not result.get("rows"): return result["hint"] rows = result.get("rows") or [] head = f"产品 {result.get('productCode')} {result.get('productName') or ''} BOM · {result.get('count')} 项:" lines = [head] for r in rows: key = "★" if r.get("isKey") else " " lines.append( f"·{key} {r.get('materialCode')} {r.get('materialName')} ×{r.get('quantity')}" + (f" @{r.get('consumeOp')}" if r.get("consumeOp") else "") ) return "\n".join(lines) if rows else head + "(空)" if ent == "routing": if result.get("hint") and not result.get("rows"): return result["hint"] rows = result.get("rows") or [] head = f"产品 {result.get('productCode')} 工艺路线 · {result.get('count')} 步:" lines = [head] for r in rows: t = r.get("runTimePerUnit") or r.get("stdTimePerUnit") lines.append( f"· seq={r.get('seq')} {r.get('operationCode')} {r.get('operationName')} " f"单件 {t} 分" ) return "\n".join(lines) if rows else head + "(空)" if ent == "flex": lines = ["柔性资源:"] for o in result.get("orders") or []: lines.append(f"· 订单 {o['orderNo']} {o['productCode']} ×{o['qty']} 交期 {o['due']}") for e in result.get("equipment") or []: lines.append(f"· 设备 {e['code']} {e['name']} 区={e.get('zone')} 能力数={e.get('caps')}") for o in result.get("operations") or []: flag = "★瓶颈" if o.get("bottleneck") else "" lines.append(f"· 工序 {o['code']} {o['name']} {flag}") return "\n".join(lines) if len(lines) > 1 else "柔性资源为空。" if ent == "mrp": order_no = result.get("orderNo") or "全部订单" aspect = result.get("aspect") or "all" purchase = result.get("purchase") or [] outsource = result.get("outsource") or [] external = result.get("externalSteps") or [] lines = [f"订单 {order_no} 的采购/委外情况:"] if aspect in ("outsource", "all"): if outsource: lines.append(f"· 委外建议:{len(outsource)} 条") for r in outsource[:8]: lines.append( f" - {r.get('orderNo')} {r.get('operationName')} ×{r.get('quantity')} " f"[{r.get('status')}] 需求日 {r.get('requiredDate')}" ) elif external: lines.append( f"· 委外建议:尚未分解,但工艺有 {len(external)} 道外协步骤" f"({ '、'.join(str(s.get('operationName') or s.get('operationCode')) for s in external[:5]) })。" "可说「分解订单」生成委外建议单。" ) else: lines.append("· 委外建议:无。工艺路线未标记外协步骤,当前没有委外订单。") if aspect in ("purchase", "all"): if purchase: lines.append(f"· 采购建议:{len(purchase)} 条") for r in purchase[:8]: lines.append( f" - {r.get('orderNo')} {r.get('materialName') or r.get('materialCode')} " f"×{r.get('quantity')} [{r.get('status')}] 建议下单 {r.get('suggestedOrderDate')}" ) else: tip = "(可先说「分解订单」生成建议)" if not result.get("decomposed") else "" lines.append(f"· 采购建议:无{tip}") if aspect == "outsource" and not outsource and not external: lines.append("结论:这个订单没有委外。") elif aspect == "outsource" and (outsource or external): lines.append("结论:这个订单涉及委外。") return "\n".join(lines) return str(result)