Files
mcp-server-demo/src/modules/bexell/tools.py
T

279 lines
9.7 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.
"""
MCP for Bexell — 试产排程评估及优化系统
使用 MCPServer 框架,4 个工具:设备产能查询、排产占用查询、品号负荷查询、试产产能评估
HTTP 模式,挂载在 /bexell/mcp 路径
"""
import json
from datetime import datetime
from typing import Dict, List
from sqlalchemy import func, text
from mcp.server import MCPServer
from auth import get_auth
from .db import get_db
from .models import EquipmentCapacity, ProductEquipmentLoad, ProductEquipmentSchedule
# 鉴权:独立 Bearer Token,API Key 来自 BEXELL_MCP_AUTH_API_KEY 环境变量
_auth, _token_verifier = get_auth(service="BEXELL")
bexell_server = MCPServer(
name="bexell-scheduling",
description="试产排程评估及优化系统",
version="1.0.0",
auth=_auth,
token_verifier=_token_verifier,
)
# ============================================================
# 工具 1: get_equipment_by_type — 按设备类型查询设备产能信息
# ============================================================
@bexell_server.tool()
async def get_equipment_by_type(equipment_type: str) -> List[Dict]:
"""
根据设备类型获取该类型所有设备的基础产能信息。
Args:
equipment_type: 设备类型,如"冲压"、"表面"、"检测"
Returns:
设备列表,每个设备包含:
- equipment_code: 设备编号
- equipment_name: 设备名称
- equipment_type: 设备类型
- max_capacity: 极限产能(H)
- standard_capacity: 标准产能(H)
- min_startup: 最小开机产能(H)
- line_change_cost: 换线成本系数
"""
db = next(get_db())
try:
rows = db.query(EquipmentCapacity).filter(
EquipmentCapacity.equipment_type == equipment_type
).order_by(EquipmentCapacity.equipment_code).all()
return [{
"equipment_code": r.equipment_code,
"equipment_name": r.equipment_name,
"equipment_type": r.equipment_type,
"max_capacity": float(r.max_capacity),
"standard_capacity": float(r.standard_capacity),
"min_startup": float(r.min_startup),
"line_change_cost": float(r.line_change_cost),
} for r in rows]
finally:
db.close()
# ============================================================
# 工具 2: get_schedule_occupied — 查询设备排产占用产能合计
# ============================================================
@bexell_server.tool()
async def get_schedule_occupied(equipment_type: str) -> List[Dict]:
"""
获取指定类型全部设备的已占用产能汇总。
Args:
equipment_type: 设备类型,如"冲压"、"表面"、"检测"
Returns:
设备占用列表,每个设备包含:
- equipment_code: 设备编号
- equipment_name: 设备名称
- total_occupied_capacity: 占用产能合计(H)
"""
db = next(get_db())
try:
rows = db.query(
ProductEquipmentSchedule.equipment_code,
ProductEquipmentSchedule.equipment_name,
func.coalesce(
func.sum(ProductEquipmentSchedule.occupied_capacity), 0
).label("total_occupied")
).filter(
ProductEquipmentSchedule.equipment_type == equipment_type
).group_by(
ProductEquipmentSchedule.equipment_code,
ProductEquipmentSchedule.equipment_name
).order_by(
ProductEquipmentSchedule.equipment_code
).all()
return [{
"equipment_code": r.equipment_code,
"equipment_name": r.equipment_name,
"total_occupied_capacity": float(r.total_occupied),
} for r in rows]
finally:
db.close()
# ============================================================
# 工具 3: get_product_load — 查询品号在各设备的标准产能
# ============================================================
@bexell_server.tool()
async def get_product_load(product_code: str, equipment_type: str) -> List[Dict]:
"""
获取指定品号在目标设备类型各设备上的标准产能。
Args:
product_code: 品号,如"A0003"
equipment_type: 设备类型,如"冲压"、"表面"、"检测"
Returns:
品号设备负荷列表,每条记录包含:
- product_code: 品号
- product_name: 品名
- equipment_code: 设备编号
- equipment_name: 设备名称
- standard_capacity: 该品号在该设备上的标准产能
"""
db = next(get_db())
try:
rows = db.query(ProductEquipmentLoad).join(
EquipmentCapacity,
ProductEquipmentLoad.equipment_code == EquipmentCapacity.equipment_code
).filter(
ProductEquipmentLoad.product_code == product_code,
EquipmentCapacity.equipment_type == equipment_type
).order_by(
ProductEquipmentLoad.equipment_code
).all()
return [{
"product_code": r.product_code,
"product_name": r.product_name,
"equipment_code": r.equipment_code,
"equipment_name": r.equipment_name,
"standard_capacity": float(r.standard_capacity),
} for r in rows]
finally:
db.close()
# ============================================================
# 工具 4: evaluate_trial_capacity — 一体化试产产能评估
# ============================================================
@bexell_server.tool()
async def evaluate_trial_capacity(
product_code: str,
trial_quantity: int
) -> Dict:
"""
一体化试产产能评估:计算指定品号在指定设备类型下所有设备的可用产能,
并推荐产能最大值设备。
计算公式:
可用产能 = 极限产能 − 占用产能合计 − 换线成本 − 最小开机×0.1 − 试产数量×品号标准产能
Args:
product_code: 品号,如"A0003"
trial_quantity: 试产数量,如 10
Returns:
评估结果字典,包含:
- product_code: 品号
- product_name: 品名
- trial_quantity: 试产数量
- evaluated_at: 评估时间
- results: 设备评估明细列表(按可用产能降序)
- recommended: 推荐设备(可用产能最大且>=0)
"""
db = next(get_db())
try:
# 查询品名
product = db.execute(text(
"SELECT product_name FROM product_info WHERE product_code = :code"
), {"code": product_code}).first()
if not product:
return {"error": f"品号 {product_code} 不存在"}
# 核心聚合 SQL
sql = text("""
SELECT
ec.equipment_code,
ec.equipment_name,
ec.equipment_type,
ec.max_capacity,
ec.line_change_cost,
ec.min_startup * 0.1 AS min_startup_buffer,
COALESCE(occ.total_occupied, 0) AS occupied_capacity,
pel.standard_capacity AS product_standard_capacity,
:trial_qty * pel.standard_capacity AS trial_demand,
(ec.max_capacity
- COALESCE(occ.total_occupied, 0)
- ec.line_change_cost
- ec.min_startup * 0.1
- :trial_qty * pel.standard_capacity
) AS available_capacity
FROM equipment_capacity ec
LEFT JOIN (
SELECT equipment_code, SUM(occupied_capacity) AS total_occupied
FROM product_equipment_schedule
GROUP BY equipment_code
) occ ON ec.equipment_code = occ.equipment_code
JOIN product_equipment_load pel
ON ec.equipment_code = pel.equipment_code
AND pel.product_code = :product_code
WHERE ec.equipment_type = (
SELECT equipment_type FROM equipment_capacity ec2
WHERE ec2.equipment_code = pel.equipment_code
LIMIT 1
)
ORDER BY available_capacity DESC
""")
rows = db.execute(sql, {
"product_code": product_code,
"trial_qty": trial_quantity
}).mappings().all()
# 组装结果
results = []
for idx, r in enumerate(rows, 1):
available = float(r.available_capacity)
results.append({
"rank": idx,
"equipment_code": r.equipment_code,
"equipment_name": r.equipment_name,
"equipment_type": r.equipment_type,
"max_capacity": float(r.max_capacity),
"occupied_capacity": float(r.occupied_capacity),
"line_change_cost": float(r.line_change_cost),
"min_startup_buffer": float(r.min_startup_buffer),
"product_standard_capacity": float(r.product_standard_capacity),
"trial_demand": float(r.trial_demand),
"available_capacity": available,
"status": "可用" if available >= 0 else "已满负荷",
})
# 推荐设备(第一个可用且 >= 0 的)
recommended = None
for r in results:
if r["available_capacity"] >= 0:
recommended = {
"equipment_code": r["equipment_code"],
"equipment_name": r["equipment_name"],
"available_capacity": r["available_capacity"],
}
break
return {
"product_code": product_code,
"product_name": product.product_name,
"trial_quantity": trial_quantity,
"evaluated_at": datetime.now().isoformat(),
"results": results,
"recommended": recommended,
}
finally:
db.close()