aps-agent/server/aps_domain/master_query.py

501 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# 主数据/订单自然语言只读查询(moduleId: domain-master-query, 可重生 ✅)
# 让对话能检索 world 中的销售订单、物料、工序、BOM、工艺路线、柔性资源。
# ============================================================
from __future__ import annotations
import re
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")
def detect_entity(text: str) -> str:
t = text or ""
if re.search(r"概览|主数据总览|资源树|工厂有什么", t):
return "overview"
if re.search(r"委外|采购建议|分解建议|外协|MRP建议", t, re.I):
return "mrp"
if re.search(r"柔性|能力池|虚拟产线|flex", t, re.I):
return "flex"
if re.search(r"BOM|物料清单|用料|子件", t, re.I):
return "bom"
if re.search(r"工艺路线|路线步骤|工序流|怎么排工序", t):
return "routing"
if re.search(r"工序库|有哪些工序|查工序|工序编码", t):
return "operation"
if re.search(r"物料|成品|原料|库存|料号", t):
return "material"
if re.search(r"订单|交期|客户|WBS", t):
return "order"
return "overview"
def extract_code(text: str) -> str | None:
"""抽订单号 / 物料码 / 工序码。"""
t = text or ""
m = re.search(r"(SO\d{6,}|\d{6,}|FO-?\d{3,}|(?:CTRL|PCB|HV|PDU)-[A-Z0-9\-]+|"
r"[0-9A-Za-z]{6,}Z1M|[LM]\d{6,}|\d{10,})", t, re.I)
if m:
return m.group(1)
m2 = re.search(r"(?:订单|物料|料号|工序|产品)\s*[::]?\s*([0-9A-Za-z\-]{4,})", t)
return m2.group(1) if m2 else None
def _find_product(world: World, code_or_name: str | None) -> dict | None:
mats = world.get("materials") or []
if not code_or_name:
return next((m for m in mats if m.get("type") == "FINISHED_PRODUCT"), None)
key = code_or_name.strip().lower()
for m in mats:
if str(m.get("code", "")).lower() == key:
return m
if key in str(m.get("name", "")).lower():
return m
# flex 成品
for m in world.get("flexMaterials") or []:
if m.get("type") == "FINISHED_PRODUCT" and (
str(m.get("code", "")).lower() == key or key in str(m.get("name", "")).lower()
):
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 _mrp_aspect(text: str) -> str:
t = text or ""
if re.search(r"委外|外协", t) and not re.search(r"采购", t):
return "outsource"
if re.search(r"采购", t) and not re.search(r"委外|外协", t):
return "purchase"
return "all"
def query_mrp(world: World, order_no: str | None = None, *, aspect: str = "all",
text: str = "") -> dict[str, Any]:
"""查某单(或全部)的采购/委外建议,并核对工艺是否有外协步骤。"""
aspect = aspect if aspect in ("outsource", "purchase", "all") else _mrp_aspect(text)
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]:
ent = entity or detect_entity(text)
if ent not in ENTITIES:
ent = "overview"
code = code or extract_code(text)
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":
return query_mrp(world, code, aspect=aspect or _mrp_aspect(text), text=text)
return query_overview(world)
def format_master_query(result: dict[str, Any]) -> str:
"""把查询结果格式化为对话可读文本。"""
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)