143 lines
8.8 KiB
Python
143 lines
8.8 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 知识资产库(moduleId: knowledge-assets, 可重生 ✅, 黄金测试 tests/golden/test_m3_knowledge.py)
|
|||
|
|
# plan.md §8.1:四类资产(算法知识/企业 SOP/历史案例/工艺知识),
|
|||
|
|
# 版本化 + 入库审批标记 + 检索强制带出处。
|
|||
|
|
# M3 形态:单文件 JSON + 种子资产;接口稳定,后续可换向量库不改调用方。
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations # 前向类型引用
|
|||
|
|
|
|||
|
|
import json # 序列化
|
|||
|
|
import os # 路径
|
|||
|
|
import tempfile # 原子写
|
|||
|
|
import threading # 互斥
|
|||
|
|
import uuid # 资产 ID
|
|||
|
|
from datetime import datetime # 时间戳
|
|||
|
|
from typing import Any # 类型标注
|
|||
|
|
|
|||
|
|
from server.timeutil import fmt_dt # 时间格式化
|
|||
|
|
|
|||
|
|
# 存储路径(与世界状态同目录)
|
|||
|
|
_PATH_ENV = "APS_KNOWLEDGE_PATH"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _seed_assets() -> list[dict[str, Any]]:
|
|||
|
|
"""种子知识资产(演示语料;真实项目由导入管线 §8.4 入库)。"""
|
|||
|
|
now = fmt_dt(datetime.now()) # 统一时间戳
|
|||
|
|
def asset(kind: str, title: str, content: str, tags: list[str]) -> dict[str, Any]:
|
|||
|
|
return { # 资产结构(DatasetAsset 的知识侧近亲)
|
|||
|
|
"assetId": uuid.uuid4().hex[:10], # 资产 ID(@知识 引用用)
|
|||
|
|
"kind": kind, # algorithm / sop / case / process
|
|||
|
|
"title": title, # 标题(引用与出处展示)
|
|||
|
|
"content": content, # 正文(M3 单块;后续分 chunk)
|
|||
|
|
"tags": tags, # 检索加权标签
|
|||
|
|
"version": "v1", # 版本号(重复入库递增)
|
|||
|
|
"approved": True, # 审批标记(§8.1 入库需审批)
|
|||
|
|
"createdAt": now, # 入库时间
|
|||
|
|
}
|
|||
|
|
return [
|
|||
|
|
asset("sop", "换线标准SOP",
|
|||
|
|
"产线换型必须遵守:① 同产品族连续生产优先,减少换线;② 换线作业标准 30 分钟,"
|
|||
|
|
"跨产品族换线 60 分钟;③ 每日 16:00 后不安排换线(夜班技师不足);"
|
|||
|
|
"④ 换线前必须完成上一批次尾检,未尾检不得拆线。",
|
|||
|
|
["换线", "SOP", "产品族", "夜班"]),
|
|||
|
|
asset("sop", "插单审批流程",
|
|||
|
|
"VIP 客户插单:计划员评估影响(延迟/成本)→ 主管审批 → 发布新版本。"
|
|||
|
|
"非 VIP 插单原则上排入下一个计划周期;确需插入的走例外审批,须附影响评估报告。",
|
|||
|
|
["插单", "审批", "VIP", "例外"]),
|
|||
|
|
asset("algorithm", "交期优先策略说明",
|
|||
|
|
"DELIVERY_FIRST 按交期升序 + 客户等级加权排序订单,优先保障最紧交期。"
|
|||
|
|
"适用:交付压力大、订单交期集中。代价:可能牺牲产能均衡与换线次数。"
|
|||
|
|
"参数:客户等级权重(VIP=3/重要=2/普通=1),同交期按下单时间先到先排。",
|
|||
|
|
["交期优先", "DELIVERY_FIRST", "策略", "权重"]),
|
|||
|
|
asset("algorithm", "产能均衡策略说明",
|
|||
|
|
"CAPACITY_BALANCE 以产线负荷方差最小为目标分配订单,避免忙闲不均。"
|
|||
|
|
"适用:多产线可互替、瓶颈不固定。代价:个别紧急订单可能被排后。"
|
|||
|
|
"建议与交期承诺看板联用,均衡后逐单核对交付风险。",
|
|||
|
|
["产能均衡", "CAPACITY_BALANCE", "负荷", "方差"]),
|
|||
|
|
asset("process", "产线A设备能力",
|
|||
|
|
"产线A(L001)适合小批量多品种:SMT 贴片 + 波峰焊;单班产能 400 标准件/日;"
|
|||
|
|
"维保窗口每周三 08:00-10:00 固定停机;夜班仅一班技师,复杂换型不排夜班。",
|
|||
|
|
["产线A", "L001", "产能", "维保"]),
|
|||
|
|
asset("case", "6月底比亚迪插单复盘",
|
|||
|
|
"6月28日比亚迪 VIP 插单 2000 件:采用交期优先重排,牺牲产线B均衡度 12%,"
|
|||
|
|
"全部订单按期交付。经验:VIP 插单后应立即跑一次沙盒对比,确认对存量订单影响 <5% 再发布。",
|
|||
|
|
["比亚迪", "插单", "复盘", "沙盒"]),
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class KnowledgeStore:
|
|||
|
|
"""知识资产仓:加载/列出/取回/新增(单文件 JSON + 原子写;P0 读 / P1 新增)。"""
|
|||
|
|
|
|||
|
|
def __init__(self, path: str | None = None) -> None:
|
|||
|
|
"""初始化:加载资产,空库自动播种。权力等级 P0(构造只读+首次播种)。"""
|
|||
|
|
self.path = path or os.environ.get(_PATH_ENV, "server/data/knowledge.json") # 存储路径
|
|||
|
|
self._lock = threading.Lock() # 并发保护
|
|||
|
|
self.assets: list[dict[str, Any]] = self._load() # 资产列表
|
|||
|
|
if not self.assets: # 空库播种
|
|||
|
|
self.assets = _seed_assets() # 种子资产
|
|||
|
|
self._write() # 落盘
|
|||
|
|
|
|||
|
|
def _load(self) -> list[dict[str, Any]]:
|
|||
|
|
"""从磁盘加载;缺失/损坏返回空(触发播种)。"""
|
|||
|
|
try:
|
|||
|
|
with open(self.path, "r", encoding="utf-8") as f: # 读文件
|
|||
|
|
return json.load(f).get("assets", []) # 取资产数组
|
|||
|
|
except (FileNotFoundError, json.JSONDecodeError): # 缺失/损坏
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
def _write(self) -> None:
|
|||
|
|
"""原子写盘(tmp + os.replace,与 WorldStore 同法)。"""
|
|||
|
|
os.makedirs(os.path.dirname(self.path) or ".", exist_ok=True) # 确保目录
|
|||
|
|
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(self.path) or ".", suffix=".tmp") # 临时文件
|
|||
|
|
try:
|
|||
|
|
with os.fdopen(fd, "w", encoding="utf-8") as f: # 写临时文件
|
|||
|
|
json.dump({"assets": self.assets}, f, ensure_ascii=False, indent=1) # 序列化
|
|||
|
|
os.replace(tmp, self.path) # 原子替换
|
|||
|
|
except BaseException: # 失败清理
|
|||
|
|
if os.path.exists(tmp):
|
|||
|
|
os.unlink(tmp)
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def list_meta(self) -> list[dict[str, Any]]:
|
|||
|
|
"""资产元信息清单(不含正文;知识面板/命令面板数据源,P0)。"""
|
|||
|
|
return [{k: a[k] for k in ("assetId", "kind", "title", "version", "createdAt")}
|
|||
|
|
for a in self.assets]
|
|||
|
|
|
|||
|
|
def get(self, asset_id: str) -> dict[str, Any] | None:
|
|||
|
|
"""按 ID 取完整资产(@知识 引用解析用,P0)。"""
|
|||
|
|
return next((a for a in self.assets if a["assetId"] == asset_id), None)
|
|||
|
|
|
|||
|
|
def find_by_title(self, title: str) -> dict[str, Any] | None:
|
|||
|
|
"""按标题模糊取资产(@知识:换线标准SOP 的解析路径,P0)。"""
|
|||
|
|
t = title.strip() # 清理输入
|
|||
|
|
return next((a for a in self.assets if t and t in a["title"]), None)
|
|||
|
|
|
|||
|
|
def add(self, kind: str, title: str, content: str, tags: list[str] | None = None) -> dict[str, Any]:
|
|||
|
|
"""新增资产(P1:只新增知识,不碰世界状态;报告入库走此口 §9.10 规则3)。"""
|
|||
|
|
with self._lock: # 串行化写
|
|||
|
|
existing = self.find_by_title(title) # 同名视为新版本
|
|||
|
|
version = "v1" # 默认首版
|
|||
|
|
if existing and existing["title"] == title: # 完全同名
|
|||
|
|
version = f"v{int(existing['version'].lstrip('v')) + 1}" # 版本递增
|
|||
|
|
asset = { # 组装资产
|
|||
|
|
"assetId": uuid.uuid4().hex[:10], "kind": kind, "title": title,
|
|||
|
|
"content": content, "tags": tags or [], "version": version,
|
|||
|
|
"approved": kind != "report", # 报告类默认待审(对外须人工批 §9.10 规则4)
|
|||
|
|
"createdAt": fmt_dt(datetime.now()),
|
|||
|
|
}
|
|||
|
|
self.assets.append(asset) # 入库
|
|||
|
|
self._write() # 落盘
|
|||
|
|
return asset # 返回
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- 进程级单例 ----------------
|
|||
|
|
_store: KnowledgeStore | None = None # 单例槽
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_knowledge() -> KnowledgeStore:
|
|||
|
|
"""取知识库单例(gateway/workflow 共用)。"""
|
|||
|
|
global _store
|
|||
|
|
if _store is None: # 首次创建
|
|||
|
|
_store = KnowledgeStore()
|
|||
|
|
return _store
|