aps-agent/server/aps_domain/master_query.py

498 lines
22 KiB
Python
Raw Normal View History

# ============================================================
# 主数据/订单自然语言只读查询(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)