aps-agent/server/knowledge/routing_templates.py

317 lines
16 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: knowledge-routing-templates, 可重生 ✅)
# machining_patterns 的文本知识 → 结构化模板(SQLite routing_templates 表):
# - RAG 回答「怎么加工」(文本),模板库供向导「直接生成路线」(结构化)
# - 知识文档与模板互相引用:template.asset_title ↔ 知识资产标题
# 面向整个机加工行业:车/铣/钻镗/磨/钣金/焊接/装配/城轨装配。
# ============================================================
from __future__ import annotations
from typing import Any
from sqlalchemy import select
from server.auth.context import get_identity
# 内置模板定义:steps = (seq, 工序码, 工序名, 设备类型, 工时下限, 上限, setup, changeover)
_BUILTIN: list[dict[str, Any]] = [
{
"code": "TPL-TURNING", "name": "车削类零件(轴/套/盘)", "category": "车削",
"keywords": "车削,轴,套,盘,销,外圆,螺纹,回转体",
"description": "回转体零件典型路线:下料→车端面→粗车→半精车→切槽/螺纹→精车→去毛刺→终检",
"asset_title": "车削工艺路线模式",
"steps": [
(10, "OP-BLANK", "下料", "锯床/剪板机", 2, 5, 10, 5),
(20, "OP-FACE", "车端面打中心孔", "卧式车床", 3, 8, 15, 10),
(30, "OP-ROUGH-TURN", "粗车外圆", "数控车床", 5, 15, 20, 15),
(40, "OP-SEMI-TURN", "半精车", "数控车床", 4, 10, 15, 10),
(50, "OP-GROOVE", "切槽/车螺纹", "数控车床", 3, 8, 15, 10),
(60, "OP-FINISH-TURN", "精车", "数控车床", 4, 12, 20, 15),
(70, "OP-DEBURR", "去毛刺", "钳工台", 2, 5, 5, 0),
(80, "OP-INSPECT", "终检", "检验台", 3, 6, 5, 0),
],
},
{
"code": "TPL-MILLING", "name": "铣削类零件(箱体/支架/平板)", "category": "铣削",
"keywords": "铣削,箱体,支架,平板,型腔,平面,壳体",
"description": "箱体支架类典型路线:毛坯→粗铣基准面→粗铣各面→半精→精铣→钻孔攻丝→去毛刺→终检",
"asset_title": "铣削工艺路线模式",
"steps": [
(10, "OP-BLANK", "下料/毛坯", "锯床", 2, 5, 10, 5),
(20, "OP-ROUGH-MILL-REF", "粗铣基准面", "立式加工中心", 8, 20, 25, 15),
(30, "OP-ROUGH-MILL", "粗铣各面/槽", "立式加工中心", 10, 30, 20, 15),
(40, "OP-SEMI-MILL", "半精铣", "立式加工中心", 8, 20, 15, 10),
(50, "OP-FINISH-MILL", "精铣", "立式加工中心", 8, 25, 20, 15),
(60, "OP-DRILL-TAP", "钻孔攻丝", "钻攻中心", 5, 15, 15, 10),
(70, "OP-DEBURR", "去毛刺", "钳工台", 3, 6, 5, 0),
(80, "OP-INSPECT", "终检", "三坐标/检验台", 5, 10, 5, 0),
],
},
{
"code": "TPL-HOLE", "name": "孔系加工(钻/镗/铰)", "category": "钻镗",
"keywords": "钻孔,镗孔,铰孔,攻丝,孔系,深孔",
"description": "孔系典型路线:钻中心孔→钻→扩→铰/镗→锪窝→攻丝;高精度孔粗镗→半精镗→精镗",
"asset_title": "钻镗孔系工艺模式",
"steps": [
(10, "OP-CENTER", "钻中心孔", "钻床", 1, 3, 10, 5),
(20, "OP-DRILL", "钻孔", "摇臂钻/加工中心", 2, 8, 10, 8),
(30, "OP-REAM-BORE", "铰孔/镗孔", "镗床/加工中心", 3, 10, 15, 10),
(40, "OP-TAP", "攻丝", "钻攻中心", 1, 4, 10, 5),
(50, "OP-INSPECT", "孔位检验", "检验台", 2, 5, 5, 0),
],
},
{
"code": "TPL-GRINDING", "name": "磨削精加工", "category": "磨削",
"keywords": "磨削,精磨,外圆磨,平面磨,淬火,超精",
"description": "热处理后精加工:校直→粗磨→半精磨→精磨→(超精/研磨)→检验;磨削常为瓶颈",
"asset_title": "磨削与超精工艺模式",
"steps": [
(10, "OP-STRAIGHTEN", "校直", "校直机", 2, 5, 10, 5),
(20, "OP-ROUGH-GRIND", "粗磨", "外圆磨床", 5, 12, 25, 20),
(30, "OP-SEMI-GRIND", "半精磨", "外圆磨床", 4, 10, 20, 15),
(40, "OP-FINISH-GRIND", "精磨", "外圆磨床", 5, 15, 25, 20),
(50, "OP-INSPECT", "形位检验", "圆度仪/检验台", 3, 8, 5, 0),
],
},
{
"code": "TPL-SHEETMETAL", "name": "钣金冲压", "category": "钣金",
"keywords": "钣金,冲压,折弯,冲裁,拉伸,板料",
"description": "板料典型路线:开料→冲裁→折弯→整形→去毛刺→表面处理",
"asset_title": "钣金冲压工艺模式",
"steps": [
(10, "OP-SHEAR", "开料", "剪板机", 1, 3, 10, 5),
(20, "OP-PUNCH", "冲裁/冲孔", "冲床", 0.5, 2, 20, 15),
(30, "OP-BEND", "折弯", "折弯机", 1, 4, 15, 10),
(40, "OP-REFORM", "整形", "整形模/压机", 1, 3, 10, 8),
(50, "OP-DEBURR", "去毛刺", "打磨台", 1, 3, 5, 0),
(60, "OP-SURFACE", "表面处理", "喷涂/电镀线", 3, 10, 15, 0),
],
},
{
"code": "TPL-WELDING", "name": "焊接结构件", "category": "焊接",
"keywords": "焊接,结构件,坡口,组对,探伤,焊缝",
"description": "下料→坡口→组对点固→焊接→矫正→探伤→机加接口→涂装;先焊后机加保证接口精度",
"asset_title": "焊接结构件工艺模式",
"steps": [
(10, "OP-CUT", "下料/坡口", "等离子切割/铣边机", 3, 8, 15, 10),
(20, "OP-FIT", "组对点固", "组对平台", 5, 15, 20, 10),
(30, "OP-WELD", "焊接", "焊接工位/机器人", 10, 40, 25, 20),
(40, "OP-CORRECT", "矫正", "火焰/机械矫正", 3, 10, 10, 5),
(50, "OP-NDT", "探伤", "UT/MT 探伤", 5, 15, 10, 0),
(60, "OP-MACHINE-IF", "机加接口", "加工中心", 8, 20, 25, 15),
],
},
{
"code": "TPL-ASSEMBLY", "name": "机械装配(部装→总装)", "category": "装配",
"keywords": "装配,部装,总装,机电,调试,测试",
"description": "零部件齐套→部装→分总成→总装→电气接线→功能测试→终检→包装;部装常为瓶颈",
"asset_title": "机械装配工艺模式",
"steps": [
(10, "OP-KIT", "齐套检验", "备料区", 5, 10, 5, 0),
(20, "OP-SUBASSY", "部装", "装配台", 30, 60, 15, 15),
(30, "OP-ASSY-1", "总装一段", "装配台", 20, 45, 10, 10),
(40, "OP-ASSY-2", "总装二段", "装配台", 20, 45, 10, 10),
(50, "OP-WIRE", "电气接线", "接线工位", 10, 30, 10, 5),
(60, "OP-TEST", "功能测试", "测试台", 10, 25, 15, 5),
(70, "OP-FINAL", "终检包装", "检验台", 5, 15, 5, 0),
],
},
{
"code": "TPL-RAIL-ASSY", "name": "城轨机构装配(部装锚瓶颈)", "category": "装配",
"keywords": "城轨,机构,部装,总装,干线,门机构",
"description": "现场完整生产路线形态:部装(瓶颈,≈45分/件)→ 总装1…N 串联(≈30分/件)→ 检验交付;"
"BOM 几乎全部消耗在首道部装",
"asset_title": "城轨机构装配路线模板",
"steps": [
(10, "OP-SUBASSY", "机构部装", "共享装配工位", 40, 50, 15, 15),
(20, "OP-ASSY-1", "总装一", "共享装配工位", 25, 35, 10, 10),
(30, "OP-ASSY-2", "总装二", "共享装配工位", 25, 35, 10, 10),
(40, "OP-ASSY-3", "总装三", "共享装配工位", 25, 35, 10, 10),
(50, "OP-INSPECT", "检验交付", "检验台", 10, 20, 5, 0),
],
},
]
def seed_templates() -> int:
"""内置模板落库(幂等);返回模板总数。资产互链:按标题解析 assetId。"""
from server.db.database import get_session
from server.db.models import RoutingTemplate, RoutingTemplateStep
# 标题 → assetId(知识资产互链)
asset_by_title: dict[str, str] = {}
try:
from server.knowledge.assets import get_knowledge
for a in get_knowledge().assets:
asset_by_title[a.get("title") or ""] = a.get("assetId") or ""
except Exception:
pass
with get_session() as session:
existing = {t.code for t in session.execute(select(RoutingTemplate).where(
RoutingTemplate.tenant_uuid == "platform",
RoutingTemplate.deleted == 0,
)).scalars()}
for tpl in _BUILTIN:
if tpl["code"] in existing:
continue
row = RoutingTemplate(
tenant_uuid="platform",
code=tpl["code"], name=tpl["name"], industry="machining",
category=tpl["category"], keywords=tpl["keywords"],
description=tpl["description"],
asset_id=asset_by_title.get(tpl.get("asset_title") or "", ""),
source="builtin",
)
session.add(row)
session.flush()
for (seq, code, name, eq, low, high, setup, chg) in tpl["steps"]:
session.add(RoutingTemplateStep(
tenant_uuid="platform",
template_id=row.id, seq=seq, operation_code=code,
operation_name=name, equipment_type=eq,
std_min_low=float(low), std_min_high=float(high),
setup_min=float(setup), changeover_min=float(chg),
))
session.commit()
return len(session.execute(select(RoutingTemplate).where(
RoutingTemplate.tenant_uuid == "platform",
RoutingTemplate.deleted == 0,
)).scalars().all())
def list_templates() -> list[dict[str, Any]]:
"""模板清单(含步骤)。"""
from server.db.database import get_session
from server.db.models import RoutingTemplate, RoutingTemplateStep
identity = get_identity()
seed_templates()
with get_session() as session:
out = []
for t in session.execute(select(RoutingTemplate).where(
RoutingTemplate.tenant_uuid.in_(("platform", identity.tenant_uuid)),
RoutingTemplate.deleted == 0,
).order_by(RoutingTemplate.id)).scalars():
steps = session.execute(
select(RoutingTemplateStep).where(
RoutingTemplateStep.tenant_uuid == t.tenant_uuid,
RoutingTemplateStep.template_id == t.id,
RoutingTemplateStep.deleted == 0,
)
.order_by(RoutingTemplateStep.seq)).scalars().all()
out.append(_template_dict(t, steps))
return out
def _template_dict(t, steps) -> dict[str, Any]:
return {
"code": t.code, "name": t.name, "industry": t.industry,
"category": t.category, "keywords": t.keywords.split(","),
"description": t.description, "assetId": t.asset_id, "source": t.source,
"steps": [{
"seq": s.seq, "operationCode": s.operation_code, "operationName": s.operation_name,
"equipmentType": s.equipment_type,
"stdMinLow": s.std_min_low, "stdMinHigh": s.std_min_high,
"stdMinDefault": round((s.std_min_low + s.std_min_high) / 2, 1),
"setupMin": s.setup_min, "changeoverMin": s.changeover_min,
} for s in steps],
}
def get_template(code: str) -> dict[str, Any] | None:
return next((t for t in list_templates() if t["code"] == code), None)
def recommend_templates(text: str, top_k: int = 3) -> list[dict[str, Any]]:
"""按产品名/描述关键词推荐模板(缺路线时向导用)。"""
text = (text or "").strip()
scored: list[tuple[float, dict]] = []
for t in list_templates():
score = 0.0
for kw in t["keywords"]:
if kw and kw in text:
score += 2.0
if t["category"] in text:
score += 1.5
for step in t["steps"]:
if step["operationName"] and step["operationName"] in text:
score += 0.5
if score > 0:
scored.append((score, t))
scored.sort(key=lambda x: -x[0])
hits = [dict(t, matchScore=s) for s, t in scored[:top_k]]
if not hits:
# 无关键词命中 → 用 RAG 检索工艺模式知识反查模板
try:
from server.knowledge.assets import get_knowledge
from server.knowledge.retrieval import hybrid_search
rag_hits = hybrid_search(get_knowledge().iter_search_units(), f"{text} 工艺路线", top_k=3)
asset_ids = {h.get("assetId") for h in rag_hits}
hits = [dict(t, matchScore=0.5) for t in list_templates()
if t.get("assetId") in asset_ids][:top_k]
except Exception:
hits = []
return hits
def apply_template_to_product(world: dict[str, Any], template_code: str,
product_code: str, product_name: str = "") -> dict[str, Any]:
"""把模板实例化为产品柔性工艺路线(工时取区间中值,来源=模板)。
调用方负责 P2 确认与落盘;返回摘要。
"""
tpl = get_template(template_code)
if not tpl:
raise ValueError(f"未找到工艺模板:{template_code}")
pn = product_name or product_code
# 已有路线 → 先清掉该产品旧步骤(模板重放)
world["flexRoutings"] = [r for r in world.get("flexRoutings") or []
if r.get("productCode") != product_code]
ops = {o["code"]: o for o in world.get("flexOperations") or []}
equipment = world.get("flexEquipment") or []
added_ops = 0
for step in tpl["steps"]:
code = step["operationCode"]
if code not in ops:
world.setdefault("flexOperations", []).append({
"code": code, "name": step["operationName"],
"isBottleneck": False, "changeoverMin": step["changeoverMin"],
})
ops[code] = world["flexOperations"][-1]
added_ops += 1
# 无能力设备时给运行中设备补该能力(能力池共享;标推断)
if not any(code in (e.get("capabilities") or []) for e in equipment):
for e in equipment:
if e.get("status") == "RUNNING":
e.setdefault("capabilities", []).append(code)
e.setdefault("opStdTime", {})[code] = step["stdMinDefault"]
world.setdefault("flexRoutings", []).append({
"productCode": product_code, "productName": pn,
"seq": step["seq"], "operationCode": code,
"requireMold": False,
"stdTimePerUnit": step["stdMinDefault"],
"stdTimeSource": "模板",
})
# 班组资格覆盖:模板工序并入通用班组(无则新建),避免 NO_TEAM 卡死
tpl_ops = [s["operationCode"] for s in tpl["steps"]]
teams = world.get("flexTeams") or []
generic = next((tm for tm in teams if tm.get("code") == "T-TPL"), None)
if generic is None and teams is not None:
generic = {"code": "T-TPL", "name": "模板工序班组(推断)", "memberCount": 4,
"supportOps": [], "skillLevel": "L2"}
world.setdefault("flexTeams", []).append(generic)
if generic is not None:
for code in tpl_ops:
if code not in generic["supportOps"]:
generic["supportOps"].append(code)
# 产品主数据缺失则补
if not any(m.get("code") == product_code for m in world.get("flexMaterials") or []):
world.setdefault("flexMaterials", []).append({
"code": product_code, "name": pn, "type": "FINISHED_PRODUCT", "unit": "件",
"stock": 0, "inTransit": 0, "safetyStock": 0, "procurementLeadTime": 0,
})
return {"template": tpl["code"], "templateName": tpl["name"],
"productCode": product_code, "steps": len(tpl["steps"]),
"addedOperations": added_ops, "assetId": tpl.get("assetId") or ""}