# -*- coding: utf-8 -*- """Template-Ti 一期 · 交接班摘要生成功能 —— issue #75。 父 Issue #11「④ LLM 报警解释 / 交接班 / NL 查询」子任务: 把班次事件记录生成为**结构化交接班摘要**(本地规则,可离线测试): - 事件归类:production(生产概况)/ equipment(设备运行)/ alarm(异常与处置)/ safety(安全注意事项)/ todo(待办事项); - 异常处置校验:alarm 事件必须含处置记录,缺失 → `needs_followup=True` (对齐交接班规范:异常事项须标注发生时间与处理人,PRD 5.4); - 章节输出:固定五节 Markdown 摘要(to_text),供交接班对话/报表使用; - 与 `TiScenarioRunner.generate_handover`(LLM + RAG 润色)互补: 本模块产出结构化底稿,LLM 层可再润色/补充。 纯本地规则实现(无 LLM 依赖);事件来源:DCS 告警、人工录入、巡检记录。 """ from __future__ import annotations import os from dataclasses import dataclass, field from datetime import date as _date from typing import Dict, List, Optional #: 默认配置资产路径(相对本模块) DEFAULT_CONFIG_PATH = os.path.join( os.path.dirname(os.path.abspath(__file__)), "config", "handover.template.yaml") #: 默认章节定义(key → 标题) DEFAULT_SECTIONS: List[Dict[str, str]] = [ {"key": "production", "title": "生产概况"}, {"key": "equipment", "title": "设备运行状态"}, {"key": "alarm", "title": "异常与处置"}, {"key": "safety", "title": "安全注意事项"}, {"key": "todo", "title": "待办事项"}, ] @dataclass class HandoverReport: """结构化交接班摘要。""" shift: str date: str sections: Dict[str, List[str]] = field(default_factory=dict) needs_followup: bool = False followup_items: List[str] = field(default_factory=list) def to_text(self, section_titles: Optional[Dict[str, str]] = None) -> str: """输出 Markdown 交接班摘要文本。""" titles = section_titles or {s["key"]: s["title"] for s in DEFAULT_SECTIONS} lines = [f"# 交接班摘要({self.date} {self.shift})"] for key, title in titles.items(): items = self.sections.get(key, []) lines.append(f"\n## {title}") lines.append("\n".join(f"- {it}" for it in items) if items else "- (无记录)") if self.needs_followup: lines.append("\n> ⚠ 待跟进:异常事项缺少处置记录,请人工确认。") return "\n".join(lines) def to_dict(self) -> dict: return { "shift": self.shift, "date": self.date, "sections": self.sections, "needs_followup": self.needs_followup, "followup_items": self.followup_items, } class HandoverGenerator: """交接班摘要生成器(事件 → 结构化报告,本地规则)。""" #: 事件类型 → 章节 key(未知类型归入待办) _TYPE_TO_SECTION = { "production": "production", "设备": "production", "equipment": "equipment", "设备运行": "equipment", "alarm": "alarm", "异常": "alarm", "报警": "alarm", "safety": "safety", "安全": "safety", "todo": "todo", "待办": "todo", } def __init__( self, sections: Optional[List[Dict[str, str]]] = None, alarm_requires_disposal: bool = True, default_shift: str = "甲班", ) -> None: self.sections = sections or DEFAULT_SECTIONS self.alarm_requires_disposal = alarm_requires_disposal self.default_shift = default_shift # ------------------------------------------------------------------ @classmethod def from_template_config(cls, path: str = DEFAULT_CONFIG_PATH) -> "HandoverGenerator": """从模板配置资产加载(config/handover.template.yaml)。""" import yaml with open(path, "r", encoding="utf-8") as fh: raw = yaml.safe_load(fh) or {} cfg = raw.get("handover", {}) or {} return cls( sections=cfg.get("sections") or DEFAULT_SECTIONS, alarm_requires_disposal=bool( cfg.get("alarm_requires_disposal", True)), default_shift=cfg.get("default_shift", "甲班"), ) # ------------------------------------------------------------------ def generate( self, events: List[dict], shift: Optional[str] = None, date: Optional[str] = None, ) -> HandoverReport: """按事件记录生成交接班摘要。 Args: events: `[{type, text, time?, operator?}]`;type 见 `_TYPE_TO_SECTION`; shift: 班次(缺省 = 配置默认); date: 日期(缺省 = 今天)。 """ sections: Dict[str, List[str]] = {s["key"]: [] for s in self.sections} report = HandoverReport( shift=shift or self.default_shift, date=date or _date.today().isoformat(), sections=sections, ) for ev in events: section = self._section_for(ev.get("type", "")) text = self._format_event(ev) sections.setdefault(section, []).append(text) if section == "alarm": self._check_disposal(ev, report) if not sections.get("alarm"): sections["alarm"] = ["本班无异常事项"] return report # ------------------------------------------------------------------ def _section_for(self, event_type: str) -> str: return self._TYPE_TO_SECTION.get(event_type.strip(), "todo") @staticmethod def _format_event(ev: dict) -> str: time_ = ev.get("time") operator = ev.get("operator") text = str(ev.get("text", "")).strip() parts = [] if time_: parts.append(f"[{time_}]") parts.append(text or "(无描述)") if operator: parts.append(f"(处理人: {operator})") return " ".join(parts) def _check_disposal(self, ev: dict, report: HandoverReport) -> None: """异常事件须含处置记录(disposal 或 operator),否则标记待跟进。""" if not self.alarm_requires_disposal: return disposal = ev.get("disposal") or ev.get("operator") if not disposal: report.needs_followup = True report.followup_items.append( f"异常事件缺处置记录: {ev.get('text', '')[:40]}")