225 lines
9.9 KiB
Python
225 lines
9.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Ti 场景编排:报警解释 / 交接班摘要 / NL 查询驾驶舱(EPIC #11)。
|
|
|
|
复用内核组件(零内核改动,换行业只改模板资产):
|
|
- ``llm_gateway.LLMGateway``:混合网关主编排(敏感度路由 → 生成 → 幻觉校验 → DLP);
|
|
- ``llm_gateway.prompts.PromptRegistry``:提示词版本库(alarm_explain / shift_handover / qa);
|
|
- ``llm_gateway.router.SensitivityRouter``:敏感度路由(行业规则 + 内置保底);
|
|
- ``rag_kb.RagKnowledgeBase``:领域 RAG(工艺规范 / SOP / 国标三类知识源)。
|
|
|
|
三个场景均通过 ``TiScenarioRunner`` 暴露,业务方只需一行调用:
|
|
runner = TiScenarioRunner()
|
|
result = runner.explain_alarm("氯化炉炉温异常")
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Callable, Dict, List, Optional
|
|
|
|
from llm_gateway.gateway import GatewayResult, LLMGateway
|
|
from llm_gateway.prompts import PromptRegistry
|
|
from llm_gateway.router import SensitivityRouter
|
|
from rag_kb import KnowledgeSourceKind, RagKnowledgeBase, load_kb_config
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 场景配置资产路径
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _scenarios_dir() -> str:
|
|
return os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def _repo_root() -> str:
|
|
"""仓库根目录(scenarios.py 向上 4 层:llm-scenarios → ti-cl4 → templates → 根)。"""
|
|
return os.path.dirname(os.path.dirname(os.path.dirname(
|
|
os.path.dirname(os.path.abspath(__file__)))))
|
|
|
|
|
|
def scenarios_config_path() -> str:
|
|
"""场景配置资产(scenarios.template.yaml)路径。"""
|
|
return os.path.join(_scenarios_dir(), "config", "scenarios.template.yaml")
|
|
|
|
|
|
def alarm_config_path() -> str:
|
|
"""报警解释场景配置资产(alarm_explain.template.yaml)路径。"""
|
|
return os.path.join(_scenarios_dir(), "config",
|
|
"alarm_explain.template.yaml")
|
|
|
|
|
|
def prompts_config_path() -> str:
|
|
"""复用 llm-gateway 提示词版本库资产路径。"""
|
|
return os.path.join(_repo_root(), "core", "llm-gateway", "config",
|
|
"prompts.template.yaml")
|
|
|
|
|
|
def kb_config_path() -> str:
|
|
"""复用 rag-kb 知识库模板资产路径。"""
|
|
return os.path.join(_repo_root(), "core", "rag-kb", "config",
|
|
"kb.template.yaml")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 演示知识文档集(Ti 场景开箱即用;接入真实客户数据时替换 loader 即可)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
DEMO_KB_DOCS: Dict[str, str] = {
|
|
"沸腾氯化工艺规范": (
|
|
"沸腾氯化炉采用流态化氯化工艺,炉温控制在 850±50℃,"
|
|
"氯气流量按加料量比例调节,加料比保持 1:2.4~1:2.8。"
|
|
"炉温异常时优先检查氯气流量与加料系统。"
|
|
),
|
|
"沸腾氯化炉操作手册": (
|
|
"开机前确认氯气缓冲罐压力、炉体密封与尾气处理系统正常;"
|
|
"运行中每 30 分钟记录一次炉温、氯气流量与出料量。"
|
|
),
|
|
"沸腾氯化炉异常处置SOP": (
|
|
"SOP-CL-001:炉温超上限(>900℃)时立即降低氯气流量并减少加料,"
|
|
"若 10 分钟内未回落则按紧急停机流程处理,并通知当班班长。"
|
|
),
|
|
"交接班报告生成规范": (
|
|
"交接班报告须包含:当班生产概况、设备运行状态、异常与处置记录、"
|
|
"安全注意事项、待办事项。异常事项必须标注发生时间与处理人。"
|
|
),
|
|
"GB/T 氯气安全使用标准": (
|
|
"氯气属于剧毒气体,作业场所应配备气体泄漏检测与报警装置,"
|
|
"作业人员须佩戴防护用品,泄漏时启动应急程序并疏散无关人员。"
|
|
),
|
|
"GB/T 钛及钛合金加工标准": (
|
|
"海绵钛产品纯度按 GB/T 标准分级,钛纯度 ≥99.5% 为一级品;"
|
|
"氯化产物杂质含量影响最终钛纯度,须按批次检验并留样。"
|
|
),
|
|
}
|
|
|
|
|
|
def demo_kb_loader(title: str) -> str:
|
|
"""演示文档加载器:按标题返回内置演示文本(换数据源时替换本函数)。"""
|
|
return DEMO_KB_DOCS.get(title, "")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Ti 场景编排
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TiScenarioRunner:
|
|
"""Template-Ti 一期 LLM 场景统一入口(EPIC #11 主体交付)。
|
|
|
|
构造参数均可注入(便于测试与替换真实组件);缺省使用仓库模板资产
|
|
(提示词版本库 + 领域 RAG + 行业路由规则)构建完整场景编排。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
gateway: Optional[LLMGateway] = None,
|
|
kb: Optional[RagKnowledgeBase] = None,
|
|
prompts: Optional[PromptRegistry] = None,
|
|
router: Optional[SensitivityRouter] = None,
|
|
kb_loader: Callable[[str], str] = demo_kb_loader,
|
|
alarm_config: Optional[dict] = None,
|
|
) -> None:
|
|
prompts = prompts or PromptRegistry.from_template_config(prompts_config_path())
|
|
kb = kb or RagKnowledgeBase.from_template_config(
|
|
load_kb_config(kb_config_path()), loader=kb_loader,
|
|
)
|
|
router = router or SensitivityRouter.from_template_config(router_config_path())
|
|
self.kb = kb
|
|
# 报警解释场景配置(issue #74:sop 类目检索 + 检索阈值)
|
|
self.alarm_cfg = alarm_config or self._load_alarm_config()
|
|
if gateway is not None:
|
|
# 注入 gateway:由调用方负责 prompt_name / 高利害配置
|
|
self._gateways = {
|
|
"alarm_explain": gateway,
|
|
"shift_handover": gateway,
|
|
"nl_query": gateway,
|
|
}
|
|
else:
|
|
# 每个场景绑定各自的提示词模板版本(可复现)
|
|
base = dict(prompts=prompts, router=router)
|
|
self._gateways = {
|
|
# 报警解释:高利害,命中即启用幻觉校验信度阈值 → 低信度转人工
|
|
"alarm_explain": LLMGateway(
|
|
prompt_name="alarm_explain",
|
|
high_stakes_names={"alarm_explain"}, **base,
|
|
),
|
|
"shift_handover": LLMGateway(
|
|
prompt_name="shift_handover", **base,
|
|
),
|
|
"nl_query": LLMGateway(prompt_name="qa", **base),
|
|
}
|
|
|
|
# -- 场景 1:报警根因解释(高利害) -----------------------------------
|
|
|
|
@staticmethod
|
|
def _load_alarm_config() -> dict:
|
|
"""加载报警解释场景配置资产(issue #74)。"""
|
|
import yaml
|
|
|
|
with open(alarm_config_path(), "r", encoding="utf-8") as fh:
|
|
return (yaml.safe_load(fh) or {}).get("alarm_explain", {})
|
|
|
|
def explain_alarm(self, alarm: str, confidence: float = 1.0,
|
|
top_k: Optional[int] = None) -> GatewayResult:
|
|
"""解释一条报警的可能原因与处置建议。
|
|
|
|
RAG 接入(issue #74):只检索 **sop 知识域**(异常处置 SOP),
|
|
并按 min_score 过滤——低于阈值视为未命中,降级提示人工确认
|
|
(高利害场景宁缺毋滥,避免无据回答)。
|
|
"""
|
|
rag = self.alarm_cfg.get("rag", {}) or {}
|
|
cat_names = rag.get("categories")
|
|
categories = ([KnowledgeSourceKind(c) for c in cat_names]
|
|
if cat_names else None)
|
|
top_k = top_k or int(rag.get("top_k", 3))
|
|
min_score = float(rag.get("min_score", 0.0))
|
|
hits = self.kb.search(alarm, top_k=top_k, categories=categories)
|
|
hits = [h for h in hits if h.score >= min_score]
|
|
if hits:
|
|
sources = [h.source for h in hits]
|
|
else:
|
|
# 未检索到 SOP:降级提示人工确认(来源占位,阻断无据回答)
|
|
sources = ["未检索到相关 SOP,请人工确认"]
|
|
return self._gateways["alarm_explain"].ask(
|
|
alarm, rag_context=sources, confidence=confidence,
|
|
)
|
|
|
|
# -- 场景 2:交接班自动摘要 -------------------------------------------
|
|
|
|
def generate_handover(self, shift_desc: str, confidence: float = 1.0,
|
|
top_k: int = 3) -> GatewayResult:
|
|
"""按班次情况生成交接班摘要(RAG 检索交接班规范)。"""
|
|
hits = self.kb.search(shift_desc, top_k=top_k, categories=None)
|
|
sources = [h.source for h in hits]
|
|
return self._gateways["shift_handover"].ask(
|
|
shift_desc, rag_context=sources, confidence=confidence,
|
|
)
|
|
|
|
# -- 场景 3:自然语言查询驾驶舱(NL → 查询意图/指标) -----------------
|
|
|
|
def query_cockpit(self, question: str, confidence: float = 1.0,
|
|
top_k: int = 3) -> GatewayResult:
|
|
"""把自然语言问题映射为驾驶舱查询意图(RAG 检索工艺指标定义)。"""
|
|
hits = self.kb.search(question, top_k=top_k, categories=None)
|
|
sources = [h.source for h in hits]
|
|
return self._gateways["nl_query"].ask(
|
|
question, rag_context=sources, confidence=confidence,
|
|
)
|
|
|
|
# -- 审计 --------------------------------------------------------------
|
|
|
|
def drain_audits(self) -> Dict[str, List[Dict[str, object]]]:
|
|
"""取走网关各组件审计记录(DLP/路由/Prompt/幻觉校验)。"""
|
|
audits: Dict[str, List[Dict[str, object]]] = {
|
|
"dlp": [], "router": [], "prompts": [], "guard": [],
|
|
}
|
|
for gw in set(self._gateways.values()):
|
|
for key, rows in gw.drain_audits().items():
|
|
audits.setdefault(key, []).extend(rows)
|
|
return audits
|
|
|
|
|
|
def router_config_path() -> str:
|
|
"""复用 llm-gateway 行业路由规则资产路径。"""
|
|
return os.path.join(_repo_root(), "core", "llm-gateway", "config",
|
|
"router.template.yaml")
|