aps-agent/server/aps_domain/master_query.py

498 lines
22 KiB
Python
Raw Permalink 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
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)